typed-openapi 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin.js +3 -0
- package/dist/cli.cjs +857 -0
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +833 -0
- package/package.json +52 -0
- package/src/asserts.ts +7 -0
- package/src/box-factory.ts +52 -0
- package/src/box.ts +84 -0
- package/src/cli.ts +47 -0
- package/src/format.ts +12 -0
- package/src/generator.ts +305 -0
- package/src/is-reference-object.ts +16 -0
- package/src/map-openapi-endpoints.ts +171 -0
- package/src/openapi-schema-to-ts.ts +157 -0
- package/src/ref-resolver.ts +206 -0
- package/src/string-utils.ts +42 -0
- package/src/topological-sort.ts +35 -0
- package/src/ts-factory.ts +24 -0
- package/src/types.ts +133 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { OpenAPIObject, ResponseObject } from "openapi3-ts/oas31";
|
|
2
|
+
import { OperationObject, ParameterObject } from "openapi3-ts/oas31";
|
|
3
|
+
import { capitalize, pick } from "pastable/server";
|
|
4
|
+
import { Box } from "./box";
|
|
5
|
+
import { createBoxFactory } from "./box-factory";
|
|
6
|
+
import { openApiSchemaToTs } from "./openapi-schema-to-ts";
|
|
7
|
+
import { createRefResolver } from "./ref-resolver";
|
|
8
|
+
import { tsFactory } from "./ts-factory";
|
|
9
|
+
import { AnyBox, BoxRef, OpenapiSchemaConvertContext } from "./types";
|
|
10
|
+
import { pathToVariableName } from "./string-utils";
|
|
11
|
+
|
|
12
|
+
const factory = tsFactory;
|
|
13
|
+
|
|
14
|
+
export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
|
|
15
|
+
const refs = createRefResolver(doc, factory);
|
|
16
|
+
const ctx: OpenapiSchemaConvertContext = { refs, factory };
|
|
17
|
+
const endpointList = [] as Array<Endpoint>;
|
|
18
|
+
|
|
19
|
+
Object.entries(doc.paths ?? {}).forEach(([path, pathItemObj]) => {
|
|
20
|
+
const pathItem = pick(pathItemObj, ["get", "put", "post", "delete", "options", "head", "patch", "trace"]);
|
|
21
|
+
Object.entries(pathItem).forEach(([method, operation]) => {
|
|
22
|
+
if (operation.deprecated) return;
|
|
23
|
+
|
|
24
|
+
const endpoint = {
|
|
25
|
+
operation,
|
|
26
|
+
method: method as Method,
|
|
27
|
+
path,
|
|
28
|
+
response: openApiSchemaToTs({ schema: {}, ctx }),
|
|
29
|
+
meta: {
|
|
30
|
+
alias: getAlias({ path, method, operation } as Endpoint),
|
|
31
|
+
areParametersRequired: false,
|
|
32
|
+
hasParameters: false,
|
|
33
|
+
},
|
|
34
|
+
} as Endpoint;
|
|
35
|
+
|
|
36
|
+
// Build a list of parameters by type + fill an object with all of them
|
|
37
|
+
const lists = { query: [] as ParameterObject[], path: [] as ParameterObject[], header: [] as ParameterObject[] };
|
|
38
|
+
const paramObjects = (operation.parameters ?? []).reduce(
|
|
39
|
+
(acc, paramOrRef) => {
|
|
40
|
+
const param = refs.unwrap(paramOrRef);
|
|
41
|
+
const schema = openApiSchemaToTs({ schema: refs.unwrap(param.schema ?? {}), ctx });
|
|
42
|
+
lists.query.push(param);
|
|
43
|
+
|
|
44
|
+
if (param.required) endpoint.meta.areParametersRequired = true;
|
|
45
|
+
endpoint.meta.hasParameters = true;
|
|
46
|
+
|
|
47
|
+
if (param.in === "query") acc.query[param.name] = schema;
|
|
48
|
+
if (param.in === "path") acc.path[param.name] = schema;
|
|
49
|
+
if (param.in === "header") acc.header[param.name] = schema;
|
|
50
|
+
|
|
51
|
+
return acc;
|
|
52
|
+
},
|
|
53
|
+
{ query: {} as Record<string, Box>, path: {} as Record<string, Box>, header: {} as Record<string, Box> },
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Filter out empty objects
|
|
57
|
+
const params = Object.entries(paramObjects).reduce((acc, [key, value]) => {
|
|
58
|
+
if (Object.keys(value).length) {
|
|
59
|
+
// @ts-expect-error
|
|
60
|
+
acc[key] = value;
|
|
61
|
+
}
|
|
62
|
+
return acc;
|
|
63
|
+
}, {} as { query?: Record<string, Box>; path?: Record<string, Box>; header?: Record<string, Box>; body?: Box });
|
|
64
|
+
|
|
65
|
+
// Body
|
|
66
|
+
if (operation.requestBody) {
|
|
67
|
+
const requestBody = refs.unwrap(operation.requestBody ?? {});
|
|
68
|
+
const content = requestBody.content;
|
|
69
|
+
const matchingMediaType = Object.keys(content).find(isAllowedParamMediaTypes);
|
|
70
|
+
|
|
71
|
+
if (matchingMediaType && content[matchingMediaType]) {
|
|
72
|
+
params.body = openApiSchemaToTs({
|
|
73
|
+
schema: content[matchingMediaType]?.schema ?? {} ?? {},
|
|
74
|
+
ctx,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Make parameters optional if none of them are required
|
|
80
|
+
if (params) {
|
|
81
|
+
const t = createBoxFactory({}, ctx);
|
|
82
|
+
if (params.query && lists.query.length && lists.query.every((param) => !param.required)) {
|
|
83
|
+
if (!params.query) params.query = {};
|
|
84
|
+
params.query = t.reference("Partial", [t.object(params.query)]) as any;
|
|
85
|
+
}
|
|
86
|
+
if (params.path && lists.path.length && lists.path.every((param) => !param.required)) {
|
|
87
|
+
params.path = t.reference("Partial", [t.object(params.path)]) as any;
|
|
88
|
+
}
|
|
89
|
+
if (params.header && lists.header.length && lists.header.every((param) => !param.required)) {
|
|
90
|
+
params.header = t.reference("Partial", [t.object(params.header)]) as any;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// No need to pass empty objects, it's confusing
|
|
94
|
+
endpoint.parameters = Object.keys(params).length ? (params as any as EndpointParameters) : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let responseObject;
|
|
98
|
+
// Match the default response or first 2xx-3xx response found
|
|
99
|
+
if (operation.responses.default) {
|
|
100
|
+
responseObject = refs.unwrap(operation.responses.default);
|
|
101
|
+
} else {
|
|
102
|
+
Object.entries(operation.responses).map(([status, responseOrRef]) => {
|
|
103
|
+
const statusCode = Number(status);
|
|
104
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
105
|
+
responseObject = refs.unwrap<ResponseObject>(responseOrRef);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const content = responseObject?.content;
|
|
111
|
+
if (content) {
|
|
112
|
+
const matchingMediaType = Object.keys(content).find(isResponseMediaType);
|
|
113
|
+
if (matchingMediaType && content[matchingMediaType]) {
|
|
114
|
+
endpoint.response = openApiSchemaToTs({
|
|
115
|
+
schema: content[matchingMediaType]?.schema ?? {} ?? {},
|
|
116
|
+
ctx,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
endpointList.push(endpoint);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return { doc, refs, endpointList, factory };
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const allowedParamMediaTypes = [
|
|
129
|
+
"application/octet-stream",
|
|
130
|
+
"multipart/form-data",
|
|
131
|
+
"application/x-www-form-urlencoded",
|
|
132
|
+
"*/*",
|
|
133
|
+
] as const;
|
|
134
|
+
const isAllowedParamMediaTypes = (
|
|
135
|
+
mediaType: string,
|
|
136
|
+
): mediaType is (typeof allowedParamMediaTypes)[number] | `application/${string}json${string}` | `text/${string}` =>
|
|
137
|
+
(mediaType.includes("application/") && mediaType.includes("json")) ||
|
|
138
|
+
allowedParamMediaTypes.includes(mediaType as any) ||
|
|
139
|
+
mediaType.includes("text/");
|
|
140
|
+
|
|
141
|
+
const isResponseMediaType = (mediaType: string) => mediaType === "application/json";
|
|
142
|
+
const getAlias = ({ path, method, operation }: Endpoint) =>
|
|
143
|
+
method + "_" + capitalize(operation.operationId ?? pathToVariableName(path));
|
|
144
|
+
|
|
145
|
+
type MutationMethod = "post" | "put" | "patch" | "delete";
|
|
146
|
+
type Method = "get" | "head" | "options" | MutationMethod;
|
|
147
|
+
|
|
148
|
+
export type EndpointParameters = {
|
|
149
|
+
body?: Box;
|
|
150
|
+
query?: Box<BoxRef> | Record<string, AnyBox>;
|
|
151
|
+
header?: Box<BoxRef> | Record<string, AnyBox>;
|
|
152
|
+
path?: Box<BoxRef> | Record<string, AnyBox>;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
type DefaultEndpoint = {
|
|
156
|
+
parameters?: EndpointParameters | undefined;
|
|
157
|
+
response: AnyBox;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
|
|
161
|
+
operation: OperationObject;
|
|
162
|
+
method: Method;
|
|
163
|
+
path: string;
|
|
164
|
+
parameters?: TConfig["parameters"];
|
|
165
|
+
meta: {
|
|
166
|
+
alias: string;
|
|
167
|
+
hasParameters: boolean;
|
|
168
|
+
areParametersRequired: boolean;
|
|
169
|
+
};
|
|
170
|
+
response: TConfig["response"];
|
|
171
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { isPrimitiveType } from "./asserts";
|
|
2
|
+
import { Box } from "./box";
|
|
3
|
+
import { createBoxFactory } from "./box-factory";
|
|
4
|
+
import { isReferenceObject } from "./is-reference-object";
|
|
5
|
+
import { AnyBoxDef, OpenapiSchemaConvertArgs } from "./types";
|
|
6
|
+
import { wrapWithQuotesIfNeeded } from "./string-utils";
|
|
7
|
+
|
|
8
|
+
export const openApiSchemaToTs = ({ schema, meta: _inheritedMeta, ctx }: OpenapiSchemaConvertArgs): Box<AnyBoxDef> => {
|
|
9
|
+
const meta = {} as OpenapiSchemaConvertArgs["meta"];
|
|
10
|
+
|
|
11
|
+
if (!schema) {
|
|
12
|
+
throw new Error("Schema is required");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const t = createBoxFactory(schema, ctx);
|
|
16
|
+
const getTs = () => {
|
|
17
|
+
if (isReferenceObject(schema)) {
|
|
18
|
+
const refInfo = ctx.refs.getInfosByRef(schema.$ref);
|
|
19
|
+
|
|
20
|
+
return t.reference(refInfo.normalized);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (Array.isArray(schema.type)) {
|
|
24
|
+
if (schema.type.length === 1) {
|
|
25
|
+
return openApiSchemaToTs({ schema: { ...schema, type: schema.type[0]! }, ctx, meta });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return t.union(schema.type.map((prop) => openApiSchemaToTs({ schema: { ...schema, type: prop }, ctx, meta })));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (schema.type === "null") {
|
|
32
|
+
return t.reference("null");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (schema.oneOf) {
|
|
36
|
+
if (schema.oneOf.length === 1) {
|
|
37
|
+
return openApiSchemaToTs({ schema: schema.oneOf[0]!, ctx, meta });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return t.union(schema.oneOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// anyOf = oneOf but with 1 or more = `T extends oneOf ? T | T[] : never`
|
|
44
|
+
if (schema.anyOf) {
|
|
45
|
+
if (schema.anyOf.length === 1) {
|
|
46
|
+
return openApiSchemaToTs({ schema: schema.anyOf[0]!, ctx, meta });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const oneOf = t.union(schema.anyOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta })));
|
|
50
|
+
return t.union([oneOf, t.array(oneOf)]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (schema.allOf) {
|
|
54
|
+
if (schema.allOf.length === 1) {
|
|
55
|
+
return openApiSchemaToTs({ schema: schema.allOf[0]!, ctx, meta });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const types = schema.allOf.map((prop) => openApiSchemaToTs({ schema: prop, ctx, meta }));
|
|
59
|
+
return t.intersection(types);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const schemaType = schema.type ? (schema.type.toLowerCase() as NonNullable<typeof schema.type>) : undefined;
|
|
63
|
+
if (schemaType && isPrimitiveType(schemaType)) {
|
|
64
|
+
if (schema.enum) {
|
|
65
|
+
if (schema.enum.length === 1) {
|
|
66
|
+
const value = schema.enum[0];
|
|
67
|
+
return t.literal(value === null ? "null" : `"${value}"`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (schemaType === "string") {
|
|
71
|
+
return t.union(schema.enum.map((value) => t.literal(`"${value}"`)));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (schema.enum.some((e) => typeof e === "string")) {
|
|
75
|
+
return t.never();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return t.union(schema.enum.map((value) => t.literal(value === null ? "null" : value)));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (schemaType === "string") return t.string();
|
|
82
|
+
if (schemaType === "boolean") return t.boolean();
|
|
83
|
+
if (schemaType === "number" || schemaType === "integer") return t.number();
|
|
84
|
+
if (schemaType === "null") return t.reference("null");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (schemaType === "array") {
|
|
88
|
+
if (schema.items) {
|
|
89
|
+
let arrayOfType = openApiSchemaToTs({ schema: schema.items, ctx, meta });
|
|
90
|
+
if (typeof arrayOfType === "string") {
|
|
91
|
+
arrayOfType = t.reference(arrayOfType);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return t.array(arrayOfType);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return t.array(t.any());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (schemaType === "object" || schema.properties || schema.additionalProperties) {
|
|
101
|
+
if (!schema.properties) {
|
|
102
|
+
return t.unknown();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let additionalProperties;
|
|
106
|
+
if (schema.additionalProperties) {
|
|
107
|
+
let additionalPropertiesType;
|
|
108
|
+
if (
|
|
109
|
+
(typeof schema.additionalProperties === "boolean" && schema.additionalProperties) ||
|
|
110
|
+
(typeof schema.additionalProperties === "object" && Object.keys(schema.additionalProperties).length === 0)
|
|
111
|
+
) {
|
|
112
|
+
additionalPropertiesType = t.any();
|
|
113
|
+
} else if (typeof schema.additionalProperties === "object") {
|
|
114
|
+
additionalPropertiesType = openApiSchemaToTs({
|
|
115
|
+
schema: schema.additionalProperties,
|
|
116
|
+
ctx,
|
|
117
|
+
meta,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
additionalProperties = t.object({ [t.string().value]: additionalPropertiesType! });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const hasRequiredArray = schema.required && schema.required.length > 0;
|
|
125
|
+
const isPartial = !schema.required?.length;
|
|
126
|
+
|
|
127
|
+
const props = Object.fromEntries(
|
|
128
|
+
Object.entries(schema.properties).map(([prop, propSchema]) => {
|
|
129
|
+
let propType = openApiSchemaToTs({ schema: propSchema, ctx, meta });
|
|
130
|
+
if (typeof propType === "string") {
|
|
131
|
+
// TODO Partial ?
|
|
132
|
+
propType = t.reference(propType);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const isRequired = Boolean(isPartial ? true : hasRequiredArray ? schema.required?.includes(prop) : false);
|
|
136
|
+
const isOptional = !isPartial && !isRequired;
|
|
137
|
+
return [
|
|
138
|
+
`${wrapWithQuotesIfNeeded(prop)}${isOptional ? "?" : ""}`,
|
|
139
|
+
isOptional ? t.optional(propType) : propType,
|
|
140
|
+
];
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const objectType = additionalProperties
|
|
145
|
+
? t.intersection([t.object(props), additionalProperties])
|
|
146
|
+
: t.object(props);
|
|
147
|
+
|
|
148
|
+
return isPartial ? t.reference("Partial", [objectType]) : objectType;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!schemaType) return t.unknown();
|
|
152
|
+
|
|
153
|
+
throw new Error(`Unsupported schema type: ${schemaType}`);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return getTs();
|
|
157
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { OpenAPIObject, ReferenceObject, SchemaObject } from "openapi3-ts/oas31";
|
|
2
|
+
import { get } from "pastable/server";
|
|
3
|
+
|
|
4
|
+
import { Box } from "./box";
|
|
5
|
+
import { isReferenceObject } from "./is-reference-object";
|
|
6
|
+
import { openApiSchemaToTs } from "./openapi-schema-to-ts";
|
|
7
|
+
import { normalizeString } from "./string-utils";
|
|
8
|
+
import { AnyBoxDef, GenericFactory } from "./types";
|
|
9
|
+
import { topologicalSort } from "./topological-sort";
|
|
10
|
+
|
|
11
|
+
const autocorrectRef = (ref: string) => (ref[1] === "/" ? ref : "#/" + ref.slice(1));
|
|
12
|
+
const componentsWithSchemas = ["schemas", "responses", "parameters", "requestBodies", "headers"];
|
|
13
|
+
|
|
14
|
+
export type RefInfo = {
|
|
15
|
+
/**
|
|
16
|
+
* The (potentially autocorrected) ref
|
|
17
|
+
* @example "#/components/schemas/MySchema"
|
|
18
|
+
*/
|
|
19
|
+
ref: string;
|
|
20
|
+
/**
|
|
21
|
+
* The name of the ref
|
|
22
|
+
* @example "MySchema"
|
|
23
|
+
* */
|
|
24
|
+
name: string;
|
|
25
|
+
normalized: string;
|
|
26
|
+
kind: "schemas" | "responses" | "parameters" | "requestBodies" | "headers";
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const createRefResolver = (doc: OpenAPIObject, factory: GenericFactory) => {
|
|
30
|
+
// both used for debugging purpose
|
|
31
|
+
const nameByRef = new Map<string, string>();
|
|
32
|
+
const refByName = new Map<string, string>();
|
|
33
|
+
|
|
34
|
+
const byRef = new Map<string, RefInfo>();
|
|
35
|
+
const byNormalized = new Map<string, RefInfo>();
|
|
36
|
+
|
|
37
|
+
const boxByRef = new Map<string, Box<AnyBoxDef>>();
|
|
38
|
+
|
|
39
|
+
const getSchemaByRef = <T = SchemaObject>(ref: string) => {
|
|
40
|
+
// #components -> #/components
|
|
41
|
+
const correctRef = autocorrectRef(ref);
|
|
42
|
+
const split = correctRef.split("/");
|
|
43
|
+
|
|
44
|
+
// "#/components/schemas/Something.jsonld" -> #/components/schemas
|
|
45
|
+
const path = split.slice(1, -1).join("/")!;
|
|
46
|
+
const normalizedPath = path.replace("#/", "").replace("#", "").replaceAll("/", ".");
|
|
47
|
+
const map = get(doc, normalizedPath) ?? ({} as any);
|
|
48
|
+
|
|
49
|
+
// "#/components/schemas/Something.jsonld" -> "Something.jsonld"
|
|
50
|
+
const name = split[split.length - 1]!;
|
|
51
|
+
const normalized = normalizeString(name);
|
|
52
|
+
|
|
53
|
+
nameByRef.set(correctRef, normalized);
|
|
54
|
+
refByName.set(normalized, correctRef);
|
|
55
|
+
|
|
56
|
+
const infos = { ref: correctRef, name, normalized, kind: normalizedPath.split(".")[1] as RefInfo["kind"] };
|
|
57
|
+
byRef.set(infos.ref, infos);
|
|
58
|
+
byNormalized.set(infos.normalized, infos);
|
|
59
|
+
|
|
60
|
+
// doc.components.schemas["Something.jsonld"]
|
|
61
|
+
const schema = map[name] as T;
|
|
62
|
+
if (!schema) {
|
|
63
|
+
throw new Error(`Unresolved ref "${name}" not found in "${path}"`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return schema;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const getInfosByRef = (ref: string) => byRef.get(autocorrectRef(ref))!;
|
|
70
|
+
|
|
71
|
+
const schemaEntries = Object.entries(doc.components ?? {}).filter(([key]) => componentsWithSchemas.includes(key));
|
|
72
|
+
|
|
73
|
+
schemaEntries.forEach(([key, component]) => {
|
|
74
|
+
Object.keys(component).map((name) => {
|
|
75
|
+
const ref = `#/components/${key}/${name}`;
|
|
76
|
+
getSchemaByRef(ref);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const directDependencies = new Map<string, Set<string>>();
|
|
81
|
+
|
|
82
|
+
// need to be done after all refs are resolved
|
|
83
|
+
schemaEntries.forEach(([key, component]) => {
|
|
84
|
+
Object.keys(component).map((name) => {
|
|
85
|
+
const ref = `#/components/${key}/${name}`;
|
|
86
|
+
const schema = getSchemaByRef(ref);
|
|
87
|
+
boxByRef.set(ref, openApiSchemaToTs({ schema, ctx: { factory, refs: { getInfosByRef } as any } }));
|
|
88
|
+
|
|
89
|
+
if (!directDependencies.has(ref)) {
|
|
90
|
+
directDependencies.set(ref, new Set<string>());
|
|
91
|
+
}
|
|
92
|
+
setSchemaDependencies(schema, directDependencies.get(ref)!);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const transitiveDependencies = getTransitiveDependencies(directDependencies);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
get: getSchemaByRef,
|
|
100
|
+
unwrap: <T extends ReferenceObject | {}>(component: T) => {
|
|
101
|
+
return (isReferenceObject(component) ? getSchemaByRef(component.$ref) : component) as Exclude<T, ReferenceObject>;
|
|
102
|
+
},
|
|
103
|
+
getInfosByRef: getInfosByRef,
|
|
104
|
+
infos: byRef,
|
|
105
|
+
/**
|
|
106
|
+
* Get the schemas in the order they should be generated, depending on their dependencies
|
|
107
|
+
* so that a schema is generated before the ones that depend on it
|
|
108
|
+
*/
|
|
109
|
+
getOrderedSchemas: () => {
|
|
110
|
+
const schemaOrderedByDependencies = topologicalSort(transitiveDependencies).map((ref) => {
|
|
111
|
+
const infos = getInfosByRef(ref);
|
|
112
|
+
return [boxByRef.get(infos.ref)!, infos] as [schema: Box<AnyBoxDef>, infos: RefInfo];
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return schemaOrderedByDependencies;
|
|
116
|
+
},
|
|
117
|
+
directDependencies,
|
|
118
|
+
transitiveDependencies,
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export interface RefResolver extends ReturnType<typeof createRefResolver> {}
|
|
123
|
+
|
|
124
|
+
const setSchemaDependencies = (schema: SchemaObject, deps: Set<string>) => {
|
|
125
|
+
const visit = (schema: SchemaObject | ReferenceObject): void => {
|
|
126
|
+
if (!schema) return;
|
|
127
|
+
|
|
128
|
+
if (isReferenceObject(schema)) {
|
|
129
|
+
deps.add(schema.$ref);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (schema.allOf) {
|
|
134
|
+
for (const allOf of schema.allOf) {
|
|
135
|
+
visit(allOf);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (schema.oneOf) {
|
|
142
|
+
for (const oneOf of schema.oneOf) {
|
|
143
|
+
visit(oneOf);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (schema.anyOf) {
|
|
150
|
+
for (const anyOf of schema.anyOf) {
|
|
151
|
+
visit(anyOf);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (schema.type === "array") {
|
|
158
|
+
if (!schema.items) return;
|
|
159
|
+
return void visit(schema.items);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (schema.type === "object" || schema.properties || schema.additionalProperties) {
|
|
163
|
+
if (schema.properties) {
|
|
164
|
+
for (const property in schema.properties) {
|
|
165
|
+
visit(schema.properties[property]!);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
170
|
+
visit(schema.additionalProperties);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
visit(schema);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const getTransitiveDependencies = (directDependencies: Map<string, Set<string>>) => {
|
|
179
|
+
const transitiveDependencies = new Map<string, Set<string>>();
|
|
180
|
+
const visitedsDeepRefs = new Set<string>();
|
|
181
|
+
|
|
182
|
+
directDependencies.forEach((deps, ref) => {
|
|
183
|
+
if (!transitiveDependencies.has(ref)) {
|
|
184
|
+
transitiveDependencies.set(ref, new Set());
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const visit = (depRef: string) => {
|
|
188
|
+
transitiveDependencies.get(ref)!.add(depRef);
|
|
189
|
+
|
|
190
|
+
const deps = directDependencies.get(depRef);
|
|
191
|
+
if (deps && ref !== depRef) {
|
|
192
|
+
deps.forEach((transitive) => {
|
|
193
|
+
const key = ref + "__" + transitive;
|
|
194
|
+
if (visitedsDeepRefs.has(key)) return;
|
|
195
|
+
|
|
196
|
+
visitedsDeepRefs.add(key);
|
|
197
|
+
visit(transitive);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
deps.forEach((dep) => visit(dep));
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return transitiveDependencies;
|
|
206
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { capitalize, kebabToCamel } from "pastable/server";
|
|
2
|
+
|
|
3
|
+
export const toSchemasRef = (name: string) => `#/components/schemas/${name}`;
|
|
4
|
+
|
|
5
|
+
export function normalizeString(text: string) {
|
|
6
|
+
const prefixed = prefixStringStartingWithNumberIfNeeded(text);
|
|
7
|
+
return prefixed
|
|
8
|
+
.normalize("NFKD") // The normalize() using NFKD method returns the Unicode Normalization Form of a given string.
|
|
9
|
+
.trim() // Remove whitespace from both sides of a string (optional)
|
|
10
|
+
.replace(/\s+/g, "_") // Replace spaces with _
|
|
11
|
+
.replace(/-+/g, "_") // Replace - with _
|
|
12
|
+
.replace(/[^\w\-]+/g, "_") // Remove all non-word chars
|
|
13
|
+
.replace(/--+/g, "-"); // Replace multiple - with single -
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const onlyWordRegex = /^\w+$/;
|
|
17
|
+
export const wrapWithQuotesIfNeeded = (str: string) => {
|
|
18
|
+
if (str[0] === '"' && str[str.length - 1] === '"') return str;
|
|
19
|
+
if (onlyWordRegex.test(str)) {
|
|
20
|
+
return str;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return `"${str}"`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const prefixStringStartingWithNumberIfNeeded = (str: string) => {
|
|
27
|
+
const firstAsNumber = Number(str[0]);
|
|
28
|
+
if (typeof firstAsNumber === "number" && !Number.isNaN(firstAsNumber)) {
|
|
29
|
+
return "_" + str;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return str;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const pathParamWithBracketsRegex = /({\w+})/g;
|
|
36
|
+
const wordPrecededByNonWordCharacter = /[^\w\-]+/g;
|
|
37
|
+
|
|
38
|
+
/** @example turns `/media-objects/{id}` into `MediaObjectsId` */
|
|
39
|
+
export const pathToVariableName = (path: string) =>
|
|
40
|
+
capitalize(kebabToCamel(path).replaceAll("/", "")) // /media-objects/{id} -> MediaObjects{id}
|
|
41
|
+
.replace(pathParamWithBracketsRegex, (group) => capitalize(group.slice(1, -1))) // {id} -> Id
|
|
42
|
+
.replace(wordPrecededByNonWordCharacter, "_"); // "/robots.txt" -> "/robots_txt"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** @see https://gist.github.com/RubyTuesdayDONO/5006455 */
|
|
2
|
+
export function topologicalSort(graph: Map<string, Set<string>>) {
|
|
3
|
+
const sorted: string[] = [], // sorted list of IDs ( returned value )
|
|
4
|
+
visited: Record<string, boolean> = {}; // hash: id of already visited node => true
|
|
5
|
+
|
|
6
|
+
function visit(name: string, ancestors: string[]) {
|
|
7
|
+
if (!Array.isArray(ancestors)) ancestors = [];
|
|
8
|
+
ancestors.push(name);
|
|
9
|
+
visited[name] = true;
|
|
10
|
+
|
|
11
|
+
const deps = graph.get(name);
|
|
12
|
+
if (deps) {
|
|
13
|
+
deps.forEach((dep) => {
|
|
14
|
+
if (ancestors.includes(dep)) {
|
|
15
|
+
// if already in ancestors, a closed chain (recursive relation) exists
|
|
16
|
+
return;
|
|
17
|
+
// throw new Error(
|
|
18
|
+
// 'Circular dependency "' + dep + '" is required by "' + name + '": ' + ancestors.join(" -> ")
|
|
19
|
+
// );
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// if already exists, do nothing
|
|
23
|
+
if (visited[dep]) return;
|
|
24
|
+
visit(dep, ancestors.slice(0)); // recursive call
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!sorted.includes(name)) sorted.push(name);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. topological sort
|
|
32
|
+
graph.forEach((_, name) => visit(name, []));
|
|
33
|
+
|
|
34
|
+
return sorted;
|
|
35
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createFactory, unwrap } from "./box-factory";
|
|
2
|
+
import { wrapWithQuotesIfNeeded } from "./string-utils";
|
|
3
|
+
|
|
4
|
+
export const tsFactory = createFactory({
|
|
5
|
+
union: (types) => types.map(unwrap).join(" | "),
|
|
6
|
+
intersection: (types) => types.map(unwrap).join(" & "),
|
|
7
|
+
array: (type) => `Array<${unwrap(type)}>`,
|
|
8
|
+
optional: (type) => `${unwrap(type)} | undefined`,
|
|
9
|
+
reference: (name, typeArgs) => `${name}${typeArgs ? `<${typeArgs.map(unwrap).join(", ")}>` : ""}`,
|
|
10
|
+
literal: (value) => value.toString(),
|
|
11
|
+
string: () => "string" as const,
|
|
12
|
+
number: () => "number" as const,
|
|
13
|
+
boolean: () => "boolean" as const,
|
|
14
|
+
unknown: () => "unknown" as const,
|
|
15
|
+
any: () => "any" as const,
|
|
16
|
+
never: () => "never" as const,
|
|
17
|
+
object: (props) => {
|
|
18
|
+
const propsString = Object.entries(props)
|
|
19
|
+
.map(([prop, type]) => `${wrapWithQuotesIfNeeded(prop)}: ${unwrap(type)}`)
|
|
20
|
+
.join(", ");
|
|
21
|
+
|
|
22
|
+
return `{ ${propsString} }`;
|
|
23
|
+
},
|
|
24
|
+
});
|