@mittwald/api-code-generator 0.0.0-development-04b7288-20240610 → 0.0.0-development-7c0a25b-20240611
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/dist/esm/commands/generate/index.js +70 -0
- package/dist/esm/commands/validate/index.js +21 -0
- package/dist/esm/generation/asyncStringJoin.js +4 -0
- package/dist/esm/generation/asyncStringJoin.test.js +5 -0
- package/dist/esm/generation/compileJsonSchema.js +10 -0
- package/dist/esm/generation/format.js +17 -0
- package/dist/esm/generation/model/CodeGenerationModel.js +43 -0
- package/dist/esm/generation/model/components/Components.js +61 -0
- package/dist/esm/generation/model/components/Parameters.js +30 -0
- package/dist/esm/generation/model/components/RequestBodies.js +25 -0
- package/dist/esm/generation/model/components/Response.js +27 -0
- package/dist/esm/generation/model/components/ResponseContent.js +12 -0
- package/dist/esm/generation/model/components/Responses.js +29 -0
- package/dist/esm/generation/model/components/Schemas.js +25 -0
- package/dist/esm/generation/model/components/SecurityScheme.js +21 -0
- package/dist/esm/generation/model/components/SecuritySchemes.js +43 -0
- package/dist/esm/generation/model/global/JSONSchema.js +44 -0
- package/dist/esm/generation/model/global/Name.js +20 -0
- package/dist/esm/generation/model/global/Name.test.js +10 -0
- package/dist/esm/generation/model/paths/Path.js +35 -0
- package/dist/esm/generation/model/paths/Paths.js +192 -0
- package/dist/esm/generation/model/paths/operation/Operation.js +108 -0
- package/dist/esm/generation/model/paths/operation/RequestParameters.js +98 -0
- package/dist/esm/generation/model/paths/operation/responses/Response.js +25 -0
- package/dist/esm/generation/model/paths/operation/responses/ResponseContent.js +31 -0
- package/dist/esm/generation/model/paths/operation/responses/ResponseContentTypes.js +47 -0
- package/dist/esm/generation/model/paths/operation/responses/Responses.js +31 -0
- package/dist/esm/generation/model/tags/Tag.js +12 -0
- package/dist/esm/generation/prepareTypeScriptOutput.js +10 -0
- package/dist/esm/generation/refs/assertNoRefs.js +6 -0
- package/dist/esm/generation/refs/componentRefsToCustomTypes.js +33 -0
- package/dist/esm/generation/refs/componentRefsToCustomTypes.test.js +44 -0
- package/dist/esm/generation/refs/extractComponentRefName.js +8 -0
- package/dist/esm/generation/refs/isRef.js +3 -0
- package/dist/esm/generation/refs/refNameToTSName.js +3 -0
- package/dist/esm/generation/refs/refNameToTSName.test.js +9 -0
- package/dist/esm/generation/refs/splitRefNamespaces.js +1 -0
- package/dist/esm/generation/refs/splitRefNamespaces.test.js +10 -0
- package/dist/esm/generation/tsNamespaceName.js +2 -0
- package/dist/esm/generation/tsTypeName.js +6 -0
- package/dist/esm/generation/tsTypeName.test.js +12 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/lib/makeError.js +7 -0
- package/dist/esm/lib/relativePath.js +3 -0
- package/dist/esm/lib/writeIfChangedAsync.js +8 -0
- package/dist/esm/loading/UniversalContentLoader.js +65 -0
- package/dist/esm/loading/UniversalContentLoader.test.js +40 -0
- package/dist/esm/loading/UniversalFileLoader.js +42 -0
- package/dist/esm/loading/types.js +1 -0
- package/dist/esm/openapi/OpenAPISchemaValidationError.js +17 -0
- package/dist/esm/openapi/OpenApiSpec.js +44 -0
- package/dist/types/commands/generate/index.d.ts +18 -0
- package/dist/types/commands/validate/index.d.ts +8 -0
- package/dist/types/generation/asyncStringJoin.d.ts +1 -0
- package/dist/types/generation/asyncStringJoin.test.d.ts +1 -0
- package/dist/types/generation/compileJsonSchema.d.ts +2 -0
- package/dist/types/generation/format.d.ts +1 -0
- package/dist/types/generation/model/CodeGenerationModel.d.ts +19 -0
- package/dist/types/generation/model/components/Components.d.ts +24 -0
- package/dist/types/generation/model/components/Parameters.d.ts +11 -0
- package/dist/types/generation/model/components/RequestBodies.d.ts +13 -0
- package/dist/types/generation/model/components/Response.d.ts +12 -0
- package/dist/types/generation/model/components/ResponseContent.d.ts +10 -0
- package/dist/types/generation/model/components/Responses.d.ts +13 -0
- package/dist/types/generation/model/components/Schemas.d.ts +13 -0
- package/dist/types/generation/model/components/SecurityScheme.d.ts +10 -0
- package/dist/types/generation/model/components/SecuritySchemes.d.ts +14 -0
- package/dist/types/generation/model/global/JSONSchema.d.ts +13 -0
- package/dist/types/generation/model/global/Name.d.ts +8 -0
- package/dist/types/generation/model/global/Name.test.d.ts +1 -0
- package/dist/types/generation/model/paths/Path.d.ts +13 -0
- package/dist/types/generation/model/paths/Paths.d.ts +25 -0
- package/dist/types/generation/model/paths/operation/Operation.d.ts +24 -0
- package/dist/types/generation/model/paths/operation/RequestParameters.d.ts +17 -0
- package/dist/types/generation/model/paths/operation/responses/Response.d.ts +16 -0
- package/dist/types/generation/model/paths/operation/responses/ResponseContent.d.ts +12 -0
- package/dist/types/generation/model/paths/operation/responses/ResponseContentTypes.d.ts +16 -0
- package/dist/types/generation/model/paths/operation/responses/Responses.d.ts +14 -0
- package/dist/types/generation/model/tags/Tag.d.ts +8 -0
- package/dist/types/generation/prepareTypeScriptOutput.d.ts +1 -0
- package/dist/types/generation/refs/assertNoRefs.d.ts +2 -0
- package/dist/types/generation/refs/componentRefsToCustomTypes.d.ts +1 -0
- package/dist/types/generation/refs/componentRefsToCustomTypes.test.d.ts +1 -0
- package/dist/types/generation/refs/extractComponentRefName.d.ts +2 -0
- package/dist/types/generation/refs/isRef.d.ts +2 -0
- package/dist/types/generation/refs/refNameToTSName.d.ts +1 -0
- package/dist/types/generation/refs/refNameToTSName.test.d.ts +1 -0
- package/dist/types/generation/refs/splitRefNamespaces.d.ts +1 -0
- package/dist/types/generation/refs/splitRefNamespaces.test.d.ts +1 -0
- package/dist/types/generation/tsNamespaceName.d.ts +1 -0
- package/dist/types/generation/tsTypeName.d.ts +1 -0
- package/dist/types/generation/tsTypeName.test.d.ts +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/lib/makeError.d.ts +1 -0
- package/dist/types/lib/relativePath.d.ts +1 -0
- package/dist/types/lib/writeIfChangedAsync.d.ts +1 -0
- package/dist/types/loading/UniversalContentLoader.d.ts +10 -0
- package/dist/types/loading/UniversalContentLoader.test.d.ts +1 -0
- package/dist/types/loading/UniversalFileLoader.d.ts +9 -0
- package/dist/types/loading/types.d.ts +4 -0
- package/dist/types/openapi/OpenAPISchemaValidationError.d.ts +7 -0
- package/dist/types/openapi/OpenApiSpec.d.ts +11 -0
- package/package.json +2 -2
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Name } from "../global/Name.js";
|
|
2
|
+
import { Path } from "./Path.js";
|
|
3
|
+
import { asyncStringJoin } from "../../asyncStringJoin.js";
|
|
4
|
+
import camelcase from "camelcase";
|
|
5
|
+
export class Paths {
|
|
6
|
+
static ns = "Paths";
|
|
7
|
+
paths;
|
|
8
|
+
model;
|
|
9
|
+
name;
|
|
10
|
+
constructor(model, paths) {
|
|
11
|
+
this.model = model;
|
|
12
|
+
this.name = new Name(Paths.ns, model.rootNamespace);
|
|
13
|
+
this.paths = Object.entries(paths ?? {})
|
|
14
|
+
.map(([name, pathDoc]) => {
|
|
15
|
+
if (pathDoc) {
|
|
16
|
+
return Path.fromDoc(this, name, pathDoc);
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
.filter((p) => !!p);
|
|
20
|
+
}
|
|
21
|
+
async compileTypes(opts) {
|
|
22
|
+
const t = {
|
|
23
|
+
ns: Paths.ns,
|
|
24
|
+
types: await asyncStringJoin(this.paths, (path) => path.compileTypes(opts)),
|
|
25
|
+
};
|
|
26
|
+
return `\
|
|
27
|
+
namespace ${t.ns} {
|
|
28
|
+
${t.types}
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
}
|
|
32
|
+
async compileOperationTypes() {
|
|
33
|
+
const t = {
|
|
34
|
+
types: this.getFlattenedOperations()
|
|
35
|
+
.map((operation) => operation.compileRequestResponseTypes())
|
|
36
|
+
.join("\r\n"),
|
|
37
|
+
};
|
|
38
|
+
return `
|
|
39
|
+
namespace Operations {
|
|
40
|
+
${t.types}
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
compileDescriptors() {
|
|
45
|
+
const t = {
|
|
46
|
+
ns: this.model.rootNamespace.tsType,
|
|
47
|
+
descriptors: this.getFlattenedOperations()
|
|
48
|
+
.map((operation) => operation.compileDescriptor())
|
|
49
|
+
.join("\r\n"),
|
|
50
|
+
};
|
|
51
|
+
return `\
|
|
52
|
+
import { Simplify } from "@mittwald/api-client-commons";
|
|
53
|
+
import { RequestType } from "@mittwald/api-client-commons";
|
|
54
|
+
import { Response } from "@mittwald/api-client-commons";
|
|
55
|
+
import { OpenAPIOperation } from "@mittwald/api-client-commons";
|
|
56
|
+
import { ${t.ns} } from "./types.js";
|
|
57
|
+
|
|
58
|
+
${t.descriptors}
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
getFlattenedOperations(httpMethod) {
|
|
62
|
+
return this.paths
|
|
63
|
+
.flatMap((p) => p.operations)
|
|
64
|
+
.filter((p) => httpMethod === undefined ||
|
|
65
|
+
p.httpMethod.raw.toLowerCase() === httpMethod.toLowerCase());
|
|
66
|
+
}
|
|
67
|
+
getGroupedOperationsByTag(httpMethod) {
|
|
68
|
+
const result = new Map();
|
|
69
|
+
for (const operation of this.getFlattenedOperations(httpMethod)) {
|
|
70
|
+
for (const tag of operation.tags) {
|
|
71
|
+
const operationsOfTag = result.get(tag);
|
|
72
|
+
if (operationsOfTag) {
|
|
73
|
+
operationsOfTag.push(operation);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
result.set(tag, [operation]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return Array.from(result.entries());
|
|
81
|
+
}
|
|
82
|
+
compileClientMethodsOfTag(tag, operations) {
|
|
83
|
+
const t = {
|
|
84
|
+
tag: tag.name.tsVar,
|
|
85
|
+
comment: tag.description,
|
|
86
|
+
methods: operations
|
|
87
|
+
.map((op) => op.compileClientMethod(tag))
|
|
88
|
+
.join(",\r\n"),
|
|
89
|
+
};
|
|
90
|
+
return `\
|
|
91
|
+
/** ${t.comment} */
|
|
92
|
+
public readonly ${t.tag} = {
|
|
93
|
+
${t.methods}
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
compileClient() {
|
|
98
|
+
const t = {
|
|
99
|
+
clientClassName: `${this.model.rootNamespace.tsType}Client`,
|
|
100
|
+
methods: this.getGroupedOperationsByTag()
|
|
101
|
+
.map(([tag, operations]) => this.compileClientMethodsOfTag(tag, operations))
|
|
102
|
+
.join("\r\n"),
|
|
103
|
+
};
|
|
104
|
+
return `\
|
|
105
|
+
import * as descriptors from "./descriptors.js";
|
|
106
|
+
import { ApiClientBase } from "@mittwald/api-client-commons";
|
|
107
|
+
|
|
108
|
+
export class ${t.clientClassName} extends ApiClientBase {
|
|
109
|
+
${t.methods}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default ${t.clientClassName}
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
compileReactClientApiName(tag) {
|
|
116
|
+
return `build${camelcase(tag.name.tsVar, {
|
|
117
|
+
pascalCase: true,
|
|
118
|
+
})}Api`;
|
|
119
|
+
}
|
|
120
|
+
compileReactClientApiBuilderOfTag(baseClientClassName, tag, operations) {
|
|
121
|
+
const t = {
|
|
122
|
+
apiName: this.compileReactClientApiName(tag),
|
|
123
|
+
methods: operations
|
|
124
|
+
.map((op) => op.compileReactClientMethod(tag))
|
|
125
|
+
.join(",\r\n"),
|
|
126
|
+
baseClientClassName,
|
|
127
|
+
};
|
|
128
|
+
return `\
|
|
129
|
+
const ${t.apiName} = (baseClient: ${t.baseClientClassName}) => ({
|
|
130
|
+
${t.methods}
|
|
131
|
+
})
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
compileReactClientPropertyInitializer(tag) {
|
|
135
|
+
const tagName = tag.name.tsVar;
|
|
136
|
+
const t = {
|
|
137
|
+
apiName: this.compileReactClientApiName(tag),
|
|
138
|
+
comment: tag.description,
|
|
139
|
+
};
|
|
140
|
+
return `\
|
|
141
|
+
this.${tagName} = ${t.apiName}(baseClient);
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
compileReactClientProperties(tag) {
|
|
145
|
+
const tagName = tag.name.tsVar;
|
|
146
|
+
const t = {
|
|
147
|
+
apiName: this.compileReactClientApiName(tag),
|
|
148
|
+
comment: tag.description,
|
|
149
|
+
};
|
|
150
|
+
return `\
|
|
151
|
+
/** ${t.comment} */
|
|
152
|
+
public readonly ${tagName}: ReturnType<typeof ${t.apiName}>;
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
compileReactClient() {
|
|
156
|
+
const baseClientClassName = `${this.model.rootNamespace.tsType}Client`;
|
|
157
|
+
const clientClassName = `${this.model.rootNamespace.tsType}ClientReact`;
|
|
158
|
+
const t = {
|
|
159
|
+
baseClientClassName,
|
|
160
|
+
clientClassName,
|
|
161
|
+
apiBuilder: this.getGroupedOperationsByTag("GET")
|
|
162
|
+
.map(([tag, operations]) => this.compileReactClientApiBuilderOfTag(baseClientClassName, tag, operations))
|
|
163
|
+
.join("\r\n"),
|
|
164
|
+
clientProperties: this.getGroupedOperationsByTag("GET")
|
|
165
|
+
.map(([tag]) => this.compileReactClientProperties(tag))
|
|
166
|
+
.join("\r\n"),
|
|
167
|
+
clientPropertyInitializers: this.getGroupedOperationsByTag("GET")
|
|
168
|
+
.map(([tag]) => this.compileReactClientPropertyInitializer(tag))
|
|
169
|
+
.join("\r\n"),
|
|
170
|
+
};
|
|
171
|
+
return `\
|
|
172
|
+
import ${t.baseClientClassName} from "./client.js";
|
|
173
|
+
import { ApiCallAsyncResourceFactory } from "@mittwald/api-client-commons/react";
|
|
174
|
+
import * as descriptors from "./descriptors.js";
|
|
175
|
+
export * from "@mittwald/react-use-promise";
|
|
176
|
+
|
|
177
|
+
${t.apiBuilder}
|
|
178
|
+
|
|
179
|
+
export class ${t.clientClassName} {
|
|
180
|
+
${t.clientProperties}
|
|
181
|
+
|
|
182
|
+
private constructor(baseClient: ${t.baseClientClassName}) {
|
|
183
|
+
${t.clientPropertyInitializers}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
public static fromBaseClient(baseClient: ${t.baseClientClassName}): ${t.clientClassName} {
|
|
187
|
+
return new ${t.clientClassName}(baseClient);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { RequestParameters } from "./RequestParameters.js";
|
|
2
|
+
import { Name } from "../../global/Name.js";
|
|
3
|
+
import { Responses } from "./responses/Responses.js";
|
|
4
|
+
export class Operation {
|
|
5
|
+
path;
|
|
6
|
+
id;
|
|
7
|
+
httpMethod;
|
|
8
|
+
parameters;
|
|
9
|
+
responses;
|
|
10
|
+
summary;
|
|
11
|
+
tags;
|
|
12
|
+
constructor(path, httpMethod, doc) {
|
|
13
|
+
const fallbackId = `${httpMethod.raw}-${path.name.raw}`;
|
|
14
|
+
this.id = new Name(doc.operationId ?? fallbackId);
|
|
15
|
+
this.path = path;
|
|
16
|
+
this.httpMethod = httpMethod;
|
|
17
|
+
this.parameters = RequestParameters.fromDoc(this, doc);
|
|
18
|
+
this.responses = new Responses(this, doc.responses);
|
|
19
|
+
this.summary = doc.summary;
|
|
20
|
+
this.tags =
|
|
21
|
+
doc.tags
|
|
22
|
+
?.map((name) => path.paths.model.tags.find((t) => t.name.raw === name))
|
|
23
|
+
.filter((t) => !!t) ?? [];
|
|
24
|
+
}
|
|
25
|
+
static fromDoc(path, httpMethod, doc) {
|
|
26
|
+
return new Operation(path, new Name(httpMethod, path.name), doc);
|
|
27
|
+
}
|
|
28
|
+
async compileTypes(options) {
|
|
29
|
+
const t = {
|
|
30
|
+
ns: this.httpMethod.tsType,
|
|
31
|
+
parameters: await this.parameters.compileTypes(options),
|
|
32
|
+
responses: await this.responses.compileTypes(options),
|
|
33
|
+
};
|
|
34
|
+
return `\
|
|
35
|
+
namespace ${t.ns} {
|
|
36
|
+
${t.parameters};
|
|
37
|
+
${t.responses};
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
compileDescriptor() {
|
|
42
|
+
const t = {
|
|
43
|
+
constName: this.id.tsVar,
|
|
44
|
+
requestType: this.parameters.compileDescriptorRequestType(),
|
|
45
|
+
responseTypes: this.responses.compileDescriptorTypes(),
|
|
46
|
+
path: this.path.name.raw,
|
|
47
|
+
method: this.httpMethod.raw.toUpperCase(),
|
|
48
|
+
operationId: this.id.raw,
|
|
49
|
+
};
|
|
50
|
+
return `\
|
|
51
|
+
/** ${this.summary} */
|
|
52
|
+
export const ${t.constName}: OpenAPIOperation<
|
|
53
|
+
${t.requestType},
|
|
54
|
+
${t.responseTypes}
|
|
55
|
+
> = {
|
|
56
|
+
path: "${t.path}",
|
|
57
|
+
method: "${t.method}",
|
|
58
|
+
operationId: "${t.operationId}",
|
|
59
|
+
};
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
compileRequestResponseTypes() {
|
|
63
|
+
const t = {
|
|
64
|
+
ns: this.id.tsType,
|
|
65
|
+
descriptor: this.id.tsVar,
|
|
66
|
+
};
|
|
67
|
+
return `\
|
|
68
|
+
namespace ${t.ns} {
|
|
69
|
+
type RequestData = InferredRequestData<typeof descriptors.${t.descriptor}>;
|
|
70
|
+
type ResponseData<TStatus extends HttpStatus = 200> = InferredResponseData<typeof descriptors.${t.descriptor}, TStatus>;
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
getMethodName(tag) {
|
|
75
|
+
const methodName = this.id.tsVar;
|
|
76
|
+
return new Name(methodName.startsWith(tag.name.tsVar)
|
|
77
|
+
? methodName.substring(tag.name.tsVar.length)
|
|
78
|
+
: methodName).tsVar;
|
|
79
|
+
}
|
|
80
|
+
compileClientMethod(tag) {
|
|
81
|
+
const methodName = this.getMethodName(tag);
|
|
82
|
+
const t = {
|
|
83
|
+
methodName,
|
|
84
|
+
descriptorName: this.id.tsVar,
|
|
85
|
+
};
|
|
86
|
+
return `\
|
|
87
|
+
/** ${this.summary} */
|
|
88
|
+
${t.methodName}: this.requestFunctionFactory(
|
|
89
|
+
descriptors.${t.descriptorName}
|
|
90
|
+
)
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
compileReactClientMethod(tag) {
|
|
94
|
+
const methodName = this.getMethodName(tag);
|
|
95
|
+
const t = {
|
|
96
|
+
methodName,
|
|
97
|
+
descriptorName: this.id.tsVar,
|
|
98
|
+
tag: tag.name.tsVar,
|
|
99
|
+
};
|
|
100
|
+
return `\
|
|
101
|
+
/** ${this.summary} */
|
|
102
|
+
${t.methodName}: new ApiCallAsyncResourceFactory(
|
|
103
|
+
descriptors.${t.descriptorName},
|
|
104
|
+
baseClient.${t.tag}.${t.methodName}
|
|
105
|
+
).getApiResource
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { JSONSchema } from "../../global/JSONSchema.js";
|
|
2
|
+
import { Name } from "../../global/Name.js";
|
|
3
|
+
import { isRef } from "../../../refs/isRef.js";
|
|
4
|
+
import invariant from "invariant";
|
|
5
|
+
export class RequestParameters {
|
|
6
|
+
static ns = "Parameters";
|
|
7
|
+
path;
|
|
8
|
+
query;
|
|
9
|
+
header;
|
|
10
|
+
body;
|
|
11
|
+
operation;
|
|
12
|
+
constructor(operation, body, path, query, header) {
|
|
13
|
+
this.operation = operation;
|
|
14
|
+
this.path = path;
|
|
15
|
+
this.query = query;
|
|
16
|
+
this.header = header;
|
|
17
|
+
this.body = body;
|
|
18
|
+
}
|
|
19
|
+
static constructParametersSchema(components, name, $in, parameters = [], securitySchemes) {
|
|
20
|
+
const properties = {};
|
|
21
|
+
const required = [];
|
|
22
|
+
for (const parameter of parameters) {
|
|
23
|
+
const resolvedParameter = components.resolveRef("parameters", parameter);
|
|
24
|
+
if (resolvedParameter.in === $in) {
|
|
25
|
+
const jsonSchemaObject = isRef(parameter)
|
|
26
|
+
? parameter
|
|
27
|
+
: parameter.schema;
|
|
28
|
+
invariant(!!jsonSchemaObject, `Could not find schema for request parameter ${name.raw}`);
|
|
29
|
+
properties[resolvedParameter.name] = jsonSchemaObject;
|
|
30
|
+
if (resolvedParameter.required) {
|
|
31
|
+
required.push(resolvedParameter.name);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const securitySchemesJSONSchemas = securitySchemes
|
|
36
|
+
.filter((s) => s.in === $in)
|
|
37
|
+
.map((s) => s.jsonSchema.asCustomTypeRef());
|
|
38
|
+
const jsonSchemaObject = {
|
|
39
|
+
allOf: [
|
|
40
|
+
{ type: "object", required, properties },
|
|
41
|
+
...securitySchemesJSONSchemas,
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
return new JSONSchema(new Name($in, name), jsonSchemaObject);
|
|
45
|
+
}
|
|
46
|
+
static fromDoc(operation, doc) {
|
|
47
|
+
const name = new Name(RequestParameters.ns, operation.httpMethod);
|
|
48
|
+
const requestBodySchema = doc.requestBody &&
|
|
49
|
+
"content" in doc.requestBody &&
|
|
50
|
+
"application/json" in doc.requestBody.content
|
|
51
|
+
? doc.requestBody.content["application/json"].schema
|
|
52
|
+
: doc.requestBody;
|
|
53
|
+
const body = requestBodySchema
|
|
54
|
+
? new JSONSchema(new Name("requestBody", name), requestBodySchema)
|
|
55
|
+
: undefined;
|
|
56
|
+
const requiredSecuritySchemeNames = doc.security?.flatMap((obj) => Object.keys(obj)) ?? [];
|
|
57
|
+
const requiredSecuritySchemes = requiredSecuritySchemeNames.map((name) => operation.path.paths.model.components.securitySchemes.requireScheme(name));
|
|
58
|
+
const components = operation.path.paths.model.components;
|
|
59
|
+
const path = RequestParameters.constructParametersSchema(components, name, "path", doc.parameters, requiredSecuritySchemes);
|
|
60
|
+
const header = RequestParameters.constructParametersSchema(components, name, "header", doc.parameters, requiredSecuritySchemes);
|
|
61
|
+
const query = RequestParameters.constructParametersSchema(components, name, "query", doc.parameters, requiredSecuritySchemes);
|
|
62
|
+
return new RequestParameters(operation, body, path, query, header);
|
|
63
|
+
}
|
|
64
|
+
async compileTypes(opts) {
|
|
65
|
+
const header = this.header?.cloneWithOptionalProperties(opts.optionalHeaders ?? []);
|
|
66
|
+
const t = {
|
|
67
|
+
ns: RequestParameters.ns,
|
|
68
|
+
path: (await this.path?.compile(opts)) ?? "",
|
|
69
|
+
body: (await this.body?.compile(opts)) ?? "",
|
|
70
|
+
header: (await header?.compile(opts)) ?? "",
|
|
71
|
+
query: (await this.query?.compile(opts)) ?? "",
|
|
72
|
+
};
|
|
73
|
+
return `\
|
|
74
|
+
namespace ${t.ns} {
|
|
75
|
+
${t.path}
|
|
76
|
+
${t.body}
|
|
77
|
+
${t.header}
|
|
78
|
+
${t.query}
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
compileDescriptorRequestType() {
|
|
83
|
+
const t = {
|
|
84
|
+
body: this.body?.name.tsTypeWithNamespace ?? "null",
|
|
85
|
+
path: this.path?.name.tsTypeWithNamespace ?? "null",
|
|
86
|
+
header: this.header?.name.tsTypeWithNamespace ?? "null",
|
|
87
|
+
query: this.query?.name.tsTypeWithNamespace ?? "null",
|
|
88
|
+
};
|
|
89
|
+
return `\
|
|
90
|
+
RequestType<
|
|
91
|
+
Simplify<${t.body}>,
|
|
92
|
+
Simplify<${t.path}>,
|
|
93
|
+
Simplify<${t.query}>,
|
|
94
|
+
Simplify<${t.header}>
|
|
95
|
+
>
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ResponseContentTypes } from "./ResponseContentTypes.js";
|
|
2
|
+
export class Response {
|
|
3
|
+
responses;
|
|
4
|
+
contentTypes;
|
|
5
|
+
httpStatus;
|
|
6
|
+
constructor(responses, httpStatus, doc) {
|
|
7
|
+
this.responses = responses;
|
|
8
|
+
this.httpStatus = httpStatus;
|
|
9
|
+
this.contentTypes = new ResponseContentTypes(this, doc);
|
|
10
|
+
}
|
|
11
|
+
static fromDoc(responses, httpStatus, doc) {
|
|
12
|
+
return new Response(responses, httpStatus, doc);
|
|
13
|
+
}
|
|
14
|
+
async compileContentTypes(opts) {
|
|
15
|
+
const t = {
|
|
16
|
+
ns: this.httpStatus.tsType,
|
|
17
|
+
types: await this.contentTypes.compileTypes(opts),
|
|
18
|
+
};
|
|
19
|
+
return `\
|
|
20
|
+
namespace ${this.httpStatus.tsType} {
|
|
21
|
+
${t.types}
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { JSONSchema } from "../../../global/JSONSchema.js";
|
|
2
|
+
import { Name } from "../../../global/Name.js";
|
|
3
|
+
export class ResponseContent {
|
|
4
|
+
mediaType;
|
|
5
|
+
schema;
|
|
6
|
+
contentTypes;
|
|
7
|
+
constructor(contentTypes, mediaType, schema) {
|
|
8
|
+
this.mediaType = new Name(mediaType, contentTypes.name);
|
|
9
|
+
this.schema = new JSONSchema(this.mediaType, schema);
|
|
10
|
+
this.contentTypes = contentTypes;
|
|
11
|
+
}
|
|
12
|
+
static buildEmpty(contentTypes) {
|
|
13
|
+
return new ResponseContent(contentTypes, "empty");
|
|
14
|
+
}
|
|
15
|
+
compileDescriptorsResponseContentType() {
|
|
16
|
+
const t = {
|
|
17
|
+
type: this.mediaType.tsTypeWithNamespace,
|
|
18
|
+
httpStatus: this.contentTypes.response.httpStatus.raw === "default"
|
|
19
|
+
? '"default"'
|
|
20
|
+
: this.contentTypes.response.httpStatus.raw,
|
|
21
|
+
mediaType: this.mediaType.raw,
|
|
22
|
+
};
|
|
23
|
+
return `\
|
|
24
|
+
Response<
|
|
25
|
+
Simplify<${t.type}>,
|
|
26
|
+
${t.httpStatus},
|
|
27
|
+
"${t.mediaType}"
|
|
28
|
+
>
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Name } from "../../../global/Name.js";
|
|
2
|
+
import { ResponseContent } from "./ResponseContent.js";
|
|
3
|
+
import { asyncStringJoin } from "../../../../asyncStringJoin.js";
|
|
4
|
+
import invariant from "invariant";
|
|
5
|
+
export class ResponseContentTypes {
|
|
6
|
+
static ns = "Content";
|
|
7
|
+
contentTypes;
|
|
8
|
+
name;
|
|
9
|
+
response;
|
|
10
|
+
constructor(response, responseDoc) {
|
|
11
|
+
this.response = response;
|
|
12
|
+
this.name = new Name(ResponseContentTypes.ns, response.httpStatus);
|
|
13
|
+
this.contentTypes =
|
|
14
|
+
"content" in responseDoc
|
|
15
|
+
? this.buildContentTypesFromResponseObject(responseDoc)
|
|
16
|
+
: "$ref" in responseDoc
|
|
17
|
+
? this.buildContentTypesFromReferenceObject(responseDoc)
|
|
18
|
+
: [ResponseContent.buildEmpty(this)];
|
|
19
|
+
}
|
|
20
|
+
buildContentTypesFromResponseObject(doc) {
|
|
21
|
+
return Object.entries(doc.content ?? {}).map(([mediaType, mediaTypeObject]) => new ResponseContent(this, mediaType, mediaTypeObject.schema));
|
|
22
|
+
}
|
|
23
|
+
buildContentTypesFromReferenceObject(doc) {
|
|
24
|
+
const responseComponents = this.response.responses.operation.path.paths.model.components.responses
|
|
25
|
+
.responses;
|
|
26
|
+
const referencedResponse = responseComponents.find((r) => `#/components/responses/${r.name.raw}` === doc.$ref);
|
|
27
|
+
invariant(!!referencedResponse, `Referenced response ${doc.$ref} not found`);
|
|
28
|
+
return referencedResponse.contents.map((c) => new ResponseContent(this, c.mediaType.raw, c.schema));
|
|
29
|
+
}
|
|
30
|
+
compileDescriptorTypes() {
|
|
31
|
+
return this.contentTypes
|
|
32
|
+
.flatMap((contentType) => contentType.compileDescriptorsResponseContentType())
|
|
33
|
+
.filter((s) => s !== "")
|
|
34
|
+
.join(" | ");
|
|
35
|
+
}
|
|
36
|
+
async compileTypes(opts) {
|
|
37
|
+
const t = {
|
|
38
|
+
ns: ResponseContentTypes.ns,
|
|
39
|
+
types: await asyncStringJoin(this.contentTypes, (contentType) => contentType.schema.compile(opts)),
|
|
40
|
+
};
|
|
41
|
+
return `\
|
|
42
|
+
namespace ${t.ns} {
|
|
43
|
+
${t.types}
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Name } from "../../../global/Name.js";
|
|
2
|
+
import { asyncStringJoin } from "../../../../asyncStringJoin.js";
|
|
3
|
+
import { Response } from "./Response.js";
|
|
4
|
+
export class Responses {
|
|
5
|
+
static ns = "Responses";
|
|
6
|
+
responses;
|
|
7
|
+
operation;
|
|
8
|
+
name;
|
|
9
|
+
constructor(operation, responses) {
|
|
10
|
+
this.operation = operation;
|
|
11
|
+
this.name = new Name(Responses.ns, operation.httpMethod);
|
|
12
|
+
this.responses = Object.entries(responses ?? {}).map(([name, response]) => Response.fromDoc(this, new Name(name, this.name), response));
|
|
13
|
+
}
|
|
14
|
+
async compileTypes(opts) {
|
|
15
|
+
const t = {
|
|
16
|
+
responses: await asyncStringJoin(this.responses, (response) => response.compileContentTypes(opts)),
|
|
17
|
+
ns: Responses.ns,
|
|
18
|
+
};
|
|
19
|
+
return `\
|
|
20
|
+
namespace ${t.ns} {
|
|
21
|
+
${t.responses}
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
compileDescriptorTypes() {
|
|
26
|
+
return this.responses
|
|
27
|
+
.flatMap((response) => response.contentTypes.compileDescriptorTypes())
|
|
28
|
+
.filter((s) => s !== "")
|
|
29
|
+
.join(" | ");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Name } from "../global/Name.js";
|
|
2
|
+
export class Tag {
|
|
3
|
+
name;
|
|
4
|
+
description;
|
|
5
|
+
constructor(name, description) {
|
|
6
|
+
this.name = name;
|
|
7
|
+
this.description = description;
|
|
8
|
+
}
|
|
9
|
+
static fromDoc(doc) {
|
|
10
|
+
return new Tag(new Name(doc.name), doc.description);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { format } from "./format.js";
|
|
2
|
+
const header = `\
|
|
3
|
+
/* eslint-disable */
|
|
4
|
+
/* prettier-ignore */
|
|
5
|
+
/* This file is auto-generated with acg (@mittwald/api-code-generator) */
|
|
6
|
+
`;
|
|
7
|
+
export const prepareTypeScriptOutput = async (content) => {
|
|
8
|
+
const formatted = await format(content);
|
|
9
|
+
return header + formatted;
|
|
10
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { refNameToTSName } from "./refNameToTSName.js";
|
|
2
|
+
import is from "@sindresorhus/is";
|
|
3
|
+
import cloneDeep from "clone-deep";
|
|
4
|
+
const getComponentRef = (something) => {
|
|
5
|
+
if ("$ref" in something &&
|
|
6
|
+
typeof something.$ref === "string" &&
|
|
7
|
+
something.$ref.startsWith("#/components/")) {
|
|
8
|
+
return something.$ref;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
export const componentRefsToCustomTypes = (rootNamespace, something, clone = true) => {
|
|
12
|
+
if (clone) {
|
|
13
|
+
something = cloneDeep(something);
|
|
14
|
+
}
|
|
15
|
+
if (!is.nonEmptyObject(something)) {
|
|
16
|
+
return something;
|
|
17
|
+
}
|
|
18
|
+
if (is.array(something)) {
|
|
19
|
+
return something.map((item) => componentRefsToCustomTypes(rootNamespace, item, false));
|
|
20
|
+
}
|
|
21
|
+
const componentRef = getComponentRef(something);
|
|
22
|
+
if (componentRef !== undefined) {
|
|
23
|
+
// see https://github.com/bcherny/json-schema-to-typescript#custom-schema-properties
|
|
24
|
+
return {
|
|
25
|
+
tsType: refNameToTSName(rootNamespace, componentRef),
|
|
26
|
+
type: "object",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return Object.fromEntries(Object.entries(something).map(([key, value]) => [
|
|
30
|
+
key,
|
|
31
|
+
componentRefsToCustomTypes(rootNamespace, value, false),
|
|
32
|
+
]));
|
|
33
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { componentRefsToCustomTypes } from "./componentRefsToCustomTypes.js";
|
|
2
|
+
test.each([
|
|
3
|
+
[
|
|
4
|
+
{
|
|
5
|
+
$ref: "#/components/foo/bar",
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
type: "object",
|
|
9
|
+
tsType: "Test.Components.Foo.Bar",
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
[
|
|
13
|
+
{
|
|
14
|
+
$ref: "#/no/component",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
$ref: "#/no/component",
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
[
|
|
21
|
+
{
|
|
22
|
+
noRef: "No fun",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
noRef: "No fun",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
{
|
|
30
|
+
nested: {
|
|
31
|
+
$ref: "#/components/foo/bar",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
nested: {
|
|
36
|
+
type: "object",
|
|
37
|
+
tsType: "Test.Components.Foo.Bar",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
])("componentRefsToCustomTypes works for test %#", (input, expected) => {
|
|
42
|
+
const result = componentRefsToCustomTypes("test", input);
|
|
43
|
+
expect(result).toMatchObject(expected);
|
|
44
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { splitRefNamespaces } from "./splitRefNamespaces.js";
|
|
2
|
+
export const extractComponentRefName = (ref, component) => {
|
|
3
|
+
const parts = splitRefNamespaces(ref);
|
|
4
|
+
if (parts[parts.length - 2] === component) {
|
|
5
|
+
return parts[parts.length - 1];
|
|
6
|
+
}
|
|
7
|
+
throw new Error(`Could not extract ref name (component: ${component}, ref: ${ref})`);
|
|
8
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { refNameToTSName } from "./refNameToTSName.js";
|
|
2
|
+
test.each([
|
|
3
|
+
["#/foo/bar", "Test.Foo.Bar"],
|
|
4
|
+
["foo", "Test.Foo"],
|
|
5
|
+
["#/", "Test"],
|
|
6
|
+
])("splitRefNamespaces works for test %#", (input, expected) => {
|
|
7
|
+
const result = refNameToTSName("Test", input);
|
|
8
|
+
expect(result).toEqual(expected);
|
|
9
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const splitRefNamespaces = ($ref) => $ref.split("/").filter((ns) => ns !== "#" && ns !== "");
|