Skip to content

Commit

Permalink
perf(spec-parser): remove unused properties when filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
SLdragon committed Jun 21, 2024
1 parent d90b957 commit 5502da6
Show file tree
Hide file tree
Showing 3 changed files with 837 additions and 1 deletion.
3 changes: 2 additions & 1 deletion packages/spec-parser/src/specFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SpecParserError } from "./specParserError";
import { ErrorType, ParseOptions } from "./interfaces";
import { ConstantString } from "./constants";
import { ValidatorFactory } from "./validators/validatorFactory";
import { SpecOptimizer } from "./specOptimizer";

export class SpecFilter {
static specFilter(
Expand Down Expand Up @@ -55,7 +56,7 @@ export class SpecFilter {
}

newSpec.paths = newPaths;
return newSpec;
return SpecOptimizer.optimize(newSpec);
} catch (err) {
throw new SpecParserError((err as Error).toString(), ErrorType.FilterSpecFailed);
}
Expand Down
206 changes: 206 additions & 0 deletions packages/spec-parser/src/specOptimizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
"use strict";

import { OpenAPIV3 } from "openapi-types";

export interface OptimizerOptions {
removeUnusedComponents: boolean;
removeUnusedTags: boolean;
removeUserDefinedRootProperty: boolean;
removeUnusedSecuritySchemas: boolean;
}

export class SpecOptimizer {
private static defaultOptions: OptimizerOptions = {
removeUnusedComponents: true,
removeUnusedTags: true,
removeUserDefinedRootProperty: true,
removeUnusedSecuritySchemas: true,
};

static optimize(spec: OpenAPIV3.Document, options?: OptimizerOptions): OpenAPIV3.Document {
const mergedOptions = {
...SpecOptimizer.defaultOptions,
...(options ?? {}),
} as Required<OptimizerOptions>;

const newSpec = JSON.parse(JSON.stringify(spec));

if (mergedOptions.removeUserDefinedRootProperty) {
SpecOptimizer.removeUserDefinedRootProperty(newSpec);
}

if (mergedOptions.removeUnusedComponents) {
SpecOptimizer.removeUnusedComponents(newSpec);
}

if (mergedOptions.removeUnusedTags) {
SpecOptimizer.removeUnusedTags(newSpec);
}

if (mergedOptions.removeUnusedSecuritySchemas) {
SpecOptimizer.removeUnusedSecuritySchemas(newSpec);
}

return newSpec;
}

private static removeUnusedSecuritySchemas(spec: OpenAPIV3.Document): void {
if (!spec.components || !spec.components.securitySchemes) {
return;
}

const usedSecuritySchemas = new Set<string>();

for (const pathKey in spec.paths) {
for (const methodKey in spec.paths[pathKey]) {
const operation: OpenAPIV3.OperationObject = (spec.paths[pathKey] as any)[methodKey];
if (operation.security) {
operation.security.forEach((securityReq) => {
for (const schemaKey in securityReq) {
usedSecuritySchemas.add(schemaKey);
}
});
}
}
}

if (spec.security) {
spec.security.forEach((securityReq) => {
for (const schemaKey in securityReq) {
usedSecuritySchemas.add(schemaKey);

Check warning on line 72 in packages/spec-parser/src/specOptimizer.ts

View check run for this annotation

Codecov / codecov/patch

packages/spec-parser/src/specOptimizer.ts#L70-L72

Added lines #L70 - L72 were not covered by tests
}
});
}

for (const schemaKey in spec.components.securitySchemes) {
if (!usedSecuritySchemas.has(schemaKey)) {
delete spec.components.securitySchemes[schemaKey];
}
}

if (Object.keys(spec.components.securitySchemes).length === 0) {
delete spec.components.securitySchemes;
}

if (Object.keys(spec.components).length === 0) {
delete spec.components;
}
}

private static removeUnusedTags(spec: OpenAPIV3.Document): void {
if (!spec.tags) {
return;
}

const usedTags = new Set<string>();

for (const pathKey in spec.paths) {
for (const methodKey in spec.paths[pathKey]) {
const operation: OpenAPIV3.OperationObject = (spec.paths[pathKey] as any)[methodKey];
if (operation.tags) {
operation.tags.forEach((tag) => usedTags.add(tag));
}
}
}

spec.tags = spec.tags.filter((tagObj) => usedTags.has(tagObj.name));
}

private static removeUserDefinedRootProperty(spec: OpenAPIV3.Document): void {
for (const key in spec) {
if (key.startsWith("x-")) {
delete (spec as any)[key];
}
}
}

private static removeUnusedComponents(spec: OpenAPIV3.Document): void {
const components = spec.components;
if (!components) {
return;
}

delete spec.components;

const usedComponentsSet = new Set<string>();

const specString = JSON.stringify(spec);
const componentReferences = SpecOptimizer.getComponentReferences(specString);

for (const reference of componentReferences) {
this.addComponent(reference, usedComponentsSet, components);
}

const newComponents: any = {};

for (const componentName of usedComponentsSet) {
const parts = componentName.split("/");
const component = this.getComponent(componentName, components);
if (component) {
let current = newComponents;
for (let i = 2; i < parts.length; i++) {
if (i === parts.length - 1) {
current[parts[i]] = component;
} else if (!current[parts[i]]) {
current[parts[i]] = {};
}
current = current[parts[i]];
}
}
}

// securitySchemes are referenced directly by name, to void issue, just keep them all and use removeUnusedSecuritySchemas to remove unused ones
if (components.securitySchemes) {
newComponents.securitySchemes = components.securitySchemes;
}

if (Object.keys(newComponents).length !== 0) {
spec.components = newComponents;
}
}

private static getComponentReferences(specString: string): string[] {
const matches = Array.from(specString.matchAll(/['"](#\/components\/.+?)['"]/g));
const matchResult = matches.map((match) => match[1]);
return matchResult;
}

private static getComponent(componentPath: string, components: OpenAPIV3.ComponentsObject): any {
const parts = componentPath.split("/");
let current: any = components;

for (const part of parts) {
if (part === "#" || part === "components") {
continue;
}
current = current[part];
if (!current) {
return null;

Check warning on line 180 in packages/spec-parser/src/specOptimizer.ts

View check run for this annotation

Codecov / codecov/patch

packages/spec-parser/src/specOptimizer.ts#L180

Added line #L180 was not covered by tests
}
}

return current;
}

private static addComponent(
componentName: string,
usedComponentsSet: Set<string>,
components: OpenAPIV3.ComponentsObject
) {
if (usedComponentsSet.has(componentName)) {
return;

Check warning on line 193 in packages/spec-parser/src/specOptimizer.ts

View check run for this annotation

Codecov / codecov/patch

packages/spec-parser/src/specOptimizer.ts#L193

Added line #L193 was not covered by tests
}
usedComponentsSet.add(componentName);

const component = this.getComponent(componentName, components);
if (component) {
const componentString = JSON.stringify(component);
const componentReferences = SpecOptimizer.getComponentReferences(componentString);
for (const reference of componentReferences) {
this.addComponent(reference, usedComponentsSet, components);
}
}
}
}
Loading

0 comments on commit 5502da6

Please sign in to comment.