ng-openapi 0.0.2
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 -0
- package/README.md +188 -0
- package/cli.cjs +1827 -0
- package/index.d.ts +169 -0
- package/index.js +1807 -0
- package/package.json +86 -0
package/cli.cjs
ADDED
|
@@ -0,0 +1,1827 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
+
mod
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
// src/lib/cli.ts
|
|
28
|
+
var import_commander = require("commander");
|
|
29
|
+
var path6 = __toESM(require("path"));
|
|
30
|
+
var fs4 = __toESM(require("fs"));
|
|
31
|
+
|
|
32
|
+
// src/lib/core/swagger-parser.ts
|
|
33
|
+
var fs = __toESM(require("fs"));
|
|
34
|
+
var SwaggerParser = class {
|
|
35
|
+
static {
|
|
36
|
+
__name(this, "SwaggerParser");
|
|
37
|
+
}
|
|
38
|
+
spec;
|
|
39
|
+
constructor(swaggerPath) {
|
|
40
|
+
const swaggerContent = fs.readFileSync(swaggerPath, "utf8");
|
|
41
|
+
this.spec = JSON.parse(swaggerContent);
|
|
42
|
+
}
|
|
43
|
+
getDefinitions() {
|
|
44
|
+
return this.spec.definitions || this.spec.components?.schemas || {};
|
|
45
|
+
}
|
|
46
|
+
getDefinition(name) {
|
|
47
|
+
const definitions = this.getDefinitions();
|
|
48
|
+
return definitions[name];
|
|
49
|
+
}
|
|
50
|
+
resolveReference(ref) {
|
|
51
|
+
const parts = ref.split("/");
|
|
52
|
+
const definitionName = parts[parts.length - 1];
|
|
53
|
+
return this.getDefinition(definitionName);
|
|
54
|
+
}
|
|
55
|
+
getAllDefinitionNames() {
|
|
56
|
+
return Object.keys(this.getDefinitions());
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/lib/core/generator.ts
|
|
61
|
+
var import_ts_morph4 = require("ts-morph");
|
|
62
|
+
|
|
63
|
+
// src/lib/generators/type/type.generator.ts
|
|
64
|
+
var import_ts_morph = require("ts-morph");
|
|
65
|
+
|
|
66
|
+
// src/lib/config/constants.ts
|
|
67
|
+
var disableLinting = `/* @ts-nocheck */
|
|
68
|
+
/* eslint-disable */
|
|
69
|
+
/* @noformat */
|
|
70
|
+
/* @formatter:off */
|
|
71
|
+
`;
|
|
72
|
+
var authorComment = `/**
|
|
73
|
+
* Generated by ng-openapi
|
|
74
|
+
`;
|
|
75
|
+
var defaultHeaderComment = disableLinting + authorComment;
|
|
76
|
+
var TYPE_GENERATOR_HEADER_COMMENT = defaultHeaderComment + `* Generated TypeScript interfaces from Swagger specification
|
|
77
|
+
* Do not edit this file manually
|
|
78
|
+
*/
|
|
79
|
+
`;
|
|
80
|
+
var SERVICE_INDEX_GENERATOR_HEADER_COMMENT = defaultHeaderComment + `* Generated service exports
|
|
81
|
+
* Do not edit this file manually
|
|
82
|
+
*/
|
|
83
|
+
`;
|
|
84
|
+
var SERVICE_GENERATOR_HEADER_COMMENT = /* @__PURE__ */ __name((controllerName) => defaultHeaderComment + `* Generated Angular service for ${controllerName} controller
|
|
85
|
+
* Do not edit this file manually
|
|
86
|
+
*/
|
|
87
|
+
`, "SERVICE_GENERATOR_HEADER_COMMENT");
|
|
88
|
+
|
|
89
|
+
// src/lib/generators/type/type.generator.ts
|
|
90
|
+
var TypeGenerator = class {
|
|
91
|
+
static {
|
|
92
|
+
__name(this, "TypeGenerator");
|
|
93
|
+
}
|
|
94
|
+
project;
|
|
95
|
+
parser;
|
|
96
|
+
sourceFile;
|
|
97
|
+
generatedTypes = /* @__PURE__ */ new Set();
|
|
98
|
+
config;
|
|
99
|
+
constructor(swaggerPath, outputRoot, config) {
|
|
100
|
+
this.config = config;
|
|
101
|
+
const outputPath = outputRoot + "/models/index.ts";
|
|
102
|
+
this.project = new import_ts_morph.Project({
|
|
103
|
+
compilerOptions: {
|
|
104
|
+
declaration: true,
|
|
105
|
+
target: import_ts_morph.ScriptTarget.ES2022,
|
|
106
|
+
module: import_ts_morph.ModuleKind.Preserve,
|
|
107
|
+
strict: true,
|
|
108
|
+
...this.config.compilerOptions
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
try {
|
|
112
|
+
this.parser = new SwaggerParser(swaggerPath);
|
|
113
|
+
this.sourceFile = this.project.createSourceFile(outputPath, "", {
|
|
114
|
+
overwrite: true
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error("Error initializing TypeGenerator:", error);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
generate() {
|
|
122
|
+
try {
|
|
123
|
+
const definitions = this.parser.getDefinitions();
|
|
124
|
+
if (!definitions || Object.keys(definitions).length === 0) {
|
|
125
|
+
console.warn("No definitions found in swagger file");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.sourceFile.insertText(0, TYPE_GENERATOR_HEADER_COMMENT);
|
|
129
|
+
Object.entries(definitions).forEach(([name, definition]) => {
|
|
130
|
+
this.generateInterface(name, definition);
|
|
131
|
+
});
|
|
132
|
+
this.sourceFile.saveSync();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error("Error in generate():", error);
|
|
135
|
+
throw new Error(`Failed to generate types: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
generateInterface(name, definition) {
|
|
139
|
+
const interfaceName = this.pascalCaseForEnums(name);
|
|
140
|
+
if (this.generatedTypes.has(interfaceName)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.generatedTypes.add(interfaceName);
|
|
144
|
+
if (definition.enum) {
|
|
145
|
+
this.generateEnum(interfaceName, definition);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (definition.allOf) {
|
|
149
|
+
this.generateCompositeType(interfaceName, definition);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const interfaceDeclaration = this.sourceFile.addInterface({
|
|
153
|
+
name: interfaceName,
|
|
154
|
+
isExported: true,
|
|
155
|
+
docs: definition.description ? [
|
|
156
|
+
definition.description
|
|
157
|
+
] : void 0
|
|
158
|
+
});
|
|
159
|
+
this.addInterfaceProperties(interfaceDeclaration, definition);
|
|
160
|
+
}
|
|
161
|
+
generateEnum(name, definition) {
|
|
162
|
+
if (!definition.enum?.length) return;
|
|
163
|
+
const isStringEnum = definition.enum.some((value) => typeof value === "string");
|
|
164
|
+
if (isStringEnum) {
|
|
165
|
+
const unionType = definition.enum.map((value) => typeof value === "string" ? `'${this.escapeString(value)}'` : String(value)).join(" | ");
|
|
166
|
+
this.sourceFile.addTypeAlias({
|
|
167
|
+
name,
|
|
168
|
+
type: unionType,
|
|
169
|
+
isExported: true,
|
|
170
|
+
docs: definition.description ? [
|
|
171
|
+
definition.description
|
|
172
|
+
] : void 0
|
|
173
|
+
});
|
|
174
|
+
} else if (definition.description && this.config.options.generateEnumBasedOnDescription) {
|
|
175
|
+
const enumDeclaration = this.sourceFile.addEnum({
|
|
176
|
+
name,
|
|
177
|
+
isExported: true
|
|
178
|
+
});
|
|
179
|
+
try {
|
|
180
|
+
const enumValueObjects = JSON.parse(definition.description);
|
|
181
|
+
enumValueObjects.forEach((enumValueObject) => {
|
|
182
|
+
enumDeclaration.addMember({
|
|
183
|
+
name: enumValueObject.Name,
|
|
184
|
+
value: enumValueObject.Value
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
} catch (e) {
|
|
188
|
+
console.error(`Failed to parse enum description for ${name}`);
|
|
189
|
+
definition.enum.forEach((value) => {
|
|
190
|
+
const enumKey = this.toEnumKey(value);
|
|
191
|
+
enumDeclaration.addMember({
|
|
192
|
+
name: enumKey,
|
|
193
|
+
value
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
const enumDeclaration = this.sourceFile.addEnum({
|
|
199
|
+
name,
|
|
200
|
+
isExported: true,
|
|
201
|
+
docs: definition.description ? [
|
|
202
|
+
definition.description
|
|
203
|
+
] : void 0
|
|
204
|
+
});
|
|
205
|
+
definition.enum.forEach((value) => {
|
|
206
|
+
const enumKey = this.toEnumKey(value);
|
|
207
|
+
enumDeclaration.addMember({
|
|
208
|
+
name: enumKey,
|
|
209
|
+
value
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
generateCompositeType(name, definition) {
|
|
215
|
+
let typeExpression = "";
|
|
216
|
+
if (definition.allOf) {
|
|
217
|
+
const types = definition.allOf.map((def) => this.resolveSwaggerType(def)).filter((type) => type !== "any" && type !== "unknown");
|
|
218
|
+
typeExpression = types.length > 0 ? types.join(" & ") : "Record<string, unknown>";
|
|
219
|
+
}
|
|
220
|
+
this.sourceFile.addTypeAlias({
|
|
221
|
+
name,
|
|
222
|
+
type: typeExpression,
|
|
223
|
+
isExported: true,
|
|
224
|
+
docs: definition.description ? [
|
|
225
|
+
definition.description
|
|
226
|
+
] : void 0
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
addInterfaceProperties(interfaceDeclaration, definition) {
|
|
230
|
+
if (!definition.properties && definition.additionalProperties === false) {
|
|
231
|
+
interfaceDeclaration.addIndexSignature({
|
|
232
|
+
keyName: "key",
|
|
233
|
+
keyType: "string",
|
|
234
|
+
returnType: "never"
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (!definition.properties && definition.additionalProperties === true) {
|
|
239
|
+
interfaceDeclaration.addIndexSignature({
|
|
240
|
+
keyName: "key",
|
|
241
|
+
keyType: "string",
|
|
242
|
+
returnType: "any"
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (!definition.properties) {
|
|
247
|
+
console.warn(`No properties found for interface ${interfaceDeclaration.getName()}`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
Object.entries(definition.properties).forEach(([propertyName, property]) => {
|
|
251
|
+
const isRequired = definition.required?.includes(propertyName) ?? false;
|
|
252
|
+
const isReadOnly = property.readOnly;
|
|
253
|
+
const propertyType = this.resolveSwaggerType(property);
|
|
254
|
+
const sanitizedName = this.sanitizePropertyName(propertyName);
|
|
255
|
+
interfaceDeclaration.addProperty({
|
|
256
|
+
name: sanitizedName,
|
|
257
|
+
type: propertyType,
|
|
258
|
+
isReadonly: isReadOnly,
|
|
259
|
+
hasQuestionToken: !isRequired,
|
|
260
|
+
docs: property.description ? [
|
|
261
|
+
property.description
|
|
262
|
+
] : void 0
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
resolveSwaggerType(schema) {
|
|
267
|
+
if (schema.$ref) {
|
|
268
|
+
return this.resolveReference(schema.$ref);
|
|
269
|
+
}
|
|
270
|
+
if (schema.enum) {
|
|
271
|
+
return schema.enum.map((value) => typeof value === "string" ? `'${this.escapeString(value)}'` : String(value)).join(" | ");
|
|
272
|
+
}
|
|
273
|
+
if (schema.allOf) {
|
|
274
|
+
return schema.allOf.map((def) => this.resolveSwaggerType(def)).filter((type) => type !== "any" && type !== "unknown").join(" & ") || "Record";
|
|
275
|
+
}
|
|
276
|
+
if (schema.oneOf) {
|
|
277
|
+
return schema.oneOf.map((def) => this.resolveSwaggerType(def)).filter((type) => type !== "any" && type !== "unknown").join(" | ") || "unknown";
|
|
278
|
+
}
|
|
279
|
+
if (schema.anyOf) {
|
|
280
|
+
return schema.anyOf.map((def) => this.resolveSwaggerType(def)).filter((type) => type !== "any" && type !== "unknown").join(" | ") || "unknown";
|
|
281
|
+
}
|
|
282
|
+
if (schema.type === "array") {
|
|
283
|
+
const itemType = schema.items ? this.getArrayItemType(schema.items) : "unknown";
|
|
284
|
+
return `${itemType}[]`;
|
|
285
|
+
}
|
|
286
|
+
if (schema.type === "object") {
|
|
287
|
+
if (schema.properties) {
|
|
288
|
+
return this.generateInlineObjectType(schema);
|
|
289
|
+
}
|
|
290
|
+
if (schema.additionalProperties) {
|
|
291
|
+
const valueType = typeof schema.additionalProperties === "object" ? this.resolveSwaggerType(schema.additionalProperties) : "unknown";
|
|
292
|
+
return `Record<string, ${valueType}>`;
|
|
293
|
+
}
|
|
294
|
+
return "Record<string, unknown>";
|
|
295
|
+
}
|
|
296
|
+
return this.mapSwaggerTypeToTypeScript(schema.type, schema.format, schema.nullable);
|
|
297
|
+
}
|
|
298
|
+
generateInlineObjectType(definition) {
|
|
299
|
+
if (!definition.properties) {
|
|
300
|
+
if (definition.additionalProperties) {
|
|
301
|
+
const additionalType = typeof definition.additionalProperties === "object" ? this.resolveSwaggerType(definition.additionalProperties) : "unknown";
|
|
302
|
+
return `Record<string, ${additionalType}>`;
|
|
303
|
+
}
|
|
304
|
+
return "Record<string, unknown>";
|
|
305
|
+
}
|
|
306
|
+
const properties = Object.entries(definition.properties).map(([key, prop]) => {
|
|
307
|
+
const isRequired = definition.required?.includes(key) ?? false;
|
|
308
|
+
const questionMark = isRequired ? "" : "?";
|
|
309
|
+
const sanitizedKey = this.sanitizePropertyName(key);
|
|
310
|
+
return `${sanitizedKey}${questionMark}: ${this.resolveSwaggerType(prop)}`;
|
|
311
|
+
}).join("; ");
|
|
312
|
+
return `{ ${properties} }`;
|
|
313
|
+
}
|
|
314
|
+
resolveReference(ref) {
|
|
315
|
+
try {
|
|
316
|
+
const refDefinition = this.parser.resolveReference(ref);
|
|
317
|
+
const refName = ref.split("/").pop();
|
|
318
|
+
if (!refName) {
|
|
319
|
+
console.warn(`Invalid reference format: ${ref}`);
|
|
320
|
+
return "unknown";
|
|
321
|
+
}
|
|
322
|
+
const typeName = this.pascalCaseForEnums(refName);
|
|
323
|
+
if (refDefinition && !this.generatedTypes.has(typeName)) {
|
|
324
|
+
this.generateInterface(refName, refDefinition);
|
|
325
|
+
}
|
|
326
|
+
return typeName;
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.warn(`Failed to resolve reference ${ref}:`, error);
|
|
329
|
+
return "unknown";
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
mapSwaggerTypeToTypeScript(type, format, isNullable) {
|
|
333
|
+
if (!type) return "unknown";
|
|
334
|
+
switch (type) {
|
|
335
|
+
case "string":
|
|
336
|
+
if (format === "date" || format === "date-time") {
|
|
337
|
+
const dateType = this.config.options.dateType === "Date" ? "Date" : "string";
|
|
338
|
+
return this.nullableType(dateType, isNullable);
|
|
339
|
+
}
|
|
340
|
+
if (format === "binary") return "Blob";
|
|
341
|
+
if (format === "uuid") return "string";
|
|
342
|
+
if (format === "email") return "string";
|
|
343
|
+
if (format === "uri") return "string";
|
|
344
|
+
return this.nullableType("string", isNullable);
|
|
345
|
+
case "number":
|
|
346
|
+
case "integer":
|
|
347
|
+
return this.nullableType("number", isNullable);
|
|
348
|
+
case "boolean":
|
|
349
|
+
return this.nullableType("boolean", isNullable);
|
|
350
|
+
case "array":
|
|
351
|
+
return this.nullableType("unknown[]", isNullable);
|
|
352
|
+
case "object":
|
|
353
|
+
return this.nullableType("Record<string, unknown>", isNullable);
|
|
354
|
+
case "null":
|
|
355
|
+
return this.nullableType("null", isNullable);
|
|
356
|
+
default:
|
|
357
|
+
console.warn(`Unknown swagger type: ${type}`);
|
|
358
|
+
return this.nullableType("unknown", isNullable);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
nullableType(type, isNullable) {
|
|
362
|
+
return type + (isNullable ? " | null" : "");
|
|
363
|
+
}
|
|
364
|
+
pascalCaseForEnums(str) {
|
|
365
|
+
return str.replace(/[^a-zA-Z0-9]/g, "_").replace(/(?:^|_)([a-z])/g, (_, char) => char.toUpperCase()).replace(/^([0-9])/, "_$1");
|
|
366
|
+
}
|
|
367
|
+
sanitizePropertyName(name) {
|
|
368
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
|
|
369
|
+
return `"${name}"`;
|
|
370
|
+
}
|
|
371
|
+
return name;
|
|
372
|
+
}
|
|
373
|
+
toEnumKey(value) {
|
|
374
|
+
return value.toString().replace(/[^a-zA-Z0-9]/g, "_").replace(/^([0-9])/, "_$1").toUpperCase();
|
|
375
|
+
}
|
|
376
|
+
getArrayItemType(items) {
|
|
377
|
+
if (Array.isArray(items)) {
|
|
378
|
+
const types = items.map((item) => this.resolveSwaggerType(item));
|
|
379
|
+
return `[${types.join(", ")}]`;
|
|
380
|
+
} else {
|
|
381
|
+
return this.resolveSwaggerType(items);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
escapeString(str) {
|
|
385
|
+
return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// src/lib/generators/utility/token.generator.ts
|
|
390
|
+
var import_ts_morph2 = require("ts-morph");
|
|
391
|
+
var path = __toESM(require("path"));
|
|
392
|
+
var TokenGenerator = class {
|
|
393
|
+
static {
|
|
394
|
+
__name(this, "TokenGenerator");
|
|
395
|
+
}
|
|
396
|
+
project;
|
|
397
|
+
constructor(project) {
|
|
398
|
+
this.project = project;
|
|
399
|
+
}
|
|
400
|
+
generate(outputDir) {
|
|
401
|
+
const tokensDir = path.join(outputDir, "tokens");
|
|
402
|
+
const filePath = path.join(tokensDir, "index.ts");
|
|
403
|
+
const sourceFile = this.project.createSourceFile(filePath, "", {
|
|
404
|
+
overwrite: true
|
|
405
|
+
});
|
|
406
|
+
sourceFile.addImportDeclaration({
|
|
407
|
+
namedImports: [
|
|
408
|
+
"InjectionToken"
|
|
409
|
+
],
|
|
410
|
+
moduleSpecifier: "@angular/core"
|
|
411
|
+
});
|
|
412
|
+
sourceFile.addVariableStatement({
|
|
413
|
+
isExported: true,
|
|
414
|
+
declarationKind: import_ts_morph2.VariableDeclarationKind.Const,
|
|
415
|
+
declarations: [
|
|
416
|
+
{
|
|
417
|
+
name: "BASE_PATH",
|
|
418
|
+
initializer: `new InjectionToken<string>('BASE_PATH', {
|
|
419
|
+
providedIn: 'root',
|
|
420
|
+
factory: () => '/api', // Default fallback
|
|
421
|
+
})`
|
|
422
|
+
}
|
|
423
|
+
],
|
|
424
|
+
leadingTrivia: `/**
|
|
425
|
+
* Injection token for the base API path
|
|
426
|
+
*/
|
|
427
|
+
`
|
|
428
|
+
});
|
|
429
|
+
sourceFile.saveSync();
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// src/lib/generators/utility/file-download.generator.ts
|
|
434
|
+
var path2 = __toESM(require("path"));
|
|
435
|
+
var FileDownloadGenerator = class {
|
|
436
|
+
static {
|
|
437
|
+
__name(this, "FileDownloadGenerator");
|
|
438
|
+
}
|
|
439
|
+
project;
|
|
440
|
+
constructor(project) {
|
|
441
|
+
this.project = project;
|
|
442
|
+
}
|
|
443
|
+
generate(outputDir) {
|
|
444
|
+
const utilsDir = path2.join(outputDir, "utils");
|
|
445
|
+
const filePath = path2.join(utilsDir, "file-download.ts");
|
|
446
|
+
const sourceFile = this.project.createSourceFile(filePath, "", {
|
|
447
|
+
overwrite: true
|
|
448
|
+
});
|
|
449
|
+
sourceFile.addImportDeclaration({
|
|
450
|
+
namedImports: [
|
|
451
|
+
"Observable"
|
|
452
|
+
],
|
|
453
|
+
moduleSpecifier: "rxjs"
|
|
454
|
+
});
|
|
455
|
+
sourceFile.addImportDeclaration({
|
|
456
|
+
namedImports: [
|
|
457
|
+
"tap"
|
|
458
|
+
],
|
|
459
|
+
moduleSpecifier: "rxjs/operators"
|
|
460
|
+
});
|
|
461
|
+
sourceFile.addFunction({
|
|
462
|
+
name: "downloadFile",
|
|
463
|
+
isExported: true,
|
|
464
|
+
parameters: [
|
|
465
|
+
{
|
|
466
|
+
name: "blob",
|
|
467
|
+
type: "Blob"
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
name: "filename",
|
|
471
|
+
type: "string"
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
name: "mimeType",
|
|
475
|
+
type: "string",
|
|
476
|
+
hasQuestionToken: true
|
|
477
|
+
}
|
|
478
|
+
],
|
|
479
|
+
returnType: "void",
|
|
480
|
+
statements: `
|
|
481
|
+
// Create a temporary URL for the blob
|
|
482
|
+
const url = window.URL.createObjectURL(blob);
|
|
483
|
+
|
|
484
|
+
// Create a temporary anchor element and trigger download
|
|
485
|
+
const link = document.createElement('a');
|
|
486
|
+
link.href = url;
|
|
487
|
+
link.download = filename;
|
|
488
|
+
|
|
489
|
+
// Append to body, click, and remove
|
|
490
|
+
document.body.appendChild(link);
|
|
491
|
+
link.click();
|
|
492
|
+
document.body.removeChild(link);
|
|
493
|
+
|
|
494
|
+
// Clean up the URL
|
|
495
|
+
window.URL.revokeObjectURL(url);`
|
|
496
|
+
});
|
|
497
|
+
sourceFile.addFunction({
|
|
498
|
+
name: "downloadFileOperator",
|
|
499
|
+
isExported: true,
|
|
500
|
+
typeParameters: [
|
|
501
|
+
{
|
|
502
|
+
name: "T",
|
|
503
|
+
constraint: "Blob"
|
|
504
|
+
}
|
|
505
|
+
],
|
|
506
|
+
parameters: [
|
|
507
|
+
{
|
|
508
|
+
name: "filename",
|
|
509
|
+
type: "string | ((blob: T) => string)"
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
name: "mimeType",
|
|
513
|
+
type: "string",
|
|
514
|
+
hasQuestionToken: true
|
|
515
|
+
}
|
|
516
|
+
],
|
|
517
|
+
returnType: "(source: Observable<T>) => Observable<T>",
|
|
518
|
+
statements: `
|
|
519
|
+
return (source: Observable<T>) => {
|
|
520
|
+
return source.pipe(
|
|
521
|
+
tap((blob: T) => {
|
|
522
|
+
const actualFilename = typeof filename === 'function' ? filename(blob) : filename;
|
|
523
|
+
downloadFile(blob, actualFilename, mimeType);
|
|
524
|
+
})
|
|
525
|
+
);
|
|
526
|
+
};`
|
|
527
|
+
});
|
|
528
|
+
sourceFile.addFunction({
|
|
529
|
+
name: "extractFilenameFromContentDisposition",
|
|
530
|
+
isExported: true,
|
|
531
|
+
parameters: [
|
|
532
|
+
{
|
|
533
|
+
name: "contentDisposition",
|
|
534
|
+
type: "string | null"
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
name: "fallbackFilename",
|
|
538
|
+
type: "string",
|
|
539
|
+
initializer: '"download"'
|
|
540
|
+
}
|
|
541
|
+
],
|
|
542
|
+
returnType: "string",
|
|
543
|
+
statements: `
|
|
544
|
+
if (!contentDisposition) {
|
|
545
|
+
return fallbackFilename;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Try to extract filename from Content-Disposition header
|
|
549
|
+
// Supports both "filename=" and "filename*=" formats
|
|
550
|
+
const filenameMatch = contentDisposition.match(/filename\\*?=['"]?([^'"\\n;]+)['"]?/i);
|
|
551
|
+
|
|
552
|
+
if (filenameMatch && filenameMatch[1]) {
|
|
553
|
+
// Decode if it's RFC 5987 encoded (filename*=UTF-8''...)
|
|
554
|
+
const filename = filenameMatch[1];
|
|
555
|
+
if (filename.includes("''")) {
|
|
556
|
+
const parts = filename.split("''");
|
|
557
|
+
if (parts.length === 2) {
|
|
558
|
+
try {
|
|
559
|
+
return decodeURIComponent(parts[1]);
|
|
560
|
+
} catch {
|
|
561
|
+
return parts[1];
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return filename;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return fallbackFilename;`
|
|
569
|
+
});
|
|
570
|
+
sourceFile.saveSync();
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// src/lib/generators/utility/date-transformer.generator.ts
|
|
575
|
+
var path3 = __toESM(require("path"));
|
|
576
|
+
var DateTransformerGenerator = class {
|
|
577
|
+
static {
|
|
578
|
+
__name(this, "DateTransformerGenerator");
|
|
579
|
+
}
|
|
580
|
+
project;
|
|
581
|
+
constructor(project) {
|
|
582
|
+
this.project = project;
|
|
583
|
+
}
|
|
584
|
+
generate(outputDir) {
|
|
585
|
+
const utilsDir = path3.join(outputDir, "utils");
|
|
586
|
+
const filePath = path3.join(utilsDir, "date-transformer.ts");
|
|
587
|
+
const sourceFile = this.project.createSourceFile(filePath, "", {
|
|
588
|
+
overwrite: true
|
|
589
|
+
});
|
|
590
|
+
sourceFile.addImportDeclaration({
|
|
591
|
+
namedImports: [
|
|
592
|
+
"HttpInterceptor",
|
|
593
|
+
"HttpRequest",
|
|
594
|
+
"HttpHandler",
|
|
595
|
+
"HttpEvent",
|
|
596
|
+
"HttpResponse"
|
|
597
|
+
],
|
|
598
|
+
moduleSpecifier: "@angular/common/http"
|
|
599
|
+
});
|
|
600
|
+
sourceFile.addImportDeclaration({
|
|
601
|
+
namedImports: [
|
|
602
|
+
"Injectable"
|
|
603
|
+
],
|
|
604
|
+
moduleSpecifier: "@angular/core"
|
|
605
|
+
});
|
|
606
|
+
sourceFile.addImportDeclaration({
|
|
607
|
+
namedImports: [
|
|
608
|
+
"Observable"
|
|
609
|
+
],
|
|
610
|
+
moduleSpecifier: "rxjs"
|
|
611
|
+
});
|
|
612
|
+
sourceFile.addImportDeclaration({
|
|
613
|
+
namedImports: [
|
|
614
|
+
"map"
|
|
615
|
+
],
|
|
616
|
+
moduleSpecifier: "rxjs/operators"
|
|
617
|
+
});
|
|
618
|
+
sourceFile.addVariableStatement({
|
|
619
|
+
isExported: true,
|
|
620
|
+
declarationKind: "const",
|
|
621
|
+
declarations: [
|
|
622
|
+
{
|
|
623
|
+
name: "ISO_DATE_REGEX",
|
|
624
|
+
initializer: "/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z?$/"
|
|
625
|
+
}
|
|
626
|
+
]
|
|
627
|
+
});
|
|
628
|
+
sourceFile.addFunction({
|
|
629
|
+
name: "transformDates",
|
|
630
|
+
isExported: true,
|
|
631
|
+
parameters: [
|
|
632
|
+
{
|
|
633
|
+
name: "obj",
|
|
634
|
+
type: "any"
|
|
635
|
+
}
|
|
636
|
+
],
|
|
637
|
+
returnType: "any",
|
|
638
|
+
statements: `
|
|
639
|
+
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
|
640
|
+
return obj;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (obj instanceof Date) {
|
|
644
|
+
return obj;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (Array.isArray(obj)) {
|
|
648
|
+
return obj.map(item => transformDates(item));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (typeof obj === 'object') {
|
|
652
|
+
const transformed: any = {};
|
|
653
|
+
for (const key of Object.keys(obj)) {
|
|
654
|
+
const value = obj[key];
|
|
655
|
+
if (typeof value === 'string' && ISO_DATE_REGEX.test(value)) {
|
|
656
|
+
transformed[key] = new Date(value);
|
|
657
|
+
} else {
|
|
658
|
+
transformed[key] = transformDates(value);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return transformed;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return obj;`
|
|
665
|
+
});
|
|
666
|
+
sourceFile.addClass({
|
|
667
|
+
name: "DateInterceptor",
|
|
668
|
+
isExported: true,
|
|
669
|
+
decorators: [
|
|
670
|
+
{
|
|
671
|
+
name: "Injectable",
|
|
672
|
+
arguments: []
|
|
673
|
+
}
|
|
674
|
+
],
|
|
675
|
+
implements: [
|
|
676
|
+
"HttpInterceptor"
|
|
677
|
+
],
|
|
678
|
+
methods: [
|
|
679
|
+
{
|
|
680
|
+
name: "intercept",
|
|
681
|
+
parameters: [
|
|
682
|
+
{
|
|
683
|
+
name: "req",
|
|
684
|
+
type: "HttpRequest<any>"
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
name: "next",
|
|
688
|
+
type: "HttpHandler"
|
|
689
|
+
}
|
|
690
|
+
],
|
|
691
|
+
returnType: "Observable<HttpEvent<any>>",
|
|
692
|
+
statements: `
|
|
693
|
+
return next.handle(req).pipe(
|
|
694
|
+
map(event => {
|
|
695
|
+
if (event instanceof HttpResponse && event.body) {
|
|
696
|
+
return event.clone({ body: transformDates(event.body) });
|
|
697
|
+
}
|
|
698
|
+
return event;
|
|
699
|
+
})
|
|
700
|
+
);`
|
|
701
|
+
}
|
|
702
|
+
]
|
|
703
|
+
});
|
|
704
|
+
sourceFile.saveSync();
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
// src/lib/generators/service/service.generator.ts
|
|
709
|
+
var import_ts_morph3 = require("ts-morph");
|
|
710
|
+
var path4 = __toESM(require("path"));
|
|
711
|
+
|
|
712
|
+
// src/lib/utils/string.utils.ts
|
|
713
|
+
function camelCase(str) {
|
|
714
|
+
const cleaned = str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase());
|
|
715
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
716
|
+
}
|
|
717
|
+
__name(camelCase, "camelCase");
|
|
718
|
+
function kebabCase(str) {
|
|
719
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
720
|
+
}
|
|
721
|
+
__name(kebabCase, "kebabCase");
|
|
722
|
+
function pascalCase(str) {
|
|
723
|
+
return str.replace(/(?:^|[-_])([a-z])/g, (_, char) => char.toUpperCase());
|
|
724
|
+
}
|
|
725
|
+
__name(pascalCase, "pascalCase");
|
|
726
|
+
|
|
727
|
+
// src/lib/utils/type.utils.ts
|
|
728
|
+
function getTypeScriptType(schemaOrType, config, formatOrNullable, isNullable, context = "type") {
|
|
729
|
+
let schema;
|
|
730
|
+
let nullable;
|
|
731
|
+
if (typeof schemaOrType === "string" || schemaOrType === void 0) {
|
|
732
|
+
schema = {
|
|
733
|
+
type: schemaOrType,
|
|
734
|
+
format: typeof formatOrNullable === "string" ? formatOrNullable : void 0
|
|
735
|
+
};
|
|
736
|
+
nullable = typeof formatOrNullable === "boolean" ? formatOrNullable : isNullable;
|
|
737
|
+
} else {
|
|
738
|
+
schema = schemaOrType;
|
|
739
|
+
nullable = typeof formatOrNullable === "boolean" ? formatOrNullable : schema.nullable;
|
|
740
|
+
}
|
|
741
|
+
if (!schema) {
|
|
742
|
+
return "any";
|
|
743
|
+
}
|
|
744
|
+
if (schema.$ref) {
|
|
745
|
+
const refName = schema.$ref.split("/").pop();
|
|
746
|
+
return nullableType(pascalCase(refName), nullable);
|
|
747
|
+
}
|
|
748
|
+
if (schema.type === "array") {
|
|
749
|
+
const itemType = schema.items ? getTypeScriptType(schema.items, config, void 0, void 0, context) : "unknown";
|
|
750
|
+
return nullable ? `(${itemType}[] | null)` : `${itemType}[]`;
|
|
751
|
+
}
|
|
752
|
+
switch (schema.type) {
|
|
753
|
+
case "string":
|
|
754
|
+
if (schema.format === "date" || schema.format === "date-time") {
|
|
755
|
+
const dateType = config.options.dateType === "Date" ? "Date" : "string";
|
|
756
|
+
return nullableType(dateType, nullable);
|
|
757
|
+
}
|
|
758
|
+
if (schema.format === "binary") {
|
|
759
|
+
const binaryType = context === "type" ? "Blob" : "File";
|
|
760
|
+
return nullableType(binaryType, nullable);
|
|
761
|
+
}
|
|
762
|
+
if (schema.format === "uuid" || schema.format === "email" || schema.format === "uri" || schema.format === "hostname" || schema.format === "ipv4" || schema.format === "ipv6") {
|
|
763
|
+
return nullableType("string", nullable);
|
|
764
|
+
}
|
|
765
|
+
return nullableType("string", nullable);
|
|
766
|
+
case "number":
|
|
767
|
+
case "integer":
|
|
768
|
+
return nullableType("number", nullable);
|
|
769
|
+
case "boolean":
|
|
770
|
+
return nullableType("boolean", nullable);
|
|
771
|
+
case "object":
|
|
772
|
+
return nullableType(context === "type" ? "Record<string, unknown>" : "any", nullable);
|
|
773
|
+
case "null":
|
|
774
|
+
return "null";
|
|
775
|
+
default:
|
|
776
|
+
console.warn(`Unknown swagger type: ${schema.type}`);
|
|
777
|
+
return nullableType("any", nullable);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
__name(getTypeScriptType, "getTypeScriptType");
|
|
781
|
+
function nullableType(type, isNullable) {
|
|
782
|
+
return type + (isNullable ? " | null" : "");
|
|
783
|
+
}
|
|
784
|
+
__name(nullableType, "nullableType");
|
|
785
|
+
|
|
786
|
+
// src/lib/generators/service/service-method/service-method-body.generator.ts
|
|
787
|
+
var ServiceMethodBodyGenerator = class {
|
|
788
|
+
static {
|
|
789
|
+
__name(this, "ServiceMethodBodyGenerator");
|
|
790
|
+
}
|
|
791
|
+
config;
|
|
792
|
+
constructor(config) {
|
|
793
|
+
this.config = config;
|
|
794
|
+
}
|
|
795
|
+
generateMethodBody(operation) {
|
|
796
|
+
const context = this.createGenerationContext(operation);
|
|
797
|
+
const bodyParts = [
|
|
798
|
+
this.generateUrlConstruction(operation, context),
|
|
799
|
+
this.generateQueryParams(context),
|
|
800
|
+
this.generateHeaders(context),
|
|
801
|
+
this.generateMultipartFormData(operation, context),
|
|
802
|
+
this.generateRequestOptions(context),
|
|
803
|
+
this.generateHttpRequest(operation, context)
|
|
804
|
+
];
|
|
805
|
+
return bodyParts.filter(Boolean).join("\n");
|
|
806
|
+
}
|
|
807
|
+
getRequestBodyType(requestBody) {
|
|
808
|
+
const content = requestBody.content || {};
|
|
809
|
+
const jsonContent = content["application/json"];
|
|
810
|
+
if (jsonContent?.schema) {
|
|
811
|
+
return getTypeScriptType(jsonContent.schema, this.config, jsonContent.schema.nullable);
|
|
812
|
+
}
|
|
813
|
+
return "any";
|
|
814
|
+
}
|
|
815
|
+
isMultipartFormData(operation) {
|
|
816
|
+
return !!operation.requestBody?.content?.["multipart/form-data"];
|
|
817
|
+
}
|
|
818
|
+
getFormDataFields(operation) {
|
|
819
|
+
if (!this.isMultipartFormData(operation)) {
|
|
820
|
+
return [];
|
|
821
|
+
}
|
|
822
|
+
const properties = operation.requestBody?.content?.["multipart/form-data"]?.schema?.properties || {};
|
|
823
|
+
return Object.keys(properties);
|
|
824
|
+
}
|
|
825
|
+
getResponseTypeFromResponse(response) {
|
|
826
|
+
const content = response.content || {};
|
|
827
|
+
if (Object.keys(content).length === 0) {
|
|
828
|
+
return "json";
|
|
829
|
+
}
|
|
830
|
+
const responseTypes = [];
|
|
831
|
+
for (const [contentType, mediaType] of Object.entries(content)) {
|
|
832
|
+
const schema = mediaType?.schema;
|
|
833
|
+
const mapping = this.config?.options?.responseTypeMapping || {};
|
|
834
|
+
if (mapping[contentType]) {
|
|
835
|
+
responseTypes.push({
|
|
836
|
+
type: mapping[contentType],
|
|
837
|
+
priority: 1,
|
|
838
|
+
contentType
|
|
839
|
+
});
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
if (schema?.format === "binary" || schema?.format === "byte") {
|
|
843
|
+
responseTypes.push({
|
|
844
|
+
type: "blob",
|
|
845
|
+
priority: 2,
|
|
846
|
+
contentType
|
|
847
|
+
});
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
if (schema?.type === "string" && (schema?.format === "binary" || schema?.format === "byte")) {
|
|
851
|
+
responseTypes.push({
|
|
852
|
+
type: "blob",
|
|
853
|
+
priority: 2,
|
|
854
|
+
contentType
|
|
855
|
+
});
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
const inferredType = this.inferResponseTypeFromContentType(contentType);
|
|
859
|
+
let priority = 3;
|
|
860
|
+
if (inferredType === "json") {
|
|
861
|
+
priority = 2;
|
|
862
|
+
}
|
|
863
|
+
responseTypes.push({
|
|
864
|
+
type: inferredType,
|
|
865
|
+
priority,
|
|
866
|
+
contentType
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
responseTypes.sort((a, b) => a.priority - b.priority);
|
|
870
|
+
return responseTypes[0]?.type || "json";
|
|
871
|
+
}
|
|
872
|
+
createGenerationContext(operation) {
|
|
873
|
+
return {
|
|
874
|
+
pathParams: operation.parameters?.filter((p) => p.in === "path") || [],
|
|
875
|
+
queryParams: operation.parameters?.filter((p) => p.in === "query") || [],
|
|
876
|
+
hasBody: !!operation.requestBody,
|
|
877
|
+
isMultipart: this.isMultipartFormData(operation),
|
|
878
|
+
formDataFields: this.getFormDataFields(operation),
|
|
879
|
+
responseType: this.determineResponseType(operation)
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
generateUrlConstruction(operation, context) {
|
|
883
|
+
let urlExpression = `\`\${this.basePath}${operation.path}\``;
|
|
884
|
+
if (context.pathParams.length > 0) {
|
|
885
|
+
context.pathParams.forEach((param) => {
|
|
886
|
+
urlExpression = urlExpression.replace(`{${param.name}}`, `\${${param.name}}`);
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
return `const url = ${urlExpression};`;
|
|
890
|
+
}
|
|
891
|
+
generateQueryParams(context) {
|
|
892
|
+
if (context.queryParams.length === 0) {
|
|
893
|
+
return "";
|
|
894
|
+
}
|
|
895
|
+
const paramMappings = context.queryParams.map((param) => `if (${param.name} !== undefined) {
|
|
896
|
+
params = params.set('${param.name}', String(${param.name}));
|
|
897
|
+
}`).join("\n");
|
|
898
|
+
return `
|
|
899
|
+
let params = new HttpParams();
|
|
900
|
+
${paramMappings}`;
|
|
901
|
+
}
|
|
902
|
+
generateHeaders(context) {
|
|
903
|
+
const hasCustomHeaders = this.config.options.customHeaders;
|
|
904
|
+
if (!hasCustomHeaders && !context.isMultipart) {
|
|
905
|
+
return "";
|
|
906
|
+
}
|
|
907
|
+
let headerCode = `
|
|
908
|
+
let headers: HttpHeaders;
|
|
909
|
+
if (options?.headers instanceof HttpHeaders) {
|
|
910
|
+
headers = options.headers;
|
|
911
|
+
} else {
|
|
912
|
+
headers = new HttpHeaders(options?.headers);
|
|
913
|
+
}`;
|
|
914
|
+
if (hasCustomHeaders) {
|
|
915
|
+
headerCode += `
|
|
916
|
+
// Add default headers if not already present
|
|
917
|
+
${Object.entries(this.config.options.customHeaders || {}).map(([key, value]) => `if (!headers.has('${key}')) {
|
|
918
|
+
headers = headers.set('${key}', '${value}');
|
|
919
|
+
}`).join("\n")}`;
|
|
920
|
+
}
|
|
921
|
+
if (context.isMultipart) {
|
|
922
|
+
headerCode += `
|
|
923
|
+
// Remove Content-Type for multipart (browser will set it with boundary)
|
|
924
|
+
headers = headers.delete('Content-Type');`;
|
|
925
|
+
} else if (!context.isMultipart) {
|
|
926
|
+
headerCode += `
|
|
927
|
+
// Set Content-Type for JSON requests if not already set
|
|
928
|
+
if (!headers.has('Content-Type')) {
|
|
929
|
+
headers = headers.set('Content-Type', 'application/json');
|
|
930
|
+
}`;
|
|
931
|
+
}
|
|
932
|
+
return headerCode;
|
|
933
|
+
}
|
|
934
|
+
generateMultipartFormData(operation, context) {
|
|
935
|
+
if (!context.isMultipart || context.formDataFields.length === 0) {
|
|
936
|
+
return "";
|
|
937
|
+
}
|
|
938
|
+
const formDataAppends = context.formDataFields.map((field) => {
|
|
939
|
+
const fieldSchema = operation.requestBody?.content?.["multipart/form-data"]?.schema?.properties?.[field];
|
|
940
|
+
const isFile = fieldSchema?.type === "string" && fieldSchema?.format === "binary";
|
|
941
|
+
const valueExpression = isFile ? field : `String(${field})`;
|
|
942
|
+
return `if (${field} !== undefined) {
|
|
943
|
+
formData.append('${field}', ${valueExpression});
|
|
944
|
+
}`;
|
|
945
|
+
}).join("\n");
|
|
946
|
+
return `
|
|
947
|
+
const formData = new FormData();
|
|
948
|
+
${formDataAppends}`;
|
|
949
|
+
}
|
|
950
|
+
generateRequestOptions(context) {
|
|
951
|
+
const options = [];
|
|
952
|
+
options.push("observe: observe as any");
|
|
953
|
+
const hasHeaders = this.config.options.customHeaders || context.isMultipart;
|
|
954
|
+
if (hasHeaders) {
|
|
955
|
+
options.push("headers");
|
|
956
|
+
}
|
|
957
|
+
if (context.queryParams.length > 0) {
|
|
958
|
+
options.push("params");
|
|
959
|
+
}
|
|
960
|
+
if (context.responseType !== "json") {
|
|
961
|
+
options.push(`responseType: '${context.responseType}' as '${context.responseType}'`);
|
|
962
|
+
}
|
|
963
|
+
options.push("reportProgress: options?.reportProgress");
|
|
964
|
+
options.push("withCredentials: options?.withCredentials");
|
|
965
|
+
if (options.length > 0) {
|
|
966
|
+
options.push("context: options?.context");
|
|
967
|
+
}
|
|
968
|
+
const formattedOptions = options.filter((opt) => opt && !opt.includes("undefined")).join(",\n ");
|
|
969
|
+
return `
|
|
970
|
+
const requestOptions: any = {
|
|
971
|
+
${formattedOptions}
|
|
972
|
+
};`;
|
|
973
|
+
}
|
|
974
|
+
generateHttpRequest(operation, context) {
|
|
975
|
+
const httpMethod = operation.method.toLowerCase();
|
|
976
|
+
let bodyParam = "";
|
|
977
|
+
if (context.hasBody) {
|
|
978
|
+
if (context.isMultipart) {
|
|
979
|
+
bodyParam = "formData";
|
|
980
|
+
} else if (operation.requestBody?.content?.["application/json"]) {
|
|
981
|
+
const bodyType = this.getRequestBodyType(operation.requestBody);
|
|
982
|
+
const isInterface = this.isDataTypeInterface(bodyType);
|
|
983
|
+
bodyParam = isInterface ? camelCase(bodyType) : "requestBody";
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
const methodsWithBody = [
|
|
987
|
+
"post",
|
|
988
|
+
"put",
|
|
989
|
+
"patch"
|
|
990
|
+
];
|
|
991
|
+
if (methodsWithBody.includes(httpMethod)) {
|
|
992
|
+
return `
|
|
993
|
+
return this.httpClient.${httpMethod}(url, ${bodyParam || "null"}, requestOptions);`;
|
|
994
|
+
} else {
|
|
995
|
+
return `
|
|
996
|
+
return this.httpClient.${httpMethod}(url, requestOptions);`;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
determineResponseType(operation) {
|
|
1000
|
+
const successResponses = [
|
|
1001
|
+
"200",
|
|
1002
|
+
"201",
|
|
1003
|
+
"202",
|
|
1004
|
+
"204",
|
|
1005
|
+
"206"
|
|
1006
|
+
];
|
|
1007
|
+
for (const statusCode of successResponses) {
|
|
1008
|
+
const response = operation.responses?.[statusCode];
|
|
1009
|
+
if (!response) continue;
|
|
1010
|
+
return this.getResponseTypeFromResponse(response);
|
|
1011
|
+
}
|
|
1012
|
+
return "json";
|
|
1013
|
+
}
|
|
1014
|
+
isDataTypeInterface(type) {
|
|
1015
|
+
const invalidTypes = [
|
|
1016
|
+
"any",
|
|
1017
|
+
"File",
|
|
1018
|
+
"string",
|
|
1019
|
+
"number",
|
|
1020
|
+
"boolean",
|
|
1021
|
+
"object",
|
|
1022
|
+
"unknown",
|
|
1023
|
+
"[]",
|
|
1024
|
+
"Array"
|
|
1025
|
+
];
|
|
1026
|
+
return !invalidTypes.some((invalidType) => type.includes(invalidType));
|
|
1027
|
+
}
|
|
1028
|
+
inferResponseTypeFromContentType(contentType) {
|
|
1029
|
+
const normalizedType = contentType.split(";")[0].trim().toLowerCase();
|
|
1030
|
+
if (normalizedType.includes("json") || normalizedType === "application/ld+json" || normalizedType === "application/hal+json" || normalizedType === "application/vnd.api+json") {
|
|
1031
|
+
return "json";
|
|
1032
|
+
}
|
|
1033
|
+
if (normalizedType.includes("xml") || normalizedType === "application/soap+xml" || normalizedType === "application/atom+xml" || normalizedType === "application/rss+xml") {
|
|
1034
|
+
return "text";
|
|
1035
|
+
}
|
|
1036
|
+
if (normalizedType.startsWith("text/")) {
|
|
1037
|
+
const binaryTextTypes = [
|
|
1038
|
+
"text/rtf",
|
|
1039
|
+
"text/cache-manifest",
|
|
1040
|
+
"text/vcard",
|
|
1041
|
+
"text/calendar"
|
|
1042
|
+
];
|
|
1043
|
+
if (binaryTextTypes.includes(normalizedType)) {
|
|
1044
|
+
return "blob";
|
|
1045
|
+
}
|
|
1046
|
+
return "text";
|
|
1047
|
+
}
|
|
1048
|
+
if (normalizedType === "application/x-www-form-urlencoded" || normalizedType === "multipart/form-data") {
|
|
1049
|
+
return "text";
|
|
1050
|
+
}
|
|
1051
|
+
if (normalizedType === "application/javascript" || normalizedType === "application/typescript" || normalizedType === "application/css" || normalizedType === "application/yaml" || normalizedType === "application/x-yaml" || normalizedType === "application/toml") {
|
|
1052
|
+
return "text";
|
|
1053
|
+
}
|
|
1054
|
+
if (normalizedType.startsWith("image/") || normalizedType.startsWith("audio/") || normalizedType.startsWith("video/") || normalizedType === "application/pdf" || normalizedType === "application/zip" || normalizedType.includes("octet-stream")) {
|
|
1055
|
+
return "arraybuffer";
|
|
1056
|
+
}
|
|
1057
|
+
return "blob";
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
// src/lib/generators/service/service-method/service-method-params.generator.ts
|
|
1062
|
+
var ServiceMethodParamsGenerator = class {
|
|
1063
|
+
static {
|
|
1064
|
+
__name(this, "ServiceMethodParamsGenerator");
|
|
1065
|
+
}
|
|
1066
|
+
config;
|
|
1067
|
+
constructor(config) {
|
|
1068
|
+
this.config = config;
|
|
1069
|
+
}
|
|
1070
|
+
generateMethodParameters(operation) {
|
|
1071
|
+
const params = this.generateApiParameters(operation);
|
|
1072
|
+
const optionsParam = this.addOptionsParameter();
|
|
1073
|
+
const combined = [
|
|
1074
|
+
...params,
|
|
1075
|
+
...optionsParam
|
|
1076
|
+
];
|
|
1077
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1078
|
+
const uniqueParams = [];
|
|
1079
|
+
for (const param of combined) {
|
|
1080
|
+
if (!seen.has(param.name)) {
|
|
1081
|
+
seen.add(param.name);
|
|
1082
|
+
uniqueParams.push(param);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return uniqueParams;
|
|
1086
|
+
}
|
|
1087
|
+
generateApiParameters(operation) {
|
|
1088
|
+
const params = [];
|
|
1089
|
+
const pathParams = operation.parameters?.filter((p) => p.in === "path") || [];
|
|
1090
|
+
pathParams.forEach((param) => {
|
|
1091
|
+
params.push({
|
|
1092
|
+
name: param.name,
|
|
1093
|
+
type: getTypeScriptType(param.schema || param, this.config),
|
|
1094
|
+
hasQuestionToken: !param.required
|
|
1095
|
+
});
|
|
1096
|
+
});
|
|
1097
|
+
if (operation.requestBody && operation.requestBody?.content?.["multipart/form-data"]) {
|
|
1098
|
+
Object.entries(operation.requestBody?.content?.["multipart/form-data"].schema?.properties ?? {}).forEach(([key, value]) => {
|
|
1099
|
+
params.push({
|
|
1100
|
+
name: key,
|
|
1101
|
+
type: getTypeScriptType(value, this.config, value.nullable),
|
|
1102
|
+
hasQuestionToken: !value.required
|
|
1103
|
+
});
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
if (operation.requestBody && operation.requestBody?.content?.["application/json"]) {
|
|
1107
|
+
const bodyType = this.getRequestBodyType(operation.requestBody);
|
|
1108
|
+
const isInterface = this.isDataTypeInterface(bodyType);
|
|
1109
|
+
params.push({
|
|
1110
|
+
name: isInterface ? camelCase(bodyType) : "requestBody",
|
|
1111
|
+
type: bodyType,
|
|
1112
|
+
hasQuestionToken: !operation.requestBody.required
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
const queryParams = operation.parameters?.filter((p) => p.in === "query") || [];
|
|
1116
|
+
queryParams.forEach((param) => {
|
|
1117
|
+
params.push({
|
|
1118
|
+
name: param.name,
|
|
1119
|
+
type: getTypeScriptType(param.schema || param, this.config),
|
|
1120
|
+
hasQuestionToken: !param.required
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
return params;
|
|
1124
|
+
}
|
|
1125
|
+
addOptionsParameter() {
|
|
1126
|
+
return [
|
|
1127
|
+
{
|
|
1128
|
+
name: "observe",
|
|
1129
|
+
type: `'body' | 'events' | 'response'`,
|
|
1130
|
+
hasQuestionToken: true
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
name: "options",
|
|
1134
|
+
type: `{ headers?: HttpHeaders; params?: HttpParams; reportProgress?: boolean; responseType?: 'arraybuffer' | 'blob' | 'json' | 'text'; withCredentials?: boolean; context?: HttpContext; }`,
|
|
1135
|
+
hasQuestionToken: true
|
|
1136
|
+
}
|
|
1137
|
+
];
|
|
1138
|
+
}
|
|
1139
|
+
isDataTypeInterface(type) {
|
|
1140
|
+
const invalidTypes = [
|
|
1141
|
+
"any",
|
|
1142
|
+
"File",
|
|
1143
|
+
"string",
|
|
1144
|
+
"number",
|
|
1145
|
+
"boolean",
|
|
1146
|
+
"object",
|
|
1147
|
+
"unknown",
|
|
1148
|
+
"[]",
|
|
1149
|
+
"Array"
|
|
1150
|
+
];
|
|
1151
|
+
return !invalidTypes.some((invalidType) => type.includes(invalidType));
|
|
1152
|
+
}
|
|
1153
|
+
getRequestBodyType(requestBody) {
|
|
1154
|
+
const content = requestBody.content || {};
|
|
1155
|
+
const jsonContent = content["application/json"];
|
|
1156
|
+
if (jsonContent?.schema) {
|
|
1157
|
+
return getTypeScriptType(jsonContent.schema, this.config, jsonContent.schema.nullable);
|
|
1158
|
+
}
|
|
1159
|
+
return "any";
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
// src/lib/generators/service/service-method/service-method-overloads.generator.ts
|
|
1164
|
+
var ServiceMethodOverloadsGenerator = class {
|
|
1165
|
+
static {
|
|
1166
|
+
__name(this, "ServiceMethodOverloadsGenerator");
|
|
1167
|
+
}
|
|
1168
|
+
config;
|
|
1169
|
+
paramsGenerator;
|
|
1170
|
+
constructor(config) {
|
|
1171
|
+
this.config = config;
|
|
1172
|
+
this.paramsGenerator = new ServiceMethodParamsGenerator(config);
|
|
1173
|
+
}
|
|
1174
|
+
generateMethodOverloads(operation) {
|
|
1175
|
+
const observeTypes = [
|
|
1176
|
+
"body",
|
|
1177
|
+
"response",
|
|
1178
|
+
"events"
|
|
1179
|
+
];
|
|
1180
|
+
const overloads = [];
|
|
1181
|
+
const responseType = this.determineResponseTypeForOperation(operation);
|
|
1182
|
+
observeTypes.forEach((observe) => {
|
|
1183
|
+
const overload = this.generateMethodOverload(operation, observe, responseType);
|
|
1184
|
+
if (overload) {
|
|
1185
|
+
overloads.push(overload);
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
return overloads;
|
|
1189
|
+
}
|
|
1190
|
+
generateMethodOverload(operation, observe, responseType) {
|
|
1191
|
+
const responseDataType = this.generateOverloadResponseType(operation);
|
|
1192
|
+
const params = this.generateOverloadParameters(operation, observe, responseType);
|
|
1193
|
+
const returnType = this.generateOverloadReturnType(responseDataType, observe);
|
|
1194
|
+
return {
|
|
1195
|
+
parameters: params,
|
|
1196
|
+
returnType
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
generateOverloadParameters(operation, observe, responseType) {
|
|
1200
|
+
const params = this.paramsGenerator.generateApiParameters(operation);
|
|
1201
|
+
const optionsParam = this.addOverloadOptionsParameter(observe, responseType);
|
|
1202
|
+
const combined = [
|
|
1203
|
+
...params,
|
|
1204
|
+
...optionsParam
|
|
1205
|
+
];
|
|
1206
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1207
|
+
const uniqueParams = [];
|
|
1208
|
+
for (const param of combined) {
|
|
1209
|
+
if (!seen.has(param.name)) {
|
|
1210
|
+
seen.add(param.name);
|
|
1211
|
+
uniqueParams.push(param);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return uniqueParams;
|
|
1215
|
+
}
|
|
1216
|
+
addOverloadOptionsParameter(observe, responseType) {
|
|
1217
|
+
return [
|
|
1218
|
+
{
|
|
1219
|
+
name: "observe",
|
|
1220
|
+
type: `'${observe}'`,
|
|
1221
|
+
hasQuestionToken: true
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
name: "options",
|
|
1225
|
+
type: `{ headers?: HttpHeaders; reportProgress?: boolean; responseType?: '${responseType}'; withCredentials?: boolean; context?: HttpContext; }`,
|
|
1226
|
+
hasQuestionToken: true
|
|
1227
|
+
}
|
|
1228
|
+
];
|
|
1229
|
+
}
|
|
1230
|
+
generateOverloadResponseType(operation) {
|
|
1231
|
+
const response = operation.responses?.["200"] || operation.responses?.["201"] || operation.responses?.["204"];
|
|
1232
|
+
if (!response) {
|
|
1233
|
+
return "any";
|
|
1234
|
+
}
|
|
1235
|
+
return this.getResponseType(response);
|
|
1236
|
+
}
|
|
1237
|
+
generateOverloadReturnType(responseType, observe) {
|
|
1238
|
+
switch (observe) {
|
|
1239
|
+
case "body":
|
|
1240
|
+
return `Observable<${responseType}>`;
|
|
1241
|
+
case "response":
|
|
1242
|
+
return `Observable<HttpResponse<${responseType}>>`;
|
|
1243
|
+
case "events":
|
|
1244
|
+
return `Observable<HttpEvent<${responseType}>>`;
|
|
1245
|
+
default:
|
|
1246
|
+
throw new Error(`Unsupported observe type: ${observe}`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
getResponseTypeFromResponse(response) {
|
|
1250
|
+
const content = response.content || {};
|
|
1251
|
+
if (Object.keys(content).length === 0) {
|
|
1252
|
+
return "json";
|
|
1253
|
+
}
|
|
1254
|
+
const responseTypes = [];
|
|
1255
|
+
for (const [contentType, mediaType] of Object.entries(content)) {
|
|
1256
|
+
const schema = mediaType?.schema;
|
|
1257
|
+
const mapping = this.config?.options?.responseTypeMapping || {};
|
|
1258
|
+
if (mapping[contentType]) {
|
|
1259
|
+
responseTypes.push({
|
|
1260
|
+
type: mapping[contentType],
|
|
1261
|
+
priority: 1,
|
|
1262
|
+
contentType
|
|
1263
|
+
});
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
if (schema?.format === "binary" || schema?.format === "byte") {
|
|
1267
|
+
responseTypes.push({
|
|
1268
|
+
type: "blob",
|
|
1269
|
+
priority: 2,
|
|
1270
|
+
contentType
|
|
1271
|
+
});
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
if (schema?.type === "string" && (schema?.format === "binary" || schema?.format === "byte")) {
|
|
1275
|
+
responseTypes.push({
|
|
1276
|
+
type: "blob",
|
|
1277
|
+
priority: 2,
|
|
1278
|
+
contentType
|
|
1279
|
+
});
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
const inferredType = this.inferResponseTypeFromContentType(contentType);
|
|
1283
|
+
let priority = 3;
|
|
1284
|
+
if (inferredType === "json") {
|
|
1285
|
+
priority = 2;
|
|
1286
|
+
}
|
|
1287
|
+
responseTypes.push({
|
|
1288
|
+
type: inferredType,
|
|
1289
|
+
priority,
|
|
1290
|
+
contentType
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
responseTypes.sort((a, b) => a.priority - b.priority);
|
|
1294
|
+
return responseTypes[0]?.type || "json";
|
|
1295
|
+
}
|
|
1296
|
+
getResponseType(response) {
|
|
1297
|
+
const responseType = this.getResponseTypeFromResponse(response);
|
|
1298
|
+
switch (responseType) {
|
|
1299
|
+
case "blob":
|
|
1300
|
+
return "Blob";
|
|
1301
|
+
case "arraybuffer":
|
|
1302
|
+
return "ArrayBuffer";
|
|
1303
|
+
case "text":
|
|
1304
|
+
return "string";
|
|
1305
|
+
case "json": {
|
|
1306
|
+
const content = response.content || {};
|
|
1307
|
+
for (const [contentType, mediaType] of Object.entries(content)) {
|
|
1308
|
+
if (this.inferResponseTypeFromContentType(contentType) === "json" && mediaType?.schema) {
|
|
1309
|
+
return getTypeScriptType(mediaType.schema, this.config, mediaType.schema.nullable);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return "any";
|
|
1313
|
+
}
|
|
1314
|
+
default:
|
|
1315
|
+
return "any";
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
determineResponseTypeForOperation(operation) {
|
|
1319
|
+
const successResponses = [
|
|
1320
|
+
"200",
|
|
1321
|
+
"201",
|
|
1322
|
+
"202",
|
|
1323
|
+
"204",
|
|
1324
|
+
"206"
|
|
1325
|
+
];
|
|
1326
|
+
for (const statusCode of successResponses) {
|
|
1327
|
+
const response = operation.responses?.[statusCode];
|
|
1328
|
+
if (!response) continue;
|
|
1329
|
+
return this.getResponseTypeFromResponse(response);
|
|
1330
|
+
}
|
|
1331
|
+
return "json";
|
|
1332
|
+
}
|
|
1333
|
+
inferResponseTypeFromContentType(contentType) {
|
|
1334
|
+
const normalizedType = contentType.split(";")[0].trim().toLowerCase();
|
|
1335
|
+
if (normalizedType.includes("json") || normalizedType === "application/ld+json" || normalizedType === "application/hal+json" || normalizedType === "application/vnd.api+json") {
|
|
1336
|
+
return "json";
|
|
1337
|
+
}
|
|
1338
|
+
if (normalizedType.includes("xml") || normalizedType === "application/soap+xml" || normalizedType === "application/atom+xml" || normalizedType === "application/rss+xml") {
|
|
1339
|
+
return "text";
|
|
1340
|
+
}
|
|
1341
|
+
if (normalizedType.startsWith("text/")) {
|
|
1342
|
+
const binaryTextTypes = [
|
|
1343
|
+
"text/rtf",
|
|
1344
|
+
"text/cache-manifest",
|
|
1345
|
+
"text/vcard",
|
|
1346
|
+
"text/calendar"
|
|
1347
|
+
];
|
|
1348
|
+
if (binaryTextTypes.includes(normalizedType)) {
|
|
1349
|
+
return "blob";
|
|
1350
|
+
}
|
|
1351
|
+
return "text";
|
|
1352
|
+
}
|
|
1353
|
+
if (normalizedType === "application/x-www-form-urlencoded" || normalizedType === "multipart/form-data") {
|
|
1354
|
+
return "text";
|
|
1355
|
+
}
|
|
1356
|
+
if (normalizedType === "application/javascript" || normalizedType === "application/typescript" || normalizedType === "application/css" || normalizedType === "application/yaml" || normalizedType === "application/x-yaml" || normalizedType === "application/toml") {
|
|
1357
|
+
return "text";
|
|
1358
|
+
}
|
|
1359
|
+
if (normalizedType.startsWith("image/") || normalizedType.startsWith("audio/") || normalizedType.startsWith("video/") || normalizedType === "application/pdf" || normalizedType === "application/zip" || normalizedType.includes("octet-stream")) {
|
|
1360
|
+
return "arraybuffer";
|
|
1361
|
+
}
|
|
1362
|
+
return "blob";
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
// src/lib/generators/service/service-method.generator.ts
|
|
1367
|
+
var ServiceMethodGenerator = class {
|
|
1368
|
+
static {
|
|
1369
|
+
__name(this, "ServiceMethodGenerator");
|
|
1370
|
+
}
|
|
1371
|
+
config;
|
|
1372
|
+
bodyGenerator;
|
|
1373
|
+
overloadsGenerator;
|
|
1374
|
+
paramsGenerator;
|
|
1375
|
+
constructor(config) {
|
|
1376
|
+
this.config = config;
|
|
1377
|
+
this.bodyGenerator = new ServiceMethodBodyGenerator(config);
|
|
1378
|
+
this.overloadsGenerator = new ServiceMethodOverloadsGenerator(config);
|
|
1379
|
+
this.paramsGenerator = new ServiceMethodParamsGenerator(config);
|
|
1380
|
+
}
|
|
1381
|
+
addServiceMethod(serviceClass, operation) {
|
|
1382
|
+
const methodName = this.generateMethodName(operation);
|
|
1383
|
+
const parameters = this.paramsGenerator.generateMethodParameters(operation);
|
|
1384
|
+
const returnType = this.generateReturnType();
|
|
1385
|
+
const methodBody = this.bodyGenerator.generateMethodBody(operation);
|
|
1386
|
+
const methodOverLoads = this.overloadsGenerator.generateMethodOverloads(operation);
|
|
1387
|
+
serviceClass.addMethod({
|
|
1388
|
+
name: methodName,
|
|
1389
|
+
parameters,
|
|
1390
|
+
returnType,
|
|
1391
|
+
statements: methodBody,
|
|
1392
|
+
overloads: methodOverLoads
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
generateMethodName(operation) {
|
|
1396
|
+
if (this.config.options.customizeMethodName) {
|
|
1397
|
+
if (operation.operationId == null) {
|
|
1398
|
+
throw new Error(`Operation ID is required for method name customization of operation: (${operation.method}) ${operation.path}`);
|
|
1399
|
+
}
|
|
1400
|
+
return this.config.options.customizeMethodName(operation.operationId);
|
|
1401
|
+
} else {
|
|
1402
|
+
return this.defaultNameGenerator(operation);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
generateReturnType() {
|
|
1406
|
+
return "Observable<any>";
|
|
1407
|
+
}
|
|
1408
|
+
defaultNameGenerator(operation) {
|
|
1409
|
+
if (operation.operationId) {
|
|
1410
|
+
return camelCase(operation.operationId);
|
|
1411
|
+
}
|
|
1412
|
+
const method = operation.method.toLowerCase();
|
|
1413
|
+
const pathParts = operation.path.split("/").filter((p) => p && !p.startsWith("{"));
|
|
1414
|
+
const resource = pathParts[pathParts.length - 1] || "resource";
|
|
1415
|
+
return `${method}${pascalCase(resource)}`;
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
// src/lib/generators/service/service.generator.ts
|
|
1420
|
+
var ServiceGenerator = class {
|
|
1421
|
+
static {
|
|
1422
|
+
__name(this, "ServiceGenerator");
|
|
1423
|
+
}
|
|
1424
|
+
project;
|
|
1425
|
+
parser;
|
|
1426
|
+
spec;
|
|
1427
|
+
config;
|
|
1428
|
+
methodGenerator;
|
|
1429
|
+
constructor(swaggerPath, project, config) {
|
|
1430
|
+
this.config = config;
|
|
1431
|
+
this.project = project;
|
|
1432
|
+
this.parser = new SwaggerParser(swaggerPath);
|
|
1433
|
+
this.spec = JSON.parse(require("fs").readFileSync(swaggerPath, "utf8"));
|
|
1434
|
+
this.methodGenerator = new ServiceMethodGenerator(config);
|
|
1435
|
+
}
|
|
1436
|
+
generate(outputRoot) {
|
|
1437
|
+
const outputDir = path4.join(outputRoot, "services");
|
|
1438
|
+
const paths = this.extractPaths();
|
|
1439
|
+
const controllerGroups = this.groupPathsByController(paths);
|
|
1440
|
+
Object.entries(controllerGroups).forEach(([controllerName, operations]) => {
|
|
1441
|
+
this.generateServiceFile(controllerName, operations, outputDir);
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
extractPaths() {
|
|
1445
|
+
const paths = [];
|
|
1446
|
+
const swaggerPaths = this.spec.paths || {};
|
|
1447
|
+
Object.entries(swaggerPaths).forEach(([path7, pathItem]) => {
|
|
1448
|
+
const methods = [
|
|
1449
|
+
"get",
|
|
1450
|
+
"post",
|
|
1451
|
+
"put",
|
|
1452
|
+
"patch",
|
|
1453
|
+
"delete",
|
|
1454
|
+
"options",
|
|
1455
|
+
"head"
|
|
1456
|
+
];
|
|
1457
|
+
methods.forEach((method) => {
|
|
1458
|
+
if (pathItem[method]) {
|
|
1459
|
+
const operation = pathItem[method];
|
|
1460
|
+
paths.push({
|
|
1461
|
+
path: path7,
|
|
1462
|
+
method: method.toUpperCase(),
|
|
1463
|
+
operationId: operation.operationId,
|
|
1464
|
+
summary: operation.summary,
|
|
1465
|
+
description: operation.description,
|
|
1466
|
+
tags: operation.tags || [],
|
|
1467
|
+
parameters: this.parseParameters(operation.parameters || [], pathItem.parameters || []),
|
|
1468
|
+
requestBody: operation.requestBody,
|
|
1469
|
+
responses: operation.responses || {}
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
});
|
|
1474
|
+
return paths;
|
|
1475
|
+
}
|
|
1476
|
+
parseParameters(operationParams, pathParams) {
|
|
1477
|
+
const allParams = [
|
|
1478
|
+
...pathParams,
|
|
1479
|
+
...operationParams
|
|
1480
|
+
];
|
|
1481
|
+
return allParams.map((param) => ({
|
|
1482
|
+
name: param.name,
|
|
1483
|
+
in: param.in,
|
|
1484
|
+
required: param.required || param.in === "path",
|
|
1485
|
+
schema: param.schema,
|
|
1486
|
+
type: param.type,
|
|
1487
|
+
format: param.format,
|
|
1488
|
+
description: param.description
|
|
1489
|
+
}));
|
|
1490
|
+
}
|
|
1491
|
+
groupPathsByController(paths) {
|
|
1492
|
+
const groups = {};
|
|
1493
|
+
paths.forEach((path7) => {
|
|
1494
|
+
let controllerName = "Default";
|
|
1495
|
+
if (path7.tags && path7.tags.length > 0) {
|
|
1496
|
+
controllerName = path7.tags[0];
|
|
1497
|
+
} else {
|
|
1498
|
+
const pathParts = path7.path.split("/").filter((p) => p && !p.startsWith("{"));
|
|
1499
|
+
if (pathParts.length > 1) {
|
|
1500
|
+
controllerName = pascalCase(pathParts[1]);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
controllerName = pascalCase(controllerName);
|
|
1504
|
+
if (!groups[controllerName]) {
|
|
1505
|
+
groups[controllerName] = [];
|
|
1506
|
+
}
|
|
1507
|
+
groups[controllerName].push(path7);
|
|
1508
|
+
});
|
|
1509
|
+
return groups;
|
|
1510
|
+
}
|
|
1511
|
+
generateServiceFile(controllerName, operations, outputDir) {
|
|
1512
|
+
const fileName = `${kebabCase(controllerName)}.service.ts`;
|
|
1513
|
+
const filePath = path4.join(outputDir, fileName);
|
|
1514
|
+
const sourceFile = this.project.createSourceFile(filePath, "", {
|
|
1515
|
+
overwrite: true
|
|
1516
|
+
});
|
|
1517
|
+
const usedTypes = this.collectUsedTypes(operations);
|
|
1518
|
+
this.addImports(sourceFile, usedTypes);
|
|
1519
|
+
this.addServiceClass(sourceFile, controllerName, operations);
|
|
1520
|
+
sourceFile.saveSync();
|
|
1521
|
+
}
|
|
1522
|
+
collectUsedTypes(operations) {
|
|
1523
|
+
const usedTypes = /* @__PURE__ */ new Set();
|
|
1524
|
+
operations.forEach((operation) => {
|
|
1525
|
+
operation.parameters?.forEach((param) => {
|
|
1526
|
+
this.collectTypesFromSchema(param.schema || param, usedTypes);
|
|
1527
|
+
});
|
|
1528
|
+
if (operation.requestBody) {
|
|
1529
|
+
this.collectTypesFromRequestBody(operation.requestBody, usedTypes);
|
|
1530
|
+
}
|
|
1531
|
+
if (operation.responses) {
|
|
1532
|
+
Object.values(operation.responses).forEach((response) => {
|
|
1533
|
+
this.collectTypesFromResponse(response, usedTypes);
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
return usedTypes;
|
|
1538
|
+
}
|
|
1539
|
+
collectTypesFromSchema(schema, usedTypes) {
|
|
1540
|
+
if (!schema) return;
|
|
1541
|
+
if (schema.$ref) {
|
|
1542
|
+
const refName = schema.$ref.split("/").pop();
|
|
1543
|
+
if (refName) {
|
|
1544
|
+
usedTypes.add(pascalCase(refName));
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
if (schema.type === "array" && schema.items) {
|
|
1548
|
+
this.collectTypesFromSchema(schema.items, usedTypes);
|
|
1549
|
+
}
|
|
1550
|
+
if (schema.type === "object" && schema.properties) {
|
|
1551
|
+
Object.values(schema.properties).forEach((prop) => {
|
|
1552
|
+
this.collectTypesFromSchema(prop, usedTypes);
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
if (schema.allOf) {
|
|
1556
|
+
schema.allOf.forEach((subSchema) => {
|
|
1557
|
+
this.collectTypesFromSchema(subSchema, usedTypes);
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
if (schema.oneOf) {
|
|
1561
|
+
schema.oneOf.forEach((subSchema) => {
|
|
1562
|
+
this.collectTypesFromSchema(subSchema, usedTypes);
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
if (schema.anyOf) {
|
|
1566
|
+
schema.anyOf.forEach((subSchema) => {
|
|
1567
|
+
this.collectTypesFromSchema(subSchema, usedTypes);
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
collectTypesFromRequestBody(requestBody, usedTypes) {
|
|
1572
|
+
const content = requestBody.content || {};
|
|
1573
|
+
Object.values(content).forEach((mediaType) => {
|
|
1574
|
+
if (mediaType.schema) {
|
|
1575
|
+
this.collectTypesFromSchema(mediaType.schema, usedTypes);
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
collectTypesFromResponse(response, usedTypes) {
|
|
1580
|
+
const content = response.content || {};
|
|
1581
|
+
Object.values(content).forEach((mediaType) => {
|
|
1582
|
+
if (mediaType.schema) {
|
|
1583
|
+
this.collectTypesFromSchema(mediaType.schema, usedTypes);
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
addImports(sourceFile, usedTypes) {
|
|
1588
|
+
sourceFile.addImportDeclarations([
|
|
1589
|
+
{
|
|
1590
|
+
namedImports: [
|
|
1591
|
+
"Injectable",
|
|
1592
|
+
"inject"
|
|
1593
|
+
],
|
|
1594
|
+
moduleSpecifier: "@angular/core"
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
namedImports: [
|
|
1598
|
+
"HttpClient",
|
|
1599
|
+
"HttpParams",
|
|
1600
|
+
"HttpHeaders",
|
|
1601
|
+
"HttpContext",
|
|
1602
|
+
"HttpResponse",
|
|
1603
|
+
"HttpEvent"
|
|
1604
|
+
],
|
|
1605
|
+
moduleSpecifier: "@angular/common/http"
|
|
1606
|
+
},
|
|
1607
|
+
{
|
|
1608
|
+
namedImports: [
|
|
1609
|
+
"Observable"
|
|
1610
|
+
],
|
|
1611
|
+
moduleSpecifier: "rxjs"
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
namedImports: [
|
|
1615
|
+
"BASE_PATH"
|
|
1616
|
+
],
|
|
1617
|
+
moduleSpecifier: "../tokens"
|
|
1618
|
+
}
|
|
1619
|
+
]);
|
|
1620
|
+
if (usedTypes.size > 0) {
|
|
1621
|
+
sourceFile.addImportDeclaration({
|
|
1622
|
+
namedImports: Array.from(usedTypes).sort(),
|
|
1623
|
+
moduleSpecifier: "../models"
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
addServiceClass(sourceFile, controllerName, operations) {
|
|
1628
|
+
const className = `${controllerName}Service`;
|
|
1629
|
+
sourceFile.insertText(0, SERVICE_GENERATOR_HEADER_COMMENT(controllerName));
|
|
1630
|
+
const serviceClass = sourceFile.addClass({
|
|
1631
|
+
name: className,
|
|
1632
|
+
isExported: true,
|
|
1633
|
+
decorators: [
|
|
1634
|
+
{
|
|
1635
|
+
name: "Injectable",
|
|
1636
|
+
arguments: [
|
|
1637
|
+
'{ providedIn: "root" }'
|
|
1638
|
+
]
|
|
1639
|
+
}
|
|
1640
|
+
]
|
|
1641
|
+
});
|
|
1642
|
+
serviceClass.addProperty({
|
|
1643
|
+
name: "httpClient",
|
|
1644
|
+
type: "HttpClient",
|
|
1645
|
+
scope: import_ts_morph3.Scope.Private,
|
|
1646
|
+
isReadonly: true,
|
|
1647
|
+
initializer: "inject(HttpClient)"
|
|
1648
|
+
});
|
|
1649
|
+
serviceClass.addProperty({
|
|
1650
|
+
name: "basePath",
|
|
1651
|
+
type: "string",
|
|
1652
|
+
scope: import_ts_morph3.Scope.Private,
|
|
1653
|
+
isReadonly: true,
|
|
1654
|
+
initializer: "inject(BASE_PATH)"
|
|
1655
|
+
});
|
|
1656
|
+
operations.forEach((operation) => {
|
|
1657
|
+
this.methodGenerator.addServiceMethod(serviceClass, operation);
|
|
1658
|
+
});
|
|
1659
|
+
if (this.hasDuplicateMethodNames(serviceClass.getMethods())) {
|
|
1660
|
+
throw new Error(`Duplicate method names found in service class ${className}. Please ensure unique method names for each operation.`);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
hasDuplicateMethodNames(arr) {
|
|
1664
|
+
return new Set(arr.map((method) => method.getName())).size !== arr.length;
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
|
|
1668
|
+
// src/lib/generators/service/service-index.generator.ts
|
|
1669
|
+
var fs2 = __toESM(require("fs"));
|
|
1670
|
+
var path5 = __toESM(require("path"));
|
|
1671
|
+
var ServiceIndexGenerator = class {
|
|
1672
|
+
static {
|
|
1673
|
+
__name(this, "ServiceIndexGenerator");
|
|
1674
|
+
}
|
|
1675
|
+
project;
|
|
1676
|
+
constructor(project) {
|
|
1677
|
+
this.project = project;
|
|
1678
|
+
}
|
|
1679
|
+
generateIndex(outputRoot) {
|
|
1680
|
+
const servicesDir = path5.join(outputRoot, "services");
|
|
1681
|
+
const indexPath = path5.join(servicesDir, "index.ts");
|
|
1682
|
+
const sourceFile = this.project.createSourceFile(indexPath, "", {
|
|
1683
|
+
overwrite: true
|
|
1684
|
+
});
|
|
1685
|
+
sourceFile.insertText(0, SERVICE_INDEX_GENERATOR_HEADER_COMMENT);
|
|
1686
|
+
const serviceFiles = fs2.readdirSync(servicesDir).filter((file) => file.endsWith(".service.ts")).map((file) => file.replace(".service.ts", ""));
|
|
1687
|
+
serviceFiles.forEach((serviceName) => {
|
|
1688
|
+
const className = pascalCase(serviceName) + "Service";
|
|
1689
|
+
sourceFile.addExportDeclaration({
|
|
1690
|
+
namedExports: [
|
|
1691
|
+
className
|
|
1692
|
+
],
|
|
1693
|
+
moduleSpecifier: `./${serviceName}.service`
|
|
1694
|
+
});
|
|
1695
|
+
});
|
|
1696
|
+
sourceFile.saveSync();
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
|
|
1700
|
+
// src/lib/core/generator.ts
|
|
1701
|
+
var fs3 = __toESM(require("fs"));
|
|
1702
|
+
async function generateFromConfig(config) {
|
|
1703
|
+
if (!fs3.existsSync(config.input)) {
|
|
1704
|
+
throw new Error(`Input file not found: ${config.input}`);
|
|
1705
|
+
}
|
|
1706
|
+
const outputPath = config.output;
|
|
1707
|
+
const generateServices = config.options.generateServices ?? true;
|
|
1708
|
+
if (!fs3.existsSync(outputPath)) {
|
|
1709
|
+
fs3.mkdirSync(outputPath, {
|
|
1710
|
+
recursive: true
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
try {
|
|
1714
|
+
const project = new import_ts_morph4.Project({
|
|
1715
|
+
compilerOptions: {
|
|
1716
|
+
declaration: true,
|
|
1717
|
+
target: import_ts_morph4.ScriptTarget.ES2022,
|
|
1718
|
+
module: import_ts_morph4.ModuleKind.Preserve,
|
|
1719
|
+
strict: true,
|
|
1720
|
+
...config.compilerOptions
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
const typeGenerator = new TypeGenerator(config.input, outputPath, config);
|
|
1724
|
+
typeGenerator.generate();
|
|
1725
|
+
console.log(`\u2705 TypeScript interfaces generated`);
|
|
1726
|
+
if (generateServices) {
|
|
1727
|
+
const tokenGenerator = new TokenGenerator(project);
|
|
1728
|
+
tokenGenerator.generate(outputPath);
|
|
1729
|
+
if (config.options.dateType === "Date") {
|
|
1730
|
+
const dateTransformer = new DateTransformerGenerator(project);
|
|
1731
|
+
dateTransformer.generate(outputPath);
|
|
1732
|
+
console.log(`\u2705 Date transformer generated`);
|
|
1733
|
+
}
|
|
1734
|
+
const fileDownloadHelper = new FileDownloadGenerator(project);
|
|
1735
|
+
fileDownloadHelper.generate(outputPath);
|
|
1736
|
+
console.log(`\u2705 File download helper generated`);
|
|
1737
|
+
const serviceGenerator = new ServiceGenerator(config.input, project, config);
|
|
1738
|
+
serviceGenerator.generate(outputPath);
|
|
1739
|
+
const indexGenerator = new ServiceIndexGenerator(project);
|
|
1740
|
+
indexGenerator.generateIndex(outputPath);
|
|
1741
|
+
console.log(`\u2705 Angular services generated`);
|
|
1742
|
+
}
|
|
1743
|
+
console.log("\u{1F389} Generation completed successfully at:", outputPath);
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
if (error instanceof Error) {
|
|
1746
|
+
console.error("\u274C Error during generation:", error.message);
|
|
1747
|
+
} else {
|
|
1748
|
+
console.error("\u274C Unknown error during generation:", error);
|
|
1749
|
+
}
|
|
1750
|
+
throw error;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
__name(generateFromConfig, "generateFromConfig");
|
|
1754
|
+
|
|
1755
|
+
// src/lib/cli.ts
|
|
1756
|
+
var program = new import_commander.Command();
|
|
1757
|
+
async function loadConfigFile(configPath) {
|
|
1758
|
+
const resolvedPath = path6.resolve(configPath);
|
|
1759
|
+
if (!fs4.existsSync(resolvedPath)) {
|
|
1760
|
+
throw new Error(`Configuration file not found: ${resolvedPath}`);
|
|
1761
|
+
}
|
|
1762
|
+
delete require.cache[require.resolve(resolvedPath)];
|
|
1763
|
+
try {
|
|
1764
|
+
if (resolvedPath.endsWith(".ts")) {
|
|
1765
|
+
require("ts-node/register");
|
|
1766
|
+
}
|
|
1767
|
+
const configModule = require(resolvedPath);
|
|
1768
|
+
const config = configModule.default || configModule.config || configModule;
|
|
1769
|
+
if (!config.input || !config.output) {
|
|
1770
|
+
throw new Error('Configuration must include "input" and "output" properties');
|
|
1771
|
+
}
|
|
1772
|
+
return config;
|
|
1773
|
+
} catch (error) {
|
|
1774
|
+
throw new Error(`Failed to load configuration file: ${error instanceof Error ? error.message : error}`);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
__name(loadConfigFile, "loadConfigFile");
|
|
1778
|
+
async function generateFromOptions(options) {
|
|
1779
|
+
try {
|
|
1780
|
+
if (options.config) {
|
|
1781
|
+
const config = await loadConfigFile(options.config);
|
|
1782
|
+
await generateFromConfig(config);
|
|
1783
|
+
} else if (options.input) {
|
|
1784
|
+
const inputPath = path6.resolve(options.input);
|
|
1785
|
+
if (!fs4.existsSync(inputPath)) {
|
|
1786
|
+
console.error(`Error: Input file not found: ${inputPath}`);
|
|
1787
|
+
process.exit(1);
|
|
1788
|
+
}
|
|
1789
|
+
const config = {
|
|
1790
|
+
input: inputPath,
|
|
1791
|
+
output: options.output || "./src/generated",
|
|
1792
|
+
options: {
|
|
1793
|
+
dateType: options.dateType || "Date",
|
|
1794
|
+
enumStyle: "enum",
|
|
1795
|
+
generateEnumBasedOnDescription: true,
|
|
1796
|
+
generateServices: !options.typesOnly
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
await generateFromConfig(config);
|
|
1800
|
+
} else {
|
|
1801
|
+
console.error("Error: Either --config or --input option is required");
|
|
1802
|
+
program.help();
|
|
1803
|
+
process.exit(1);
|
|
1804
|
+
}
|
|
1805
|
+
console.log("\u2728 Generation completed successfully!");
|
|
1806
|
+
} catch (error) {
|
|
1807
|
+
console.error("\u274C Generation failed:", error instanceof Error ? error.message : error);
|
|
1808
|
+
process.exit(1);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
__name(generateFromOptions, "generateFromOptions");
|
|
1812
|
+
program.name("ng-openapi").description("Generate Angular services and types from Swagger/OpenAPI spec").version("0.0.1").option("-c, --config <path>", "Path to configuration file").option("-i, --input <path>", "Path to Swagger/OpenAPI specification file").option("-o, --output <path>", "Output directory", "./src/generated").option("--types-only", "Generate only TypeScript interfaces").option("--date-type <type>", "Date type to use (string | Date)", "Date").action(async (options) => {
|
|
1813
|
+
await generateFromOptions(options);
|
|
1814
|
+
});
|
|
1815
|
+
program.command("generate").alias("gen").description("Generate code from Swagger specification").option("-c, --config <path>", "Path to configuration file").option("-i, --input <path>", "Path to Swagger/OpenAPI specification file").option("-o, --output <path>", "Output directory", "./src/generated").option("--types-only", "Generate only TypeScript interfaces").option("--date-type <type>", "Date type to use (string | Date)", "Date").action(async (options) => {
|
|
1816
|
+
await generateFromOptions(options);
|
|
1817
|
+
});
|
|
1818
|
+
program.on("--help", () => {
|
|
1819
|
+
console.log("");
|
|
1820
|
+
console.log("Examples:");
|
|
1821
|
+
console.log(" $ ng-openapi -c ./openapi.config.ts");
|
|
1822
|
+
console.log(" $ ng-openapi -i ./swagger.json -o ./src/api");
|
|
1823
|
+
console.log(" $ ng-openapi generate -c ./openapi.config.ts");
|
|
1824
|
+
console.log(" $ ng-openapi generate -i ./api.yaml --types-only");
|
|
1825
|
+
});
|
|
1826
|
+
program.parse();
|
|
1827
|
+
//# sourceMappingURL=cli.cjs.map
|