@goast/typescript 0.8.0-beta.1 → 0.8.0
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/LICENSE +21 -21
- package/README.md +52 -52
- package/assets/client/angular/angular-service.utils.ts +82 -82
- package/assets/client/angular/request-builder.ts +377 -377
- package/assets/client/fetch/fetch-client.utils.ts +52 -52
- package/assets/client/k6/request-builder.js +448 -448
- package/assets/client/k6/request-builder.ts +366 -366
- package/assets/stubs/easy-network-stub/easy-network-stub.utils.ts +257 -257
- package/esm/src/generators/models/model-generator.d.ts +22 -0
- package/esm/src/generators/models/model-generator.d.ts.map +1 -1
- package/esm/src/generators/models/model-generator.js +37 -5
- package/esm/src/generators/services/angular-services/angular-service-generator.js +9 -9
- package/esm/src/generators/services/angular-services/angular-services-generator.js +2 -2
- package/esm/src/generators/services/easy-network-stub/easy-network-stub-generator.js +9 -9
- package/esm/src/generators/services/fetch-clients/fetch-client-generator.js +4 -4
- package/esm/src/generators/services/k6-clients/k6-client-generator.d.ts.map +1 -1
- package/esm/src/generators/services/k6-clients/k6-client-generator.js +23 -13
- package/esm/src/import-collection.d.ts.map +1 -1
- package/esm/src/import-collection.js +2 -2
- package/package.json +2 -2
- package/script/src/generators/models/model-generator.d.ts +22 -0
- package/script/src/generators/models/model-generator.d.ts.map +1 -1
- package/script/src/generators/models/model-generator.js +37 -5
- package/script/src/generators/services/angular-services/angular-service-generator.js +9 -9
- package/script/src/generators/services/angular-services/angular-services-generator.js +2 -2
- package/script/src/generators/services/easy-network-stub/easy-network-stub-generator.js +9 -9
- package/script/src/generators/services/fetch-clients/fetch-client-generator.js +4 -4
- package/script/src/generators/services/k6-clients/k6-client-generator.d.ts.map +1 -1
- package/script/src/generators/services/k6-clients/k6-client-generator.js +23 -13
- package/script/src/import-collection.d.ts.map +1 -1
- package/script/src/import-collection.js +1 -1
- package/src/mod.ts +7 -0
- package/src/src/assets.ts +9 -0
- package/src/src/ast/_index.ts +54 -0
- package/src/src/ast/common.ts +1 -0
- package/src/src/ast/index.ts +1 -0
- package/src/src/ast/node.ts +10 -0
- package/src/src/ast/nodes/argument.ts +40 -0
- package/src/src/ast/nodes/array-type.ts +49 -0
- package/src/src/ast/nodes/arrow-function.ts +91 -0
- package/src/src/ast/nodes/call.ts +55 -0
- package/src/src/ast/nodes/class.ts +133 -0
- package/src/src/ast/nodes/constructor-parameter.ts +70 -0
- package/src/src/ast/nodes/constructor.ts +77 -0
- package/src/src/ast/nodes/decorator.ts +76 -0
- package/src/src/ast/nodes/doc-tag.ts +291 -0
- package/src/src/ast/nodes/doc.ts +102 -0
- package/src/src/ast/nodes/enum-value.ts +72 -0
- package/src/src/ast/nodes/enum.ts +77 -0
- package/src/src/ast/nodes/export.ts +54 -0
- package/src/src/ast/nodes/function-type.ts +60 -0
- package/src/src/ast/nodes/function.ts +107 -0
- package/src/src/ast/nodes/generic-parameter.ts +71 -0
- package/src/src/ast/nodes/indexer.ts +60 -0
- package/src/src/ast/nodes/interface.ts +117 -0
- package/src/src/ast/nodes/intersection-type.ts +67 -0
- package/src/src/ast/nodes/lookup-type.ts +53 -0
- package/src/src/ast/nodes/method.ts +138 -0
- package/src/src/ast/nodes/object-type.ts +52 -0
- package/src/src/ast/nodes/object.ts +48 -0
- package/src/src/ast/nodes/parameter.ts +85 -0
- package/src/src/ast/nodes/property-accessor.ts +173 -0
- package/src/src/ast/nodes/property.ts +192 -0
- package/src/src/ast/nodes/reference.ts +194 -0
- package/src/src/ast/nodes/string.ts +49 -0
- package/src/src/ast/nodes/tuple.ts +53 -0
- package/src/src/ast/nodes/type-alias.ts +75 -0
- package/src/src/ast/nodes/typeof.ts +42 -0
- package/src/src/ast/nodes/types.ts +40 -0
- package/src/src/ast/nodes/union-type.ts +62 -0
- package/src/src/ast/nodes/variable.ts +73 -0
- package/src/src/ast/references/angular.ts +24 -0
- package/src/src/ast/references/easy-network-stub.ts +21 -0
- package/src/src/ast/references/index.ts +5 -0
- package/src/src/ast/references/k6.ts +8 -0
- package/src/src/ast/references/rxjs.ts +7 -0
- package/src/src/ast/references/typescript.ts +35 -0
- package/src/src/ast/utils/get-type-script-builder-options.ts +19 -0
- package/src/src/ast/utils/to-ts-node.ts +27 -0
- package/src/src/ast/utils/write-ts-generic-parameters.ts +25 -0
- package/src/src/ast/utils/write-ts-members.ts +28 -0
- package/src/src/ast/utils/write-ts-nodes.ts +35 -0
- package/src/src/ast/utils/write-ts-parameters.ts +25 -0
- package/src/src/common-results.ts +26 -0
- package/src/src/config.ts +96 -0
- package/src/src/file-builder.ts +137 -0
- package/src/src/generators/file-generator.ts +15 -0
- package/src/src/generators/index.ts +6 -0
- package/src/src/generators/models/index.ts +3 -0
- package/src/src/generators/models/model-generator.ts +530 -0
- package/src/src/generators/models/models-generator.ts +108 -0
- package/src/src/generators/models/models.ts +121 -0
- package/src/src/generators/services/angular-services/angular-service-generator.ts +431 -0
- package/src/src/generators/services/angular-services/angular-services-generator.ts +295 -0
- package/src/src/generators/services/angular-services/index.ts +3 -0
- package/src/src/generators/services/angular-services/models.ts +277 -0
- package/src/src/generators/services/angular-services/refs.ts +54 -0
- package/src/src/generators/services/easy-network-stub/easy-network-stub-generator.ts +250 -0
- package/src/src/generators/services/easy-network-stub/easy-network-stubs-generator.ts +211 -0
- package/src/src/generators/services/easy-network-stub/index.ts +3 -0
- package/src/src/generators/services/easy-network-stub/models.ts +124 -0
- package/src/src/generators/services/easy-network-stub/refs.ts +47 -0
- package/src/src/generators/services/fetch-clients/fetch-client-generator.ts +305 -0
- package/src/src/generators/services/fetch-clients/fetch-clients-generator.ts +163 -0
- package/src/src/generators/services/fetch-clients/index.ts +3 -0
- package/src/src/generators/services/fetch-clients/models.ts +130 -0
- package/src/src/generators/services/fetch-clients/refs.ts +21 -0
- package/src/src/generators/services/k6-clients/index.ts +3 -0
- package/src/src/generators/services/k6-clients/k6-client-generator.ts +515 -0
- package/src/src/generators/services/k6-clients/k6-clients-generator.ts +171 -0
- package/src/src/generators/services/k6-clients/models.ts +247 -0
- package/src/src/generators/services/k6-clients/refs.ts +16 -0
- package/src/src/import-collection.ts +236 -0
- package/src/src/utils.ts +79 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type ApiSchema,
|
|
5
|
+
type AppendValue,
|
|
6
|
+
type AppendValueGroup,
|
|
7
|
+
appendValueGroup,
|
|
8
|
+
type ArrayLikeApiSchema,
|
|
9
|
+
type BasicAppendValue,
|
|
10
|
+
type CombinedLikeApiSchema,
|
|
11
|
+
DEFAULT_IGNORED_SCHEMA_PROPERTIES,
|
|
12
|
+
getSchemaReference,
|
|
13
|
+
getSourceDisplayName,
|
|
14
|
+
type MaybePromise,
|
|
15
|
+
notNullish,
|
|
16
|
+
type Nullable,
|
|
17
|
+
type ObjectLikeApiSchema,
|
|
18
|
+
resolveAnyOfAndAllOf,
|
|
19
|
+
type StringLikeApiSchema,
|
|
20
|
+
toCasing,
|
|
21
|
+
} from '@goast/core';
|
|
22
|
+
|
|
23
|
+
import { ts } from '../../ast/index.js';
|
|
24
|
+
import type { TypeScriptComponentOutputKind } from '../../common-results.js';
|
|
25
|
+
import { TypeScriptFileBuilder } from '../../file-builder.js';
|
|
26
|
+
import { getSourceDocLine } from '../../utils.js';
|
|
27
|
+
import { TypeScriptFileGenerator } from '../file-generator.js';
|
|
28
|
+
import type { TypeScriptModelGeneratorContext, TypeScriptModelGeneratorOutput } from './models.js';
|
|
29
|
+
|
|
30
|
+
type Context = TypeScriptModelGeneratorContext;
|
|
31
|
+
type Output = TypeScriptModelGeneratorOutput;
|
|
32
|
+
type Builder = TypeScriptFileBuilder;
|
|
33
|
+
|
|
34
|
+
export interface TypeScriptModelGenerator<TOutput extends Output = Output> {
|
|
35
|
+
generate(ctx: Context): MaybePromise<TOutput>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class DefaultTypeScriptModelGenerator extends TypeScriptFileGenerator<Context, Output>
|
|
39
|
+
implements TypeScriptModelGenerator {
|
|
40
|
+
public generate(ctx: Context): MaybePromise<Output> {
|
|
41
|
+
if (this.shouldGenerateTypeDeclaration(ctx, ctx.schema)) {
|
|
42
|
+
const name = this.getDeclarationTypeName(ctx, ctx.schema);
|
|
43
|
+
const filePath = this.getFilePath(ctx, ctx.schema);
|
|
44
|
+
|
|
45
|
+
const fileContent = this.getFileContent(ctx);
|
|
46
|
+
TypeScriptFileBuilder.generate({
|
|
47
|
+
logName: `model ${getSourceDisplayName(ctx.data, ctx.schema)} [${name}]`,
|
|
48
|
+
filePath,
|
|
49
|
+
options: ctx.config,
|
|
50
|
+
generator: (b) => b.append(fileContent),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const exports = fileContent.values.filter(
|
|
54
|
+
(x): x is AppendValue<Builder> & { name: string; export: true } =>
|
|
55
|
+
typeof x === 'object' && x !== null && 'export' in x && x.export === true && 'name' in x,
|
|
56
|
+
);
|
|
57
|
+
const node = exports.find((x) => x.name === name);
|
|
58
|
+
return {
|
|
59
|
+
component: name,
|
|
60
|
+
filePath,
|
|
61
|
+
imports: [
|
|
62
|
+
{
|
|
63
|
+
kind: 'file',
|
|
64
|
+
name,
|
|
65
|
+
modulePath: filePath,
|
|
66
|
+
type: node instanceof ts.Interface || node instanceof ts.TypeAlias ? 'type-import' : 'import',
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
additionalExports: exports
|
|
70
|
+
.filter((x) => x.name !== name)
|
|
71
|
+
.map(
|
|
72
|
+
(x) => ({
|
|
73
|
+
name: x.name,
|
|
74
|
+
type: x instanceof ts.TypeAlias || x instanceof ts.Interface ? 'type-export' : 'export',
|
|
75
|
+
} as const),
|
|
76
|
+
),
|
|
77
|
+
kind: this.getNodeKind(node),
|
|
78
|
+
};
|
|
79
|
+
} else {
|
|
80
|
+
const builder = new TypeScriptFileBuilder(undefined, ctx.config);
|
|
81
|
+
builder.append(this.getType(ctx, ctx.schema));
|
|
82
|
+
const imports = builder.imports.imports;
|
|
83
|
+
builder.imports.clear();
|
|
84
|
+
return { component: builder.toString(false), imports };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected getNodeKind(node: unknown): TypeScriptComponentOutputKind | undefined {
|
|
89
|
+
if (node instanceof ts.Interface) return 'interface';
|
|
90
|
+
if (node instanceof ts.TypeAlias) return 'type';
|
|
91
|
+
if (node instanceof ts.Enum) return 'enum';
|
|
92
|
+
if (node instanceof ts.Class) return 'class';
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
protected getFileContent(ctx: Context): AppendValueGroup<Builder> {
|
|
97
|
+
const content = appendValueGroup<Builder>([], '\n');
|
|
98
|
+
const interfaceSchema = this.resolveInterfaceSchema(ctx);
|
|
99
|
+
if (interfaceSchema) {
|
|
100
|
+
content.values.push(this.getInterface(ctx, interfaceSchema));
|
|
101
|
+
} else if (
|
|
102
|
+
ctx.config.enumGeneration !== 'union' &&
|
|
103
|
+
ctx.schema.kind === 'string' &&
|
|
104
|
+
ctx.schema.enum !== undefined &&
|
|
105
|
+
ctx.schema.enum.length > 0
|
|
106
|
+
) {
|
|
107
|
+
content.values.push(this.getEnum(ctx));
|
|
108
|
+
} else if (ctx.schema.discriminator && Object.keys(ctx.schema.discriminator.mapping).length > 0) {
|
|
109
|
+
content.values.push(
|
|
110
|
+
ts.typeAlias(
|
|
111
|
+
this.getDiscriminatorTypeName(ctx, ctx.schema),
|
|
112
|
+
ts.unionType(Object.keys(ctx.schema.discriminator.mapping).map((x) => ts.string(x))),
|
|
113
|
+
),
|
|
114
|
+
ts.typeAlias(
|
|
115
|
+
this.getBaseTypeName(ctx, ctx.schema),
|
|
116
|
+
this.getType(ctx, ctx.schema, { skipSchemas: true }) ?? this.getAnyType(ctx),
|
|
117
|
+
{ export: true },
|
|
118
|
+
),
|
|
119
|
+
this.getTypeAlias(ctx, ctx.schema),
|
|
120
|
+
);
|
|
121
|
+
} else {
|
|
122
|
+
content.values.push(this.getTypeAlias(ctx, ctx.schema));
|
|
123
|
+
if (ctx.schema.enum) {
|
|
124
|
+
content.values.push(this.getEnumValuesVariable(ctx, ctx.schema));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return content;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected getEnum(ctx: Context): ts.Enum<Builder> {
|
|
131
|
+
return ts.enum(this.getDeclarationTypeName(ctx, ctx.schema), {
|
|
132
|
+
doc: ts.doc({
|
|
133
|
+
description: this.getSchemaDocDescription(ctx, ctx.schema),
|
|
134
|
+
tags: [ctx.schema.deprecated ? ts.docTag('deprecated') : null],
|
|
135
|
+
}),
|
|
136
|
+
export: true,
|
|
137
|
+
members: (ctx.schema.enum?.map((x) => String(x)) ?? []).map((x) =>
|
|
138
|
+
ts.enumValue(toCasing(x, ctx.config.enumValueNameCasing), { value: ts.string(x) })
|
|
139
|
+
),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
protected getInterface(ctx: Context, schema: ObjectLikeApiSchema): ts.Interface<Builder> {
|
|
144
|
+
return ts.interface(this.getDeclarationTypeName(ctx, ctx.schema), {
|
|
145
|
+
export: true,
|
|
146
|
+
doc: ts.doc({
|
|
147
|
+
description: this.getSchemaDocDescription(ctx, schema as ApiSchema),
|
|
148
|
+
tags: [ctx.schema.deprecated ? ts.docTag('deprecated') : null],
|
|
149
|
+
}),
|
|
150
|
+
members: [...this.getProperties(ctx, schema), this.getIndexer(ctx, schema)],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
protected getTypeAlias(ctx: Context, schema: ApiSchema): ts.TypeAlias<Builder> {
|
|
155
|
+
const hasDiscriminator = schema.discriminator && Object.keys(schema.discriminator.mapping).length > 0;
|
|
156
|
+
const result = ts.typeAlias<Builder>(
|
|
157
|
+
this.getDeclarationTypeName(ctx, schema),
|
|
158
|
+
(hasDiscriminator ? this.getDiscriminatorType(ctx, schema) : this.getType(ctx, schema, { skipSchemas: true })) ??
|
|
159
|
+
this.getAnyType(ctx),
|
|
160
|
+
{
|
|
161
|
+
export: true,
|
|
162
|
+
doc: ts.doc({
|
|
163
|
+
description: this.getSchemaDocDescription(ctx, schema),
|
|
164
|
+
tags: [ctx.schema.deprecated ? ts.docTag('deprecated') : null],
|
|
165
|
+
}),
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (hasDiscriminator) {
|
|
170
|
+
const propertyValue = this.getDiscriminatorTypeName(ctx, ctx.schema);
|
|
171
|
+
result.generics.push(
|
|
172
|
+
ts.genericParameter(this.getDiscriminatorGenericParamName(ctx, schema), {
|
|
173
|
+
constraint: propertyValue,
|
|
174
|
+
default: propertyValue,
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const inheritedSchemas = this.getInheritedSchemas(ctx, schema);
|
|
180
|
+
if (inheritedSchemas.length > 0) {
|
|
181
|
+
const fixedProperties = inheritedSchemas.reduce((p, c) => {
|
|
182
|
+
const mappingValues = Object.entries(c.discriminator.mapping)
|
|
183
|
+
.filter(([_, value]) => value.id === schema.id)
|
|
184
|
+
.map(([key]) => ts.string(key));
|
|
185
|
+
if (c.discriminator.propertyName in p) {
|
|
186
|
+
p[c.discriminator.propertyName].types.push(...mappingValues);
|
|
187
|
+
} else {
|
|
188
|
+
p[c.discriminator.propertyName] = ts.unionType(mappingValues);
|
|
189
|
+
}
|
|
190
|
+
return p;
|
|
191
|
+
}, {} as Record<string, ts.UnionType<Builder>>);
|
|
192
|
+
result.type = ts.intersectionType([
|
|
193
|
+
ts.refs.omit([result.type, ts.unionType(Object.keys(fixedProperties).map((x) => ts.string(x)))]),
|
|
194
|
+
ts.objectType({
|
|
195
|
+
members: Object.entries(fixedProperties).map(([key, value]) =>
|
|
196
|
+
ts.property(key, { type: value, readonly: ctx.config.immutableTypes })
|
|
197
|
+
),
|
|
198
|
+
}),
|
|
199
|
+
]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
protected getEnumValuesVariable(ctx: Context, schema: ApiSchema): ts.Variable<Builder> {
|
|
206
|
+
const enumTypeName = this.getDeclarationTypeName(ctx, schema);
|
|
207
|
+
const values = schema.enum?.map((x) => ts.toNode(x)) ?? [];
|
|
208
|
+
return ts.variable(toCasing(`${enumTypeName}-Values`, ctx.config.constantNameCasing), {
|
|
209
|
+
doc: ts.doc({
|
|
210
|
+
description: `All possible values of the enum \`${enumTypeName}\`.`,
|
|
211
|
+
}),
|
|
212
|
+
export: true,
|
|
213
|
+
readonly: true,
|
|
214
|
+
value: ts.tuple(values, { asConst: true }),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
protected getIndexer(ctx: Context, schema: ObjectLikeApiSchema): ts.Indexer<Builder> | null {
|
|
219
|
+
return schema.additionalProperties
|
|
220
|
+
? ts.indexer(
|
|
221
|
+
ts.refs.string(),
|
|
222
|
+
(schema.additionalProperties === true ? null : this.getMemberType(ctx, schema.additionalProperties)) ??
|
|
223
|
+
this.getAnyType(ctx),
|
|
224
|
+
{ readonly: ctx.config.immutableTypes },
|
|
225
|
+
)
|
|
226
|
+
: null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
protected getProperties(ctx: Context, schema: ObjectLikeApiSchema): ts.Property<Builder>[] {
|
|
230
|
+
return Array.from(schema.properties.values()).map((property) => {
|
|
231
|
+
return ts.property(property.name, {
|
|
232
|
+
doc: ts.doc({
|
|
233
|
+
description: this.getSchemaDocDescription(ctx, property.schema),
|
|
234
|
+
tags: [property.schema.deprecated ? ts.docTag('deprecated') : null],
|
|
235
|
+
}),
|
|
236
|
+
readonly: ctx.config.immutableTypes,
|
|
237
|
+
optional: !schema.required.has(property.name),
|
|
238
|
+
type: ts.unionType([this.getType(ctx, property.schema), property.schema.nullable ? 'null' : null]),
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The type of a schema used as a *member* of an enclosing type - a composition branch, an array element, an
|
|
245
|
+
* `additionalProperties` value - with the schema's own nullability applied.
|
|
246
|
+
*
|
|
247
|
+
* `getType` renders a schema's type without its `null` on purpose: the two contexts that render a schema's type
|
|
248
|
+
* as a whole declare nullability themselves. A property has it added by `getProperties`, and a top-level type
|
|
249
|
+
* alias deliberately does not hoist it, because that would change what every declared type means for consumers.
|
|
250
|
+
* A member position has neither - nothing downstream can add the `null` back - so unless it is admitted here the
|
|
251
|
+
* emitted type cannot represent a value the spec explicitly permits.
|
|
252
|
+
*/
|
|
253
|
+
protected getMemberType(
|
|
254
|
+
ctx: Context,
|
|
255
|
+
schema: Nullable<ApiSchema>,
|
|
256
|
+
options?: { skipSchemas?: boolean; useBaseType?: boolean },
|
|
257
|
+
): ts.Type<Builder> | null {
|
|
258
|
+
const type = this.getType(ctx, schema, options);
|
|
259
|
+
return type !== null && schema && this.isNullableMember(schema) ? ts.unionType([type, 'null']) : type;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Whether a schema needs a `null` added to the type `getType` renders for it. A schema that only ever renders as
|
|
264
|
+
* `null` (the null type itself) already admits it, and adding a second one would emit `(null) | (null)`.
|
|
265
|
+
*
|
|
266
|
+
* Nullability is read through the reference chain the same way `getType` reads the type, so a `$ref` to a
|
|
267
|
+
* declared nullable schema - whose own declaration does not hoist the `null` - still admits null where it is used.
|
|
268
|
+
*/
|
|
269
|
+
protected isNullableMember(schema: ApiSchema): boolean {
|
|
270
|
+
const resolved = getSchemaReference(schema, DEFAULT_IGNORED_SCHEMA_PROPERTIES);
|
|
271
|
+
if (resolved.kind === 'null') return false;
|
|
272
|
+
return schema.nullable === true || resolved.nullable === true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
protected getType(
|
|
276
|
+
ctx: Context,
|
|
277
|
+
schema: Nullable<ApiSchema>,
|
|
278
|
+
options?: { skipSchemas?: boolean; useBaseType?: boolean },
|
|
279
|
+
): ts.Type<Builder> | null {
|
|
280
|
+
if (!schema) return this.getAnyType(ctx);
|
|
281
|
+
|
|
282
|
+
schema = getSchemaReference(schema, DEFAULT_IGNORED_SCHEMA_PROPERTIES);
|
|
283
|
+
|
|
284
|
+
if (options?.useBaseType && ctx.schema.inheritedSchemas.some((x) => x.id === schema.id)) {
|
|
285
|
+
return ts.reference(this.getBaseTypeName(ctx, schema), this.getFilePath(ctx, schema), {
|
|
286
|
+
importType: 'type-import',
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!options?.skipSchemas && this.shouldGenerateTypeDeclaration(ctx, schema)) {
|
|
291
|
+
if (schema.id === ctx.schema.id) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return ts.reference(this.getDeclarationTypeName(ctx, schema), this.getFilePath(ctx, schema), {
|
|
295
|
+
importType: 'type-import',
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (schema.enum) {
|
|
300
|
+
return ts.unionType(schema.enum.map((x) => ts.toNode(x)));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
switch (schema.kind) {
|
|
304
|
+
case 'boolean':
|
|
305
|
+
return 'boolean';
|
|
306
|
+
case 'integer':
|
|
307
|
+
case 'number':
|
|
308
|
+
return 'number';
|
|
309
|
+
case 'string':
|
|
310
|
+
return this.getStringType(ctx, schema);
|
|
311
|
+
case 'null':
|
|
312
|
+
return 'null';
|
|
313
|
+
case 'unknown':
|
|
314
|
+
return this.getAnyType(ctx);
|
|
315
|
+
case 'array':
|
|
316
|
+
return this.getArrayType(ctx, schema);
|
|
317
|
+
case 'object':
|
|
318
|
+
return this.getObjectType(ctx, schema);
|
|
319
|
+
case 'combined':
|
|
320
|
+
return this.getCombinedType(ctx, schema);
|
|
321
|
+
case 'multi-type':
|
|
322
|
+
return this.getMultiType(ctx, schema);
|
|
323
|
+
case 'oneOf':
|
|
324
|
+
return this.getOneOfType(ctx, schema);
|
|
325
|
+
default:
|
|
326
|
+
return this.getAnyType(ctx);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
protected getStringType(ctx: Context, schema: StringLikeApiSchema): ts.Type<Builder> {
|
|
331
|
+
if (schema.format === 'binary') {
|
|
332
|
+
return ctx.config.binarySchemaType();
|
|
333
|
+
}
|
|
334
|
+
return 'string';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
protected getArrayType(ctx: Context, schema: ArrayLikeApiSchema): ts.Type<Builder> {
|
|
338
|
+
return ts.arrayType(this.getMemberType(ctx, schema.items) ?? this.getAnyType(ctx), {
|
|
339
|
+
readonly: ctx.config.immutableTypes,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
protected getObjectType(ctx: Context, schema: ObjectLikeApiSchema): ts.Type<Builder> {
|
|
344
|
+
const parts = [
|
|
345
|
+
schema.properties.size > 0 ? ts.objectType({ members: this.getProperties(ctx, schema) }) : null,
|
|
346
|
+
schema.additionalProperties ? ts.objectType({ members: [this.getIndexer(ctx, schema)] }) : null,
|
|
347
|
+
schema.allOf.length > 0 || schema.anyOf.length > 0 ? this.getCombinedType(ctx, schema) : null,
|
|
348
|
+
].filter(notNullish);
|
|
349
|
+
if (parts.length === 0) {
|
|
350
|
+
parts.push(
|
|
351
|
+
ts.objectType({
|
|
352
|
+
members: [ts.indexer(ts.refs.string(), ts.refs.never(), { readonly: ctx.config.immutableTypes })],
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return ts.intersectionType(parts);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
protected getDiscriminatorType(ctx: Context, schema: ApiSchema): ts.Type<Builder> {
|
|
360
|
+
if (schema.discriminator && Object.keys(schema.discriminator.mapping).length > 0) {
|
|
361
|
+
const discriminator = schema.discriminator;
|
|
362
|
+
return ts.intersectionType([
|
|
363
|
+
this.getBaseTypeName(ctx, schema as ApiSchema),
|
|
364
|
+
ts.lookupType(
|
|
365
|
+
ts.objectType({
|
|
366
|
+
members: Object.entries(discriminator.mapping).map(([key, value]) =>
|
|
367
|
+
ts.property(key, {
|
|
368
|
+
type: ts.intersectionType([
|
|
369
|
+
ts.objectType({
|
|
370
|
+
members: [
|
|
371
|
+
ts.property(discriminator.propertyName, {
|
|
372
|
+
type: ts.string(key),
|
|
373
|
+
readonly: ctx.config.immutableTypes,
|
|
374
|
+
}),
|
|
375
|
+
],
|
|
376
|
+
}),
|
|
377
|
+
this.getType(ctx, value),
|
|
378
|
+
]),
|
|
379
|
+
})
|
|
380
|
+
),
|
|
381
|
+
}),
|
|
382
|
+
this.getDiscriminatorGenericParamName(ctx, schema as ApiSchema),
|
|
383
|
+
),
|
|
384
|
+
]);
|
|
385
|
+
} else {
|
|
386
|
+
return ts.refs.unknown();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
protected getCombinedType(ctx: Context, schema: CombinedLikeApiSchema): ts.Type<Builder> {
|
|
391
|
+
if (schema.allOf.length === 0 && schema.anyOf.length === 0) {
|
|
392
|
+
return this.getAnyType(ctx);
|
|
393
|
+
}
|
|
394
|
+
const useBaseType = schema.id === ctx.schema.id;
|
|
395
|
+
// allOf branches must all hold at once, i.e. an intersection. anyOf branches only require at least one to hold,
|
|
396
|
+
// i.e. a union - TypeScript unions already admit values matching several branches, so no Partial<> wrapper is
|
|
397
|
+
// needed to express that overlap. A schema with both intersects the allOf branches with the anyOf union.
|
|
398
|
+
return ts.intersectionType([
|
|
399
|
+
...schema.allOf.map((x) => this.getMemberType(ctx, x, { useBaseType })),
|
|
400
|
+
schema.anyOf.length > 0
|
|
401
|
+
? ts.unionType(schema.anyOf.map((x) => this.getMemberType(ctx, x, { useBaseType })))
|
|
402
|
+
: null,
|
|
403
|
+
]);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
protected getOneOfType(ctx: Context, schema: ApiSchema<'oneOf'>): ts.Type<Builder> {
|
|
407
|
+
if (schema.oneOf.length === 0) {
|
|
408
|
+
return this.getAnyType(ctx);
|
|
409
|
+
}
|
|
410
|
+
return ts.unionType(schema.oneOf.map((x) => this.getMemberType(ctx, x)));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
protected getMultiType(ctx: Context, schema: ApiSchema<'multi-type'>): ts.Type<Builder> {
|
|
414
|
+
if (schema.type.length === 0) {
|
|
415
|
+
return this.getAnyType(ctx);
|
|
416
|
+
}
|
|
417
|
+
return ts.unionType(
|
|
418
|
+
schema.type.map((x) => {
|
|
419
|
+
switch (x) {
|
|
420
|
+
case 'string':
|
|
421
|
+
return this.getStringType(ctx, schema);
|
|
422
|
+
case 'number':
|
|
423
|
+
case 'integer':
|
|
424
|
+
return 'number';
|
|
425
|
+
case 'boolean':
|
|
426
|
+
return 'boolean';
|
|
427
|
+
case 'null':
|
|
428
|
+
return 'null';
|
|
429
|
+
case 'array':
|
|
430
|
+
return this.getArrayType(ctx, schema);
|
|
431
|
+
case 'object':
|
|
432
|
+
return this.getObjectType(ctx, schema);
|
|
433
|
+
default:
|
|
434
|
+
return 'never';
|
|
435
|
+
}
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
protected getSchemaDocDescription(ctx: Context, schema: ApiSchema): BasicAppendValue<Builder> {
|
|
441
|
+
const docSegments = [schema.description?.trim()];
|
|
442
|
+
|
|
443
|
+
if (ctx.config.includeSourceInDocs) {
|
|
444
|
+
docSegments.push(getSourceDocLine(schema));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return docSegments.filter(notNullish).join('\n\n');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
protected shouldGenerateTypeDeclaration(ctx: Context, schema: ApiSchema): boolean {
|
|
451
|
+
// All named schemas should have its own type declaration
|
|
452
|
+
if (!schema.isNameGenerated) {
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// All enum types should have its own type declaration
|
|
457
|
+
if (schema.enum !== undefined && schema.enum.length > 0) {
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// All primitive types already exist and do not need its own type declaration
|
|
462
|
+
if (
|
|
463
|
+
schema.kind !== 'array' &&
|
|
464
|
+
schema.kind !== 'combined' &&
|
|
465
|
+
schema.kind !== 'multi-type' &&
|
|
466
|
+
schema.kind !== 'object' &&
|
|
467
|
+
schema.kind !== 'oneOf'
|
|
468
|
+
) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Nullable by oneOf schemas do not need their own type declaration
|
|
473
|
+
if (schema.kind === 'oneOf' && schema.oneOf?.length === 2 && schema.oneOf.some((x) => x.kind === 'null')) {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Dynamically generated schemas do not have its own type declaration
|
|
478
|
+
if (!ctx.data.schemas.some((x) => x.id === schema.id)) {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Check if "real" schema
|
|
483
|
+
if (getSchemaReference(ctx.schema, DEFAULT_IGNORED_SCHEMA_PROPERTIES) !== ctx.schema) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// For all other types, check if the user has enabled inline unnamed schemas
|
|
488
|
+
return !ctx.config.inlineUnnamedSchemas;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
protected resolveInterfaceSchema(ctx: Context): ApiSchema<'object'> | undefined {
|
|
492
|
+
if (
|
|
493
|
+
ctx.config.typeDeclaration === 'prefer-interface' &&
|
|
494
|
+
(ctx.schema.kind === 'object' || ctx.schema.kind === 'combined')
|
|
495
|
+
) {
|
|
496
|
+
return resolveAnyOfAndAllOf(ctx.schema, true);
|
|
497
|
+
}
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
protected getDeclarationTypeName(ctx: Context, schema: ApiSchema): string {
|
|
502
|
+
return toCasing(schema.name, ctx.config.typeNameCasing);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
protected getDiscriminatorGenericParamName(ctx: Context, schema: ApiSchema): string {
|
|
506
|
+
return toCasing(schema.discriminator?.propertyName, ctx.config.genericParamNameCasing);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
protected getDiscriminatorTypeName(ctx: Context, schema: ApiSchema): string {
|
|
510
|
+
return toCasing(schema.name + '_Discriminator', ctx.config.typeNameCasing);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
protected getBaseTypeName(ctx: Context, schema: ApiSchema): string {
|
|
514
|
+
return toCasing(schema.name, ctx.config.discriminatorBaseTypeCasing);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
protected getInheritedSchemas(ctx: Context, schema: ApiSchema): ApiSchema['inheritedSchemas'] {
|
|
518
|
+
return schema.inheritedSchemas
|
|
519
|
+
.filter((schema) => this.shouldGenerateTypeDeclaration(ctx, schema) && !schema.isNameGenerated)
|
|
520
|
+
.filter((item, index, self) => self.indexOf(item) === index);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
protected getFilePath(ctx: Context, schema: ApiSchema): string {
|
|
524
|
+
return resolve(
|
|
525
|
+
ctx.config.outputDir,
|
|
526
|
+
ctx.config.modelsDir,
|
|
527
|
+
`${toCasing(schema.name, ctx.config.modelFileNameCasing ?? ctx.config.fileNameCasing)}.ts`,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type ApiSchema,
|
|
5
|
+
type AppendValueGroup,
|
|
6
|
+
appendValueGroup,
|
|
7
|
+
Factory,
|
|
8
|
+
type MaybePromise,
|
|
9
|
+
type OpenApiGeneratorContext,
|
|
10
|
+
OpenApiSchemasGenerationProviderBase,
|
|
11
|
+
} from '@goast/core';
|
|
12
|
+
|
|
13
|
+
import { ts } from '../../ast/index.js';
|
|
14
|
+
import { TypeScriptFileBuilder } from '../../file-builder.js';
|
|
15
|
+
import { DefaultTypeScriptModelGenerator, type TypeScriptModelGenerator } from './model-generator.js';
|
|
16
|
+
import {
|
|
17
|
+
defaultTypeScriptModelsGeneratorConfig,
|
|
18
|
+
type TypeScriptModelGeneratorOutput,
|
|
19
|
+
type TypeScriptModelsGeneratorConfig,
|
|
20
|
+
type TypeScriptModelsGeneratorContext,
|
|
21
|
+
type TypeScriptModelsGeneratorInput,
|
|
22
|
+
type TypeScriptModelsGeneratorOutput,
|
|
23
|
+
} from './models.js';
|
|
24
|
+
|
|
25
|
+
type Input = TypeScriptModelsGeneratorInput;
|
|
26
|
+
type Output = TypeScriptModelsGeneratorOutput;
|
|
27
|
+
type Config = TypeScriptModelsGeneratorConfig;
|
|
28
|
+
type SchemaOutput = TypeScriptModelGeneratorOutput;
|
|
29
|
+
type Context = TypeScriptModelsGeneratorContext;
|
|
30
|
+
|
|
31
|
+
export class TypeScriptModelsGenerator extends OpenApiSchemasGenerationProviderBase<
|
|
32
|
+
Input,
|
|
33
|
+
Output,
|
|
34
|
+
Config,
|
|
35
|
+
SchemaOutput,
|
|
36
|
+
Context
|
|
37
|
+
> {
|
|
38
|
+
private readonly _modelGeneratorFactory: Factory<TypeScriptModelGenerator, []>;
|
|
39
|
+
|
|
40
|
+
constructor(modelGeneratorFactory?: Factory<TypeScriptModelGenerator, []>) {
|
|
41
|
+
super();
|
|
42
|
+
this._modelGeneratorFactory = modelGeneratorFactory ?? Factory.fromValue(new DefaultTypeScriptModelGenerator());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected override initResult(): Output {
|
|
46
|
+
return {
|
|
47
|
+
typescript: {
|
|
48
|
+
models: {},
|
|
49
|
+
indexFiles: { models: undefined },
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected override buildContext(
|
|
55
|
+
context: OpenApiGeneratorContext<TypeScriptModelsGeneratorInput>,
|
|
56
|
+
config?: Partial<Config> | undefined,
|
|
57
|
+
): Context {
|
|
58
|
+
return this.getProviderContext(context, config, defaultTypeScriptModelsGeneratorConfig);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
protected override generateAdditionalFiles(ctx: TypeScriptModelsGeneratorContext): MaybePromise<void> {
|
|
62
|
+
ctx.output.typescript.indexFiles.models = this.generateIndexFile(ctx);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
protected override generateSchema(ctx: Context, schema: ApiSchema): MaybePromise<SchemaOutput> {
|
|
66
|
+
const modelGenerator = this._modelGeneratorFactory.create();
|
|
67
|
+
return modelGenerator.generate({
|
|
68
|
+
...ctx,
|
|
69
|
+
schema,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected override addSchemaResult(ctx: Context, schema: ApiSchema, result: SchemaOutput): void {
|
|
74
|
+
ctx.output.typescript.models[schema.id] = result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
protected generateIndexFile(ctx: Context): string | null {
|
|
78
|
+
const filePath = this.getIndexFilePath(ctx);
|
|
79
|
+
TypeScriptFileBuilder.tryGenerate({
|
|
80
|
+
logName: 'model index file',
|
|
81
|
+
filePath,
|
|
82
|
+
options: ctx.config,
|
|
83
|
+
generator: (b) => b.append(this.getIndexFileContent(ctx)),
|
|
84
|
+
});
|
|
85
|
+
return filePath;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected getIndexFilePath(ctx: Context): string | null {
|
|
89
|
+
return ctx.config.modelsIndexFile ? resolve(ctx.config.outputDir, ctx.config.modelsIndexFile) : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected getIndexFileContent(ctx: Context): AppendValueGroup<TypeScriptFileBuilder> {
|
|
93
|
+
return appendValueGroup(
|
|
94
|
+
Object.values(ctx.output.typescript.models).flatMap((x) => {
|
|
95
|
+
const { filePath } = x;
|
|
96
|
+
if (!filePath) return [];
|
|
97
|
+
return [
|
|
98
|
+
ts.export(x.component, filePath, {
|
|
99
|
+
kind: x.kind === 'type' || x.kind === 'interface' ? 'type-export' : 'export',
|
|
100
|
+
}),
|
|
101
|
+
...(x.additionalExports?.map((e) =>
|
|
102
|
+
typeof e === 'string' ? ts.export(e, filePath) : ts.export(e.name, filePath, { kind: e.type })
|
|
103
|
+
) ?? []),
|
|
104
|
+
];
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|