Skip to content

Commit

Permalink
feat: Add before/after hooks and definitions options (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonluca authored Mar 28, 2023
1 parent cc0cf4c commit 4ae492a
Show file tree
Hide file tree
Showing 10 changed files with 1,997 additions and 1,326 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ jobs:
strategy:
matrix:
node-version:
- 14
- 16
- 18
steps:
- uses: actions/checkout@v3
Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,48 @@ If the handler is not provided, the default handler is used. If `additionalPrope

See `test/pattern_properties.test.js` for examples how this works.

#### `definitionKeywords` (array)

By default, definitions are not converted. If your documents follow the convention of having a definitions object at the root of a (sub)schema, you can set definitionKeywords to `['definitions']`.

```js
var schema = {
definitions: {
sharedDefinition: {
type: "object",
properties: {
foo: {
type: "string",
nullable: true,
},
},
},
},
};
var convertedSchema = toJsonSchema(schema, {
definitionKeywords: ["definitions"],
});
console.log(convertedSchema);
```

prints

```js
{
$schema: 'http://json-schema.org/draft-04/schema#',
definitions: {
sharedDefinition: {
type: 'object',
properties: {
foo: {
type: ['string', 'null']
}
}
}
}
}
```

## Converting OpenAPI parameters

OpenAPI parameters can be converted:
Expand Down
23 changes: 11 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,19 @@
},
"devDependencies": {
"@types/json-schema": "^7.0.11",
"@types/lodash-es": "^4.17.6",
"@typescript-eslint/eslint-plugin": "^5.32.0",
"@typescript-eslint/parser": "^5.32.0",
"c8": "^7.12.0",
"eslint": "^8.21.0",
"eslint-config-prettier": "^8.5.0",
"@types/lodash-es": "^4.17.7",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"c8": "^7.13.0",
"eslint": "^8.36.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-unused-imports": "^2.0.0",
"nock": "^13.2.9",
"openapi-typescript": "^5.4.1",
"prettier": "^2.7.1",
"semantic-release": "^19.0.3",
"typescript": "^4.7.4",
"vitest": "^0.20.3"
"openapi-typescript": "^5.4.1y",
"prettier": "^2.8.7",
"semantic-release": "^21.0.0",
"typescript": "^5.0.2",
"vitest": "^0.29.8"
},
"prettier": {
"printWidth": 120,
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import deepEqual from "fast-deep-equal";
import { fromSchema, fromParameter } from "./lib/convert";
import type { Options, OptionsInternal, OpenAPI3 } from "./types";
import type { Options, OptionsInternal, OpenAPI3 } from "./openapi-schema-types";
import { NOT_SUPPORTED, STRUCTS } from "./consts";
import { cloneDeep } from "lodash-es";
import type { JSONSchema4 } from "json-schema";
Expand Down Expand Up @@ -31,6 +31,7 @@ const resolveOptions = (_options?: Options): OptionsInternal => {
options.cloneSchema ??= true;
options.supportPatternProperties = Boolean(options.supportPatternProperties);
options.keepNotSupported ??= [];
options.definitionKeywords ??= [];
options.strictMode ??= true;

if (typeof options.patternPropertiesHandler !== "function") {
Expand Down
8 changes: 6 additions & 2 deletions src/lib/converters/parameter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import convertFromSchema from "./schema";
import InvalidInputError from "../errors/invalid-input-error";
import type { OptionsInternal } from "../../types";
import type { OptionsInternal } from "../../openapi-schema-types";
import type { ParameterObject } from "openapi-typescript/src/types";
import type { ResponseObject } from "openapi-typescript/src/types";
import type { JSONSchema4 } from "json-schema";
Expand All @@ -25,12 +25,16 @@ const convertFromContents = (parameter: ResponseObject, options: OptionsInternal
return schemas;
};

const isResponseObject = (parameter: ParameterObject | ResponseObject): parameter is ResponseObject => {
return Boolean(parameter) && "content" in parameter && Boolean(parameter.content);
};

// Convert from OpenAPI 3.0 `ParameterObject` to JSON schema v4
const convertFromParameter = (parameter: ParameterObject | ResponseObject, options: OptionsInternal): JSONSchema4 => {
if ("schema" in parameter && parameter.schema) {
return convertParameterSchema(parameter, parameter.schema, options);
}
if ("content" in parameter && parameter.content) {
if (isResponseObject(parameter)) {
return convertFromContents(parameter, options);
}
if (options.strictMode) {
Expand Down
53 changes: 33 additions & 20 deletions src/lib/converters/schema.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { isObject } from "../utils/isObject";
import InvalidTypeError from "../errors/invalid-type-error";
import type { OptionsInternal } from "../../types";
import type { OptionsInternal } from "../../openapi-schema-types";
import { cloneDeep } from "lodash-es";
import type { STRUCTS } from "../../consts";
import type { JSONSchema4, JSONSchema4TypeName } from "json-schema";
import { VALID_OPENAPI_FORMATS } from "../../consts";
import type { SchemaObject } from "openapi-typescript/src/types";
import type { PatternPropertiesHandler } from "../../types";
import type { PatternPropertiesHandler } from "../../openapi-schema-types";
import type { OpenAPI3 } from "openapi-typescript";
import type { ReferenceObject } from "openapi-typescript/src/types";

Expand All @@ -25,17 +24,19 @@ function convertSchema(schema: OpenAPI3 | SchemaObject | ReferenceObject, option
const structs = options._structs;
const notSupported = options._notSupported;
const strictMode = options.strictMode;
let i = 0;
let j = 0;
let struct: typeof STRUCTS[number] | null = null;
const definitionKeywords = options.definitionKeywords || [];
const beforeTransform = options.beforeTransform;
const afterTransform = options.afterTransform;

for (i; i < structs.length; i++) {
struct = structs[i];
if (beforeTransform) {
schema = beforeTransform(schema, options);
}

for (const struct of structs) {
if (Array.isArray(schema[struct])) {
let cloned = false;

for (j; j < schema[struct].length; j++) {
for (let j = 0; j < schema[struct].length; j++) {
if (!isObject(schema[struct][j])) {
if (options.cloneSchema && !cloned) {
cloned = true;
Expand All @@ -57,12 +58,18 @@ function convertSchema(schema: OpenAPI3 | SchemaObject | ReferenceObject, option
}
let convertedSchema = schema as SchemaObject;

for (const def of definitionKeywords) {
if (typeof schema[def] === "object") {
schema[def] = convertProperties(schema[def], options);
}
}

if ("properties" in convertedSchema) {
convertedSchema.properties = convertProperties(convertedSchema.properties, options);

if (Array.isArray(convertedSchema.required)) {
convertedSchema.required = convertedSchema.required.filter(
(key) => convertedSchema.properties?.[key] !== undefined,
(key) => "properties" in convertedSchema && convertedSchema.properties?.[key] !== undefined,
);
if (convertedSchema.required.length === 0) {
delete convertedSchema.required;
Expand All @@ -73,7 +80,7 @@ function convertSchema(schema: OpenAPI3 | SchemaObject | ReferenceObject, option
}
}

if (strictMode) {
if (strictMode && "type" in convertedSchema) {
validateType(convertedSchema.type);
}

Expand All @@ -84,8 +91,12 @@ function convertSchema(schema: OpenAPI3 | SchemaObject | ReferenceObject, option
convertedSchema = convertPatternProperties(convertedSchema, options.patternPropertiesHandler);
}

for (i = 0; i < notSupported.length; i++) {
delete convertedSchema[notSupported[i]];
for (const item of notSupported) {
delete convertedSchema[item];
}

if (afterTransform) {
return afterTransform(convertedSchema, options);
}

return convertedSchema as JSONSchema4;
Expand Down Expand Up @@ -130,13 +141,15 @@ function convertProperties(
}

function convertTypes(schema: SchemaObject) {
const type = schema.type as JSONSchema4TypeName;
const schemaEnum = schema.enum as unknown[];
if (type !== undefined && schema.nullable === true) {
(<JSONSchema4>schema).type = [type, "null"];

if (Array.isArray(schemaEnum) && !schemaEnum.includes(null)) {
schema.enum = schemaEnum.concat([null]);
if ("type" in schema) {
const type = schema.type as JSONSchema4TypeName;
const schemaEnum = schema.enum as (string | null)[];
if (type !== undefined && schema.nullable === true) {
(<JSONSchema4>schema).type = [type, "null"];
if (Array.isArray(schemaEnum) && !schemaEnum.includes(null)) {
// @ts-ignore
schema.enum = schemaEnum.concat([null]);
}
}
}

Expand Down
9 changes: 7 additions & 2 deletions src/types.ts → src/openapi-schema-types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { NOT_SUPPORTED, STRUCTS } from "./consts";
import type { OpenAPI3 } from "openapi-typescript";
import type { SchemaObject } from "openapi-typescript/src/types";
import type { ReferenceObject } from "openapi-typescript/src/types";
import type { JSONSchema4 } from "json-schema";
export type { OpenAPI3 };
// We don't know what the shape of the object looks like when it's passed in, but we know its some mix of these two
export type PatternPropertiesHandler = (schema: SchemaObject) => SchemaObject;
Expand All @@ -9,16 +11,19 @@ export interface Options {
dateToDateTime?: boolean;
cloneSchema?: boolean;
supportPatternProperties?: boolean;
keepNotSupported?: typeof NOT_SUPPORTED[number][];
keepNotSupported?: (typeof NOT_SUPPORTED)[number][];
strictMode?: boolean;
removeReadOnly?: boolean;
removeWriteOnly?: boolean;
patternPropertiesHandler?: PatternPropertiesHandler;
definitionKeywords?: string[];
beforeTransform?: (schema: SchemaObject | ReferenceObject | OpenAPI3, options: Options) => SchemaObject;
afterTransform?: (schema: SchemaObject | ReferenceObject | OpenAPI3, options: Options) => JSONSchema4;
}

export interface OptionsInternal extends Options {
_removeProps: string[];
_structs: typeof STRUCTS;
_notSupported: typeof NOT_SUPPORTED[number][];
_notSupported: (typeof NOT_SUPPORTED)[number][];
patternPropertiesHandler: PatternPropertiesHandler;
}
37 changes: 37 additions & 0 deletions test/definition_keyworks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import convert from "../src";

it("handles conversion in keywords specified in additionalKeywords", function ({ expect }) {
const schema = {
definitions: {
sharedDefinition: {
type: "object",
properties: {
foo: {
type: "string",
nullable: true,
},
},
},
},
};

const result = convert(schema, {
definitionKeywords: ["definitions"],
});

const expected = {
$schema: "http://json-schema.org/draft-04/schema#",
definitions: {
sharedDefinition: {
type: "object",
properties: {
foo: {
type: ["string", "null"],
},
},
},
},
};

expect(result).toEqual(expected);
});
26 changes: 26 additions & 0 deletions test/transform.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import convert from "../src";

it("handles conversion in keywords specified in additionalKeywords", function ({ expect }) {
const schema = {
type: "boolean",
};

const result = convert(schema, {
beforeTransform: function (schema) {
schema.type = "string";
return schema;
},
afterTransform: function (schema) {
schema.examples = ["foo", "bar"];
return schema;
},
});

const expected = {
$schema: "http://json-schema.org/draft-04/schema#",
type: "string",
examples: ["foo", "bar"],
};

expect(result).toEqual(expected);
});
Loading

0 comments on commit 4ae492a

Please sign in to comment.