Skip to content

Commit

Permalink
add content type whitelist option
Browse files Browse the repository at this point in the history
  • Loading branch information
lzurbriggen committed Dec 20, 2023
1 parent 3581f6d commit 4b3e633
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 19 deletions.
77 changes: 67 additions & 10 deletions bin/openapi-simplifier.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,28 @@

var commander = require('commander');
var fs = require('fs');
var yaml = require('yaml');
var oas31 = require('openapi3-ts/oas31');
var yaml = require('yaml');

var version = "1.1.3";
var version = "1.2.0";

/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
/* eslint-disable @typescript-eslint/no-explicit-any */
const availableOptimizations = {
"duplicate-read-write-schema": duplicateReadWriteSchemas,
"repoint-schema-refs": repointSchemaReferences,
"remove-duplicate-json-content": removeDuplicateJsonContent,
"filter-content-types": filterContentTypes,
};
function simplifySchemaString(content, optimizations = Object.keys(availableOptimizations)) {
function simplifySchemaString(content, optimizations = Object.keys(availableOptimizations), optimizationArgs) {
const parsedSchema = readSchema(content);
const simplifiedSchema = simplifySchema(parsedSchema, optimizations);
const simplifiedSchema = simplifySchema(parsedSchema, optimizations, optimizationArgs);
return yaml.stringify(simplifiedSchema, { indent: 2, aliasDuplicateObjects: false });
}
function simplifySchema(content, optimizations = Object.keys(availableOptimizations)) {
function simplifySchema(content, optimizations = Object.keys(availableOptimizations), optimizationArgs) {
let schema = content;
for (const optKey of optimizations) {
schema = availableOptimizations[optKey](schema);
schema = availableOptimizations[optKey](schema, optimizationArgs);
}
return schema;
}
Expand Down Expand Up @@ -97,6 +98,58 @@ function removeDuplicateJsonContentInContent(content) {
});
return resContent;
}
function filterContentTypes(schema, args) {
var _a;
if (!schema.paths)
return schema;
const whitelist = (_a = args === null || args === void 0 ? void 0 : args.allowedResponseContentTypes) !== null && _a !== void 0 ? _a : [];
if (whitelist.length === 0) {
return schema;
}
return {
...schema,
paths: Object.fromEntries(Object.entries(schema.paths).map(([path, pathDefinition]) => {
if (!isSpecifiedPath(pathDefinition))
return [path, pathDefinition];
return [
path,
{
...pathDefinition,
get: filterContentTypesInOperation(pathDefinition.get, whitelist),
put: filterContentTypesInOperation(pathDefinition.put, whitelist),
post: filterContentTypesInOperation(pathDefinition.post, whitelist),
delete: filterContentTypesInOperation(pathDefinition.delete, whitelist),
options: filterContentTypesInOperation(pathDefinition.options, whitelist),
head: filterContentTypesInOperation(pathDefinition.head, whitelist),
patch: filterContentTypesInOperation(pathDefinition.patch, whitelist),
trace: filterContentTypesInOperation(pathDefinition.trace, whitelist),
},
];
})),
};
}
function filterContentTypesInOperation(schema, whitelist) {
if (schema === undefined)
return undefined;
return {
...schema,
responses: Object.fromEntries(Object.entries(schema.responses).map(([code, response]) => {
if ("content" in response && response.content) {
return [code, { ...response, content: filterContentTypesInContent(response.content, whitelist) }];
}
return [code, { response }];
})),
};
}
function filterContentTypesInContent(content, whitelist) {
const resContent = {};
Object.entries(content !== null && content !== void 0 ? content : {}).forEach(([newKey, newMediaObj]) => {
if (whitelist.includes(newKey)) {
resContent[newKey] = newMediaObj;
}
});
return resContent;
}
function removeDuplicateJsonContentInOperation(schema) {
if (schema === undefined)
return undefined;
Expand All @@ -120,7 +173,7 @@ function removeDuplicateJsonContentInOperation(schema) {
};
}
function removeDuplicateJsonContent(schema) {
if (schema.paths == null)
if (!schema.paths)
return schema;
return {
...schema,
Expand Down Expand Up @@ -309,17 +362,21 @@ commander.program
.version(version)
.argument("<string>", "input file")
.option("-o, --output <string>", "output file")
.option("-i, --include [string...]", `apply only certain optimizations. available optimizations: ${Object.keys(availableOptimizations).join(", ")}`);
.option("-i, --include [string...]", `apply only certain optimizations. available optimizations: ${Object.keys(availableOptimizations).join(", ")}`)
.option("--allowed-response-content-types [string]", "allowed content types in responses, works only if filter-content-types optimization is enabled");
commander.program.parse();
const [inputFile] = commander.program.args;
const { output, include } = commander.program.opts();
const { output, include, allowedResponseContentTypes, } = commander.program.opts();
const wrongOpt = include === null || include === void 0 ? void 0 : include.find((o) => !(o in availableOptimizations));
if (wrongOpt) {
commander.program.error(`Unexpected optimization: "${wrongOpt}" is not defined.`, { exitCode: 2 });
}
const outputWriter = output
? (content) => fs.writeFileSync(output, content)
: (content) => process.stdout.write(content);
const optimizationArgs = {
allowedResponseContentTypes,
};
const content = fs.readFileSync(inputFile === "-" ? 0 : inputFile).toString();
const simplfiedContent = simplifySchemaString(content, include);
const simplfiedContent = simplifySchemaString(content, include, optimizationArgs);
outputWriter(simplfiedContent);
82 changes: 76 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { parse, stringify } from "yaml";
import {
ContentObject,
isReferenceObject,
Expand All @@ -15,33 +14,41 @@ import {
type ResponseObject,
type SchemaObject,
} from "openapi3-ts/oas31";
import { parse, stringify } from "yaml";

type SchemaEntry = SchemaObject | ReferenceObject;

export const availableOptimizations = {
"duplicate-read-write-schema": duplicateReadWriteSchemas,
"repoint-schema-refs": repointSchemaReferences,
"remove-duplicate-json-content": removeDuplicateJsonContent,
"filter-content-types": filterContentTypes,
};

export type OptimizationKey = keyof typeof availableOptimizations;

export type OptimizationArgs = {
allowedResponseContentTypes: string[];
};

export function simplifySchemaString(
content: string,
optimizations: OptimizationKey[] = Object.keys(availableOptimizations) as OptimizationKey[]
optimizations: OptimizationKey[] = Object.keys(availableOptimizations) as OptimizationKey[],
optimizationArgs?: OptimizationArgs
): string {
const parsedSchema = readSchema(content);
const simplifiedSchema = simplifySchema(parsedSchema, optimizations);
const simplifiedSchema = simplifySchema(parsedSchema, optimizations, optimizationArgs);
return stringify(simplifiedSchema, { indent: 2, aliasDuplicateObjects: false });
}

export function simplifySchema(
content: OpenAPIObject,
optimizations: OptimizationKey[] = Object.keys(availableOptimizations) as OptimizationKey[]
optimizations: OptimizationKey[] = Object.keys(availableOptimizations) as OptimizationKey[],
optimizationArgs?: OptimizationArgs
): OpenAPIObject {
let schema = content;
for (const optKey of optimizations) {
schema = availableOptimizations[optKey](schema);
schema = availableOptimizations[optKey](schema, optimizationArgs);
}
return schema;
}
Expand Down Expand Up @@ -125,6 +132,69 @@ function removeDuplicateJsonContentInContent(content: ContentObject): ContentObj
return resContent;
}

function filterContentTypes(schema: OpenAPIObject, args?: OptimizationArgs): OpenAPIObject {
if (!schema.paths) return schema;

const whitelist = args?.allowedResponseContentTypes ?? [];
if (whitelist.length === 0) {
return schema;
}

return {
...schema,
paths: Object.fromEntries(
Object.entries(schema.paths).map(([path, pathDefinition]) => {
if (!isSpecifiedPath(pathDefinition)) return [path, pathDefinition];

return [
path,
{
...pathDefinition,
get: filterContentTypesInOperation(pathDefinition.get, whitelist),
put: filterContentTypesInOperation(pathDefinition.put, whitelist),
post: filterContentTypesInOperation(pathDefinition.post, whitelist),
delete: filterContentTypesInOperation(pathDefinition.delete, whitelist),
options: filterContentTypesInOperation(pathDefinition.options, whitelist),
head: filterContentTypesInOperation(pathDefinition.head, whitelist),
patch: filterContentTypesInOperation(pathDefinition.patch, whitelist),
trace: filterContentTypesInOperation(pathDefinition.trace, whitelist),
},
];
})
),
};
}

function filterContentTypesInOperation(
schema: OperationObject | undefined,
whitelist: string[]
): OperationObject | undefined {
if (schema === undefined) return undefined;

return {
...schema,
responses: Object.fromEntries(

Check warning on line 176 in src/index.ts

View workflow job for this annotation

GitHub Actions / Test / Lint

Unsafe assignment of an `any` value
Object.entries<ResponseObject | ReferenceObject>(schema.responses).map(([code, response]) => {
if ("content" in response && response.content) {
return [code, { ...response, content: filterContentTypesInContent(response.content, whitelist) }];
}

return [code, { response }];
})
),
};
}

function filterContentTypesInContent(content: ContentObject, whitelist: string[]): ContentObject {
const resContent: ContentObject = {};
Object.entries(content ?? {}).forEach(([newKey, newMediaObj]) => {
if (whitelist.includes(newKey)) {
resContent[newKey] = newMediaObj;
}
});
return resContent;
}

function removeDuplicateJsonContentInOperation(schema: OperationObject | undefined): OperationObject | undefined {
if (schema === undefined) return undefined;

Expand Down Expand Up @@ -155,7 +225,7 @@ function removeDuplicateJsonContentInOperation(schema: OperationObject | undefin
}

function removeDuplicateJsonContent(schema: OpenAPIObject): OpenAPIObject {
if (schema.paths == null) return schema;
if (!schema.paths) return schema;

return {
...schema,
Expand Down
18 changes: 15 additions & 3 deletions src/script.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { program } from "commander";
import fs from "fs";
import { version } from "../package.json" assert { type: "json" };
import { availableOptimizations, simplifySchemaString, type OptimizationKey } from "./index";
import { OptimizationArgs, availableOptimizations, simplifySchemaString, type OptimizationKey } from "./index";

program
.name("openapi-simplifier")
Expand All @@ -12,12 +12,20 @@ program
.option(
"-i, --include [string...]",
`apply only certain optimizations. available optimizations: ${Object.keys(availableOptimizations).join(", ")}`
)
.option(
"--allowed-response-content-types [string]",
"allowed content types in responses, works only if filter-content-types optimization is enabled"
);

program.parse();

const [inputFile] = program.args;
const { output, include }: { output?: string; include?: string[] } = program.opts();
const {
output,
include,
allowedResponseContentTypes,
}: { output?: string; include?: string[]; allowedResponseContentTypes: string[] } = program.opts();

const wrongOpt = include?.find((o) => !(o in availableOptimizations));
if (wrongOpt) {
Expand All @@ -28,7 +36,11 @@ const outputWriter = output
? (content: string) => fs.writeFileSync(output, content)
: (content: string) => process.stdout.write(content);

const optimizationArgs: OptimizationArgs = {
allowedResponseContentTypes,
};

const content = fs.readFileSync(inputFile === "-" ? 0 : inputFile).toString();
const simplfiedContent = simplifySchemaString(content, include as OptimizationKey[] | undefined);
const simplfiedContent = simplifySchemaString(content, include as OptimizationKey[] | undefined, optimizationArgs);

outputWriter(simplfiedContent);

0 comments on commit 4b3e633

Please sign in to comment.