Skip to content

Commit 4ae492a

Browse files
authored
feat: Add before/after hooks and definitions options (#52)
1 parent cc0cf4c commit 4ae492a

File tree

10 files changed

+1997
-1326
lines changed

10 files changed

+1997
-1326
lines changed

.github/workflows/test.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ jobs:
88
strategy:
99
matrix:
1010
node-version:
11-
- 14
12-
- 16
1311
- 18
1412
steps:
1513
- uses: actions/checkout@v3

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,48 @@ If the handler is not provided, the default handler is used. If `additionalPrope
122122

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

125+
#### `definitionKeywords` (array)
126+
127+
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']`.
128+
129+
```js
130+
var schema = {
131+
definitions: {
132+
sharedDefinition: {
133+
type: "object",
134+
properties: {
135+
foo: {
136+
type: "string",
137+
nullable: true,
138+
},
139+
},
140+
},
141+
},
142+
};
143+
var convertedSchema = toJsonSchema(schema, {
144+
definitionKeywords: ["definitions"],
145+
});
146+
console.log(convertedSchema);
147+
```
148+
149+
prints
150+
151+
```js
152+
{
153+
$schema: 'http://json-schema.org/draft-04/schema#',
154+
definitions: {
155+
sharedDefinition: {
156+
type: 'object',
157+
properties: {
158+
foo: {
159+
type: ['string', 'null']
160+
}
161+
}
162+
}
163+
}
164+
}
165+
```
166+
125167
## Converting OpenAPI parameters
126168

127169
OpenAPI parameters can be converted:

package.json

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,19 @@
3131
},
3232
"devDependencies": {
3333
"@types/json-schema": "^7.0.11",
34-
"@types/lodash-es": "^4.17.6",
35-
"@typescript-eslint/eslint-plugin": "^5.32.0",
36-
"@typescript-eslint/parser": "^5.32.0",
37-
"c8": "^7.12.0",
38-
"eslint": "^8.21.0",
39-
"eslint-config-prettier": "^8.5.0",
34+
"@types/lodash-es": "^4.17.7",
35+
"@typescript-eslint/eslint-plugin": "^5.57.0",
36+
"@typescript-eslint/parser": "^5.57.0",
37+
"c8": "^7.13.0",
38+
"eslint": "^8.36.0",
39+
"eslint-config-prettier": "^8.8.0",
4040
"eslint-plugin-prettier": "^4.2.1",
4141
"eslint-plugin-unused-imports": "^2.0.0",
42-
"nock": "^13.2.9",
43-
"openapi-typescript": "^5.4.1",
44-
"prettier": "^2.7.1",
45-
"semantic-release": "^19.0.3",
46-
"typescript": "^4.7.4",
47-
"vitest": "^0.20.3"
42+
"openapi-typescript": "^5.4.1y",
43+
"prettier": "^2.8.7",
44+
"semantic-release": "^21.0.0",
45+
"typescript": "^5.0.2",
46+
"vitest": "^0.29.8"
4847
},
4948
"prettier": {
5049
"printWidth": 120,

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import deepEqual from "fast-deep-equal";
22
import { fromSchema, fromParameter } from "./lib/convert";
3-
import type { Options, OptionsInternal, OpenAPI3 } from "./types";
3+
import type { Options, OptionsInternal, OpenAPI3 } from "./openapi-schema-types";
44
import { NOT_SUPPORTED, STRUCTS } from "./consts";
55
import { cloneDeep } from "lodash-es";
66
import type { JSONSchema4 } from "json-schema";
@@ -31,6 +31,7 @@ const resolveOptions = (_options?: Options): OptionsInternal => {
3131
options.cloneSchema ??= true;
3232
options.supportPatternProperties = Boolean(options.supportPatternProperties);
3333
options.keepNotSupported ??= [];
34+
options.definitionKeywords ??= [];
3435
options.strictMode ??= true;
3536

3637
if (typeof options.patternPropertiesHandler !== "function") {

src/lib/converters/parameter.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import convertFromSchema from "./schema";
22
import InvalidInputError from "../errors/invalid-input-error";
3-
import type { OptionsInternal } from "../../types";
3+
import type { OptionsInternal } from "../../openapi-schema-types";
44
import type { ParameterObject } from "openapi-typescript/src/types";
55
import type { ResponseObject } from "openapi-typescript/src/types";
66
import type { JSONSchema4 } from "json-schema";
@@ -25,12 +25,16 @@ const convertFromContents = (parameter: ResponseObject, options: OptionsInternal
2525
return schemas;
2626
};
2727

28+
const isResponseObject = (parameter: ParameterObject | ResponseObject): parameter is ResponseObject => {
29+
return Boolean(parameter) && "content" in parameter && Boolean(parameter.content);
30+
};
31+
2832
// Convert from OpenAPI 3.0 `ParameterObject` to JSON schema v4
2933
const convertFromParameter = (parameter: ParameterObject | ResponseObject, options: OptionsInternal): JSONSchema4 => {
3034
if ("schema" in parameter && parameter.schema) {
3135
return convertParameterSchema(parameter, parameter.schema, options);
3236
}
33-
if ("content" in parameter && parameter.content) {
37+
if (isResponseObject(parameter)) {
3438
return convertFromContents(parameter, options);
3539
}
3640
if (options.strictMode) {

src/lib/converters/schema.ts

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { isObject } from "../utils/isObject";
22
import InvalidTypeError from "../errors/invalid-type-error";
3-
import type { OptionsInternal } from "../../types";
3+
import type { OptionsInternal } from "../../openapi-schema-types";
44
import { cloneDeep } from "lodash-es";
5-
import type { STRUCTS } from "../../consts";
65
import type { JSONSchema4, JSONSchema4TypeName } from "json-schema";
76
import { VALID_OPENAPI_FORMATS } from "../../consts";
87
import type { SchemaObject } from "openapi-typescript/src/types";
9-
import type { PatternPropertiesHandler } from "../../types";
8+
import type { PatternPropertiesHandler } from "../../openapi-schema-types";
109
import type { OpenAPI3 } from "openapi-typescript";
1110
import type { ReferenceObject } from "openapi-typescript/src/types";
1211

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

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

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

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

61+
for (const def of definitionKeywords) {
62+
if (typeof schema[def] === "object") {
63+
schema[def] = convertProperties(schema[def], options);
64+
}
65+
}
66+
6067
if ("properties" in convertedSchema) {
6168
convertedSchema.properties = convertProperties(convertedSchema.properties, options);
6269

6370
if (Array.isArray(convertedSchema.required)) {
6471
convertedSchema.required = convertedSchema.required.filter(
65-
(key) => convertedSchema.properties?.[key] !== undefined,
72+
(key) => "properties" in convertedSchema && convertedSchema.properties?.[key] !== undefined,
6673
);
6774
if (convertedSchema.required.length === 0) {
6875
delete convertedSchema.required;
@@ -73,7 +80,7 @@ function convertSchema(schema: OpenAPI3 | SchemaObject | ReferenceObject, option
7380
}
7481
}
7582

76-
if (strictMode) {
83+
if (strictMode && "type" in convertedSchema) {
7784
validateType(convertedSchema.type);
7885
}
7986

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

87-
for (i = 0; i < notSupported.length; i++) {
88-
delete convertedSchema[notSupported[i]];
94+
for (const item of notSupported) {
95+
delete convertedSchema[item];
96+
}
97+
98+
if (afterTransform) {
99+
return afterTransform(convertedSchema, options);
89100
}
90101

91102
return convertedSchema as JSONSchema4;
@@ -130,13 +141,15 @@ function convertProperties(
130141
}
131142

132143
function convertTypes(schema: SchemaObject) {
133-
const type = schema.type as JSONSchema4TypeName;
134-
const schemaEnum = schema.enum as unknown[];
135-
if (type !== undefined && schema.nullable === true) {
136-
(<JSONSchema4>schema).type = [type, "null"];
137-
138-
if (Array.isArray(schemaEnum) && !schemaEnum.includes(null)) {
139-
schema.enum = schemaEnum.concat([null]);
144+
if ("type" in schema) {
145+
const type = schema.type as JSONSchema4TypeName;
146+
const schemaEnum = schema.enum as (string | null)[];
147+
if (type !== undefined && schema.nullable === true) {
148+
(<JSONSchema4>schema).type = [type, "null"];
149+
if (Array.isArray(schemaEnum) && !schemaEnum.includes(null)) {
150+
// @ts-ignore
151+
schema.enum = schemaEnum.concat([null]);
152+
}
140153
}
141154
}
142155

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { NOT_SUPPORTED, STRUCTS } from "./consts";
22
import type { OpenAPI3 } from "openapi-typescript";
33
import type { SchemaObject } from "openapi-typescript/src/types";
4+
import type { ReferenceObject } from "openapi-typescript/src/types";
5+
import type { JSONSchema4 } from "json-schema";
46
export type { OpenAPI3 };
57
// 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
68
export type PatternPropertiesHandler = (schema: SchemaObject) => SchemaObject;
@@ -9,16 +11,19 @@ export interface Options {
911
dateToDateTime?: boolean;
1012
cloneSchema?: boolean;
1113
supportPatternProperties?: boolean;
12-
keepNotSupported?: typeof NOT_SUPPORTED[number][];
14+
keepNotSupported?: (typeof NOT_SUPPORTED)[number][];
1315
strictMode?: boolean;
1416
removeReadOnly?: boolean;
1517
removeWriteOnly?: boolean;
1618
patternPropertiesHandler?: PatternPropertiesHandler;
19+
definitionKeywords?: string[];
20+
beforeTransform?: (schema: SchemaObject | ReferenceObject | OpenAPI3, options: Options) => SchemaObject;
21+
afterTransform?: (schema: SchemaObject | ReferenceObject | OpenAPI3, options: Options) => JSONSchema4;
1722
}
1823

1924
export interface OptionsInternal extends Options {
2025
_removeProps: string[];
2126
_structs: typeof STRUCTS;
22-
_notSupported: typeof NOT_SUPPORTED[number][];
27+
_notSupported: (typeof NOT_SUPPORTED)[number][];
2328
patternPropertiesHandler: PatternPropertiesHandler;
2429
}

test/definition_keyworks.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import convert from "../src";
2+
3+
it("handles conversion in keywords specified in additionalKeywords", function ({ expect }) {
4+
const schema = {
5+
definitions: {
6+
sharedDefinition: {
7+
type: "object",
8+
properties: {
9+
foo: {
10+
type: "string",
11+
nullable: true,
12+
},
13+
},
14+
},
15+
},
16+
};
17+
18+
const result = convert(schema, {
19+
definitionKeywords: ["definitions"],
20+
});
21+
22+
const expected = {
23+
$schema: "http://json-schema.org/draft-04/schema#",
24+
definitions: {
25+
sharedDefinition: {
26+
type: "object",
27+
properties: {
28+
foo: {
29+
type: ["string", "null"],
30+
},
31+
},
32+
},
33+
},
34+
};
35+
36+
expect(result).toEqual(expected);
37+
});

test/transform.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import convert from "../src";
2+
3+
it("handles conversion in keywords specified in additionalKeywords", function ({ expect }) {
4+
const schema = {
5+
type: "boolean",
6+
};
7+
8+
const result = convert(schema, {
9+
beforeTransform: function (schema) {
10+
schema.type = "string";
11+
return schema;
12+
},
13+
afterTransform: function (schema) {
14+
schema.examples = ["foo", "bar"];
15+
return schema;
16+
},
17+
});
18+
19+
const expected = {
20+
$schema: "http://json-schema.org/draft-04/schema#",
21+
type: "string",
22+
examples: ["foo", "bar"],
23+
};
24+
25+
expect(result).toEqual(expected);
26+
});

0 commit comments

Comments
 (0)