swallowkit 1.0.0-beta.22 ā 1.0.0-beta.24
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/README.ja.md +9 -4
- package/README.md +9 -4
- package/dist/cli/commands/dev.d.ts +14 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +187 -54
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +33 -18
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +0 -3
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +3 -172
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/core/project/validation.js +2 -2
- package/dist/core/project/validation.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +5 -6
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
- package/dist/core/scaffold/native-schema-generator.js +677 -0
- package/dist/core/scaffold/native-schema-generator.js.map +1 -0
- package/dist/utils/python-uv.d.ts +21 -0
- package/dist/utils/python-uv.d.ts.map +1 -0
- package/dist/utils/python-uv.js +112 -0
- package/dist/utils/python-uv.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/dev.test.ts +95 -0
- package/src/__tests__/model-parser.test.ts +44 -64
- package/src/__tests__/python-uv.test.ts +48 -0
- package/src/__tests__/scaffold.test.ts +54 -26
- package/src/cli/commands/dev.ts +258 -74
- package/src/cli/commands/init.ts +45 -19
- package/src/cli/commands/scaffold.ts +3 -213
- package/src/core/project/validation.ts +2 -2
- package/src/core/scaffold/model-parser.ts +7 -7
- package/src/core/scaffold/native-schema-generator.ts +798 -0
- package/src/utils/python-uv.ts +97 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PYTHON_SCHEMA_CODEGEN_REQUIREMENT = exports.NSWAG_CONSOLECORE_VERSION = void 0;
|
|
37
|
+
exports.buildCSharpCodegenToolManifestSource = buildCSharpCodegenToolManifestSource;
|
|
38
|
+
exports.buildPythonCodegenRequirementsSource = buildPythonCodegenRequirementsSource;
|
|
39
|
+
exports.getCSharpSchemaModelPath = getCSharpSchemaModelPath;
|
|
40
|
+
exports.getCSharpSchemaOptionPath = getCSharpSchemaOptionPath;
|
|
41
|
+
exports.getPythonSchemaModelPath = getPythonSchemaModelPath;
|
|
42
|
+
exports.getCSharpNativeGeneratorArgs = getCSharpNativeGeneratorArgs;
|
|
43
|
+
exports.getPythonNativeGeneratorArgs = getPythonNativeGeneratorArgs;
|
|
44
|
+
exports.generateLanguageSchemaArtifacts = generateLanguageSchemaArtifacts;
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const child_process_1 = require("child_process");
|
|
48
|
+
const model_parser_1 = require("./model-parser");
|
|
49
|
+
const openapi_generator_1 = require("./openapi-generator");
|
|
50
|
+
const python_uv_1 = require("../../utils/python-uv");
|
|
51
|
+
exports.NSWAG_CONSOLECORE_VERSION = "14.7.1";
|
|
52
|
+
exports.PYTHON_SCHEMA_CODEGEN_REQUIREMENT = "datamodel-code-generator>=0.44.0,<1.0.0";
|
|
53
|
+
const PYTHON_OUTPUT_MODEL_TYPE = "pydantic_v2.BaseModel";
|
|
54
|
+
function getMachineAwareStdio() {
|
|
55
|
+
return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
|
|
56
|
+
}
|
|
57
|
+
function canRun(command, args, cwd, env) {
|
|
58
|
+
const result = (0, child_process_1.spawnSync)(command, args, {
|
|
59
|
+
cwd,
|
|
60
|
+
env,
|
|
61
|
+
stdio: "ignore",
|
|
62
|
+
});
|
|
63
|
+
return !result.error && result.status === 0;
|
|
64
|
+
}
|
|
65
|
+
async function runCommand(command, args, cwd, errorMessage, env) {
|
|
66
|
+
await new Promise((resolve, reject) => {
|
|
67
|
+
const child = (0, child_process_1.spawn)(command, args, {
|
|
68
|
+
cwd,
|
|
69
|
+
env,
|
|
70
|
+
stdio: getMachineAwareStdio(),
|
|
71
|
+
});
|
|
72
|
+
child.on("close", (code) => {
|
|
73
|
+
if (code === 0) {
|
|
74
|
+
resolve();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
reject(new Error(`${errorMessage} (${command} ${args.join(" ")}) exited with code ${code}`));
|
|
78
|
+
});
|
|
79
|
+
child.on("error", (error) => reject(new Error(`${errorMessage}: ${error.message}`)));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function buildCSharpCodegenToolManifestSource() {
|
|
83
|
+
return `${JSON.stringify({
|
|
84
|
+
version: 1,
|
|
85
|
+
isRoot: true,
|
|
86
|
+
tools: {
|
|
87
|
+
"nswag.consolecore": {
|
|
88
|
+
version: exports.NSWAG_CONSOLECORE_VERSION,
|
|
89
|
+
commands: ["nswag"],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
}, null, 2)}\n`;
|
|
93
|
+
}
|
|
94
|
+
function buildPythonCodegenRequirementsSource() {
|
|
95
|
+
return `${exports.PYTHON_SCHEMA_CODEGEN_REQUIREMENT}\n`;
|
|
96
|
+
}
|
|
97
|
+
function toSnakeCase(value) {
|
|
98
|
+
return value
|
|
99
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
100
|
+
.replace(/[-\s]+/g, "_")
|
|
101
|
+
.toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
function toPascalIdentifier(value) {
|
|
104
|
+
if (value.includes("-") || value.includes("_")) {
|
|
105
|
+
return value
|
|
106
|
+
.split(/[-_]/)
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
109
|
+
.join("");
|
|
110
|
+
}
|
|
111
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
112
|
+
}
|
|
113
|
+
function isDateLikeField(field) {
|
|
114
|
+
return field.type === "date" || (field.type === "string" && field.name.toLowerCase().endsWith("at"));
|
|
115
|
+
}
|
|
116
|
+
function getCSharpSchemaModelPath(outputDir, modelName) {
|
|
117
|
+
return path.join(outputDir, "src", "SwallowKitBackendModels", "Model", `${modelName}.cs`);
|
|
118
|
+
}
|
|
119
|
+
function getCSharpSchemaOptionPath(outputDir) {
|
|
120
|
+
return path.join(outputDir, "src", "SwallowKitBackendModels", "Client", "Option.cs");
|
|
121
|
+
}
|
|
122
|
+
function getPythonSchemaModelPath(outputDir, modelName) {
|
|
123
|
+
return path.join(outputDir, "backend_models", "models", `${toSnakeCase(modelName)}.py`);
|
|
124
|
+
}
|
|
125
|
+
function getCSharpNativeGeneratorArgs(specPath, outputPath) {
|
|
126
|
+
return [
|
|
127
|
+
"tool",
|
|
128
|
+
"run",
|
|
129
|
+
"nswag",
|
|
130
|
+
"openapi2csclient",
|
|
131
|
+
`/input:${specPath}`,
|
|
132
|
+
`/output:${outputPath}`,
|
|
133
|
+
"/namespace:SwallowKitBackendModels",
|
|
134
|
+
"/GenerateClientClasses:false",
|
|
135
|
+
"/GenerateClientInterfaces:false",
|
|
136
|
+
"/GenerateResponseClasses:false",
|
|
137
|
+
"/GenerateExceptionClasses:false",
|
|
138
|
+
"/GenerateDtoTypes:true",
|
|
139
|
+
"/GenerateNullableReferenceTypes:true",
|
|
140
|
+
"/GenerateOptionalPropertiesAsNullable:true",
|
|
141
|
+
"/JsonLibrary:SystemTextJson",
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
function getPythonNativeGeneratorArgs(specPath, outputPath) {
|
|
145
|
+
return [
|
|
146
|
+
"-m",
|
|
147
|
+
"datamodel_code_generator",
|
|
148
|
+
"--input",
|
|
149
|
+
specPath,
|
|
150
|
+
"--input-file-type",
|
|
151
|
+
"openapi",
|
|
152
|
+
"--output",
|
|
153
|
+
outputPath,
|
|
154
|
+
"--output-model-type",
|
|
155
|
+
PYTHON_OUTPUT_MODEL_TYPE,
|
|
156
|
+
"--target-python-version",
|
|
157
|
+
"3.11",
|
|
158
|
+
"--disable-timestamp",
|
|
159
|
+
"--use-union-operator",
|
|
160
|
+
"--collapse-root-models",
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
function getCSharpFieldBaseType(field) {
|
|
164
|
+
if (field.isNestedSchema && field.nestedModelName) {
|
|
165
|
+
return field.nestedModelName;
|
|
166
|
+
}
|
|
167
|
+
if (field.enumValues?.length) {
|
|
168
|
+
return `${toPascalIdentifier(field.name)}Enum`;
|
|
169
|
+
}
|
|
170
|
+
if (field.isArray) {
|
|
171
|
+
return `List<${getCSharpArrayElementType(field)}>`;
|
|
172
|
+
}
|
|
173
|
+
switch (field.type) {
|
|
174
|
+
case "string":
|
|
175
|
+
return isDateLikeField(field) ? "DateTime" : "string";
|
|
176
|
+
case "number":
|
|
177
|
+
return "decimal";
|
|
178
|
+
case "boolean":
|
|
179
|
+
return "bool";
|
|
180
|
+
case "date":
|
|
181
|
+
return "DateTime";
|
|
182
|
+
case "object":
|
|
183
|
+
return "Dictionary<string, object>";
|
|
184
|
+
default:
|
|
185
|
+
return "object";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function getCSharpArrayElementType(field) {
|
|
189
|
+
if (field.isNestedSchema && field.nestedModelName) {
|
|
190
|
+
return field.nestedModelName;
|
|
191
|
+
}
|
|
192
|
+
if (field.enumValues?.length) {
|
|
193
|
+
return `${toPascalIdentifier(field.name)}Enum`;
|
|
194
|
+
}
|
|
195
|
+
switch (field.type) {
|
|
196
|
+
case "string":
|
|
197
|
+
return isDateLikeField(field) ? "DateTime" : "string";
|
|
198
|
+
case "number":
|
|
199
|
+
return "decimal";
|
|
200
|
+
case "boolean":
|
|
201
|
+
return "bool";
|
|
202
|
+
case "date":
|
|
203
|
+
return "DateTime";
|
|
204
|
+
case "object":
|
|
205
|
+
return "Dictionary<string, object>";
|
|
206
|
+
default:
|
|
207
|
+
return "object";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function getCSharpPropertyType(field) {
|
|
211
|
+
const baseType = getCSharpFieldBaseType(field);
|
|
212
|
+
return field.isOptional ? `${baseType}?` : baseType;
|
|
213
|
+
}
|
|
214
|
+
function getCSharpOptionType(field) {
|
|
215
|
+
return `Option<${getCSharpPropertyType(field)}>`;
|
|
216
|
+
}
|
|
217
|
+
function generateLegacyCompatibleOptionSource() {
|
|
218
|
+
return `// <auto-generated>
|
|
219
|
+
// Minimal Option<T> for OpenAPI Generator model compatibility.
|
|
220
|
+
// Full client supporting files are excluded to avoid Polly version conflicts.
|
|
221
|
+
// </auto-generated>
|
|
222
|
+
|
|
223
|
+
#nullable enable
|
|
224
|
+
|
|
225
|
+
namespace SwallowKitBackendModels.Client
|
|
226
|
+
{
|
|
227
|
+
/// <summary>
|
|
228
|
+
/// A wrapper for nullable/optional properties generated by OpenAPI Generator.
|
|
229
|
+
/// Tracks whether a value has been explicitly set (distinguishing null from absent).
|
|
230
|
+
/// </summary>
|
|
231
|
+
public readonly struct Option<TValue>
|
|
232
|
+
{
|
|
233
|
+
/// <summary>Whether this option has been explicitly set.</summary>
|
|
234
|
+
public bool IsSet { get; }
|
|
235
|
+
|
|
236
|
+
/// <summary>The contained value (may be default if not set).</summary>
|
|
237
|
+
public TValue Value { get; }
|
|
238
|
+
|
|
239
|
+
/// <summary>Create an Option with an explicit value.</summary>
|
|
240
|
+
public Option(TValue value)
|
|
241
|
+
{
|
|
242
|
+
IsSet = true;
|
|
243
|
+
Value = value;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// <summary>Implicit conversion from Option to its inner value.</summary>
|
|
247
|
+
public static implicit operator TValue(Option<TValue> option) => option.Value;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
`;
|
|
251
|
+
}
|
|
252
|
+
function buildCSharpEnumMembers(values) {
|
|
253
|
+
return values
|
|
254
|
+
.map((value, index) => ` ${toPascalIdentifier(value)} = ${index + 1}`)
|
|
255
|
+
.join(",\n\n");
|
|
256
|
+
}
|
|
257
|
+
function buildCSharpEnumFromStringCases(field, nullable) {
|
|
258
|
+
const enumType = `${toPascalIdentifier(field.name)}Enum`;
|
|
259
|
+
return field.enumValues
|
|
260
|
+
.map((value) => ` if (value.Equals("${value}", StringComparison.Ordinal))\n return ${enumType}.${toPascalIdentifier(value)};`)
|
|
261
|
+
.join("\n\n") + (nullable ? `\n\n return null;` : `\n\n throw new NotImplementedException($"Could not convert value to type ${enumType}: '{value}'");`);
|
|
262
|
+
}
|
|
263
|
+
function buildCSharpEnumToJsonCases(field) {
|
|
264
|
+
const enumType = `${toPascalIdentifier(field.name)}Enum`;
|
|
265
|
+
return field.enumValues
|
|
266
|
+
.map((value) => ` if (value == ${enumType}.${toPascalIdentifier(value)})\n return "${value}";`)
|
|
267
|
+
.join("\n\n");
|
|
268
|
+
}
|
|
269
|
+
function generateLegacyCompatibleCSharpModelSource(model) {
|
|
270
|
+
const requiredFields = model.fields.filter((field) => !field.isOptional);
|
|
271
|
+
const optionalFields = model.fields.filter((field) => field.isOptional);
|
|
272
|
+
const constructorParams = [
|
|
273
|
+
...requiredFields.map((field) => `${getCSharpFieldBaseType(field)} ${field.name}`),
|
|
274
|
+
...optionalFields.map((field) => `${getCSharpOptionType(field)} ${field.name} = default`),
|
|
275
|
+
].join(", ");
|
|
276
|
+
const constructorAssignments = [
|
|
277
|
+
...requiredFields.map((field) => ` ${toPascalIdentifier(field.name)} = ${field.name};`),
|
|
278
|
+
...optionalFields.map((field) => ` ${toPascalIdentifier(field.name)}Option = ${field.name};`),
|
|
279
|
+
" OnCreated();",
|
|
280
|
+
].join("\n");
|
|
281
|
+
const enumBlocks = model.fields
|
|
282
|
+
.filter((field) => field.enumValues?.length)
|
|
283
|
+
.map((field) => {
|
|
284
|
+
const enumType = `${toPascalIdentifier(field.name)}Enum`;
|
|
285
|
+
return ` /// <summary>
|
|
286
|
+
/// Defines ${toPascalIdentifier(field.name)}
|
|
287
|
+
/// </summary>
|
|
288
|
+
[JsonConverter(typeof(JsonStringEnumConverter))]
|
|
289
|
+
public enum ${enumType}
|
|
290
|
+
{
|
|
291
|
+
${buildCSharpEnumMembers(field.enumValues)}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
public static ${enumType} ${enumType}FromString(string value)
|
|
295
|
+
{
|
|
296
|
+
${buildCSharpEnumFromStringCases(field, false)}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public static ${enumType}? ${enumType}FromStringOrDefault(string value)
|
|
300
|
+
{
|
|
301
|
+
${buildCSharpEnumFromStringCases(field, true)}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
public static string ${enumType}ToJsonValue(${enumType}? value)
|
|
305
|
+
{
|
|
306
|
+
${buildCSharpEnumToJsonCases(field)}
|
|
307
|
+
|
|
308
|
+
throw new NotImplementedException($"Value could not be handled: '{value}'");
|
|
309
|
+
}`;
|
|
310
|
+
})
|
|
311
|
+
.join("\n\n");
|
|
312
|
+
const propertyBlocks = model.fields
|
|
313
|
+
.map((field) => {
|
|
314
|
+
const propertyName = toPascalIdentifier(field.name);
|
|
315
|
+
const propertyType = getCSharpPropertyType(field);
|
|
316
|
+
if (!field.isOptional) {
|
|
317
|
+
return ` /// <summary>
|
|
318
|
+
/// Gets or Sets ${propertyName}
|
|
319
|
+
/// </summary>
|
|
320
|
+
[JsonPropertyName("${field.name}")]
|
|
321
|
+
public ${propertyType} ${propertyName} { get; set; }`;
|
|
322
|
+
}
|
|
323
|
+
return ` /// <summary>
|
|
324
|
+
/// Used to track the state of ${propertyName}
|
|
325
|
+
/// </summary>
|
|
326
|
+
[JsonIgnore]
|
|
327
|
+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
|
328
|
+
public ${getCSharpOptionType(field)} ${propertyName}Option { get; private set; }
|
|
329
|
+
|
|
330
|
+
/// <summary>
|
|
331
|
+
/// Gets or Sets ${propertyName}
|
|
332
|
+
/// </summary>
|
|
333
|
+
[JsonPropertyName("${field.name}")]
|
|
334
|
+
public ${propertyType} ${propertyName} { get { return this.${propertyName}Option; } set { this.${propertyName}Option = new(value); } }`;
|
|
335
|
+
})
|
|
336
|
+
.join("\n\n");
|
|
337
|
+
const toStringBody = model.fields
|
|
338
|
+
.map((field) => ` sb.Append(" ${toPascalIdentifier(field.name)}: ").Append(${toPascalIdentifier(field.name)}).Append("\\n");`)
|
|
339
|
+
.join("\n");
|
|
340
|
+
return `// <auto-generated>
|
|
341
|
+
/*
|
|
342
|
+
* ${model.name} API
|
|
343
|
+
*
|
|
344
|
+
* Generated from SwallowKit Zod model metadata.
|
|
345
|
+
*
|
|
346
|
+
* The version of the OpenAPI document: 1.0.0
|
|
347
|
+
* Generated by native SwallowKit schema compatibility layer
|
|
348
|
+
*/
|
|
349
|
+
|
|
350
|
+
#nullable enable
|
|
351
|
+
|
|
352
|
+
using System;
|
|
353
|
+
using System.Collections.Generic;
|
|
354
|
+
using System.ComponentModel.DataAnnotations;
|
|
355
|
+
using System.Text;
|
|
356
|
+
using System.Text.Json.Serialization;
|
|
357
|
+
using SwallowKitBackendModels.Client;
|
|
358
|
+
|
|
359
|
+
namespace SwallowKitBackendModels.Model
|
|
360
|
+
{
|
|
361
|
+
/// <summary>
|
|
362
|
+
/// ${model.name}
|
|
363
|
+
/// </summary>
|
|
364
|
+
public partial class ${model.name} : IValidatableObject
|
|
365
|
+
{
|
|
366
|
+
[JsonConstructor]
|
|
367
|
+
public ${model.name}(${constructorParams})
|
|
368
|
+
{
|
|
369
|
+
${constructorAssignments}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
partial void OnCreated();
|
|
373
|
+
${enumBlocks ? `\n\n${enumBlocks}` : ""}
|
|
374
|
+
|
|
375
|
+
${propertyBlocks}
|
|
376
|
+
|
|
377
|
+
public override string ToString()
|
|
378
|
+
{
|
|
379
|
+
var sb = new StringBuilder();
|
|
380
|
+
sb.Append("class ${model.name} {\\n");
|
|
381
|
+
${toStringBody}
|
|
382
|
+
sb.Append("}\\n");
|
|
383
|
+
return sb.ToString();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
IEnumerable<ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
|
|
387
|
+
{
|
|
388
|
+
yield break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
`;
|
|
393
|
+
}
|
|
394
|
+
function getPythonTypeName(field) {
|
|
395
|
+
if (field.isNestedSchema && field.nestedModelName) {
|
|
396
|
+
return field.nestedModelName;
|
|
397
|
+
}
|
|
398
|
+
if (field.isArray) {
|
|
399
|
+
return `List[${getPythonArrayElementType(field)}]`;
|
|
400
|
+
}
|
|
401
|
+
switch (field.type) {
|
|
402
|
+
case "string":
|
|
403
|
+
return isDateLikeField(field) ? "datetime" : "StrictStr";
|
|
404
|
+
case "number":
|
|
405
|
+
return "Union[StrictFloat, StrictInt]";
|
|
406
|
+
case "boolean":
|
|
407
|
+
return "StrictBool";
|
|
408
|
+
case "date":
|
|
409
|
+
return "datetime";
|
|
410
|
+
case "object":
|
|
411
|
+
return "Dict[str, Any]";
|
|
412
|
+
default:
|
|
413
|
+
return "Any";
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function getPythonArrayElementType(field) {
|
|
417
|
+
if (field.isNestedSchema && field.nestedModelName) {
|
|
418
|
+
return field.nestedModelName;
|
|
419
|
+
}
|
|
420
|
+
switch (field.type) {
|
|
421
|
+
case "string":
|
|
422
|
+
return isDateLikeField(field) ? "datetime" : "StrictStr";
|
|
423
|
+
case "number":
|
|
424
|
+
return "Union[StrictFloat, StrictInt]";
|
|
425
|
+
case "boolean":
|
|
426
|
+
return "StrictBool";
|
|
427
|
+
case "date":
|
|
428
|
+
return "datetime";
|
|
429
|
+
case "object":
|
|
430
|
+
return "Dict[str, Any]";
|
|
431
|
+
default:
|
|
432
|
+
return "Any";
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function buildPythonFieldDeclaration(field) {
|
|
436
|
+
const pythonName = toSnakeCase(field.name);
|
|
437
|
+
const typeName = field.isOptional ? `Optional[${getPythonTypeName(field)}]` : getPythonTypeName(field);
|
|
438
|
+
const aliasSuffix = pythonName !== field.name ? `, alias="${field.name}"` : "";
|
|
439
|
+
if (field.isOptional) {
|
|
440
|
+
return `${pythonName}: ${typeName} = Field(default=None${aliasSuffix})`;
|
|
441
|
+
}
|
|
442
|
+
if (aliasSuffix) {
|
|
443
|
+
return `${pythonName}: ${typeName} = Field(${aliasSuffix.slice(2)})`;
|
|
444
|
+
}
|
|
445
|
+
return `${pythonName}: ${typeName}`;
|
|
446
|
+
}
|
|
447
|
+
function buildPythonEnumValidators(model) {
|
|
448
|
+
return model.fields
|
|
449
|
+
.filter((field) => field.enumValues?.length)
|
|
450
|
+
.map((field) => {
|
|
451
|
+
const pythonName = toSnakeCase(field.name);
|
|
452
|
+
const enumSet = field.enumValues.map((value) => `'${value}'`).join(", ");
|
|
453
|
+
return ` @field_validator('${pythonName}')
|
|
454
|
+
def ${pythonName}_validate_enum(cls, value):
|
|
455
|
+
"""Validates the enum"""
|
|
456
|
+
if value is None:
|
|
457
|
+
return value
|
|
458
|
+
|
|
459
|
+
if value not in set([${enumSet}]):
|
|
460
|
+
raise ValueError("must be one of enum values (${field.enumValues.map((value) => `'${value}'`).join(", ")})")
|
|
461
|
+
return value`;
|
|
462
|
+
})
|
|
463
|
+
.join("\n\n");
|
|
464
|
+
}
|
|
465
|
+
function buildPythonModelImports(model) {
|
|
466
|
+
const nestedImports = Array.from(new Set(model.fields
|
|
467
|
+
.filter((field) => field.isNestedSchema && field.nestedModelName)
|
|
468
|
+
.map((field) => field.nestedModelName)))
|
|
469
|
+
.map((modelName) => `from .${toSnakeCase(modelName)} import ${modelName}`)
|
|
470
|
+
.join("\n");
|
|
471
|
+
return nestedImports ? `${nestedImports}\n\n` : "";
|
|
472
|
+
}
|
|
473
|
+
function generateLegacyCompatiblePythonModelSource(model) {
|
|
474
|
+
const fieldDeclarations = model.fields.map((field) => ` ${buildPythonFieldDeclaration(field)}`).join("\n");
|
|
475
|
+
const propertyNames = model.fields.map((field) => `"${field.name}"`).join(", ");
|
|
476
|
+
const validators = buildPythonEnumValidators(model);
|
|
477
|
+
const dictAssignments = model.fields
|
|
478
|
+
.map((field) => ` "${field.name}": obj.get("${field.name}")`)
|
|
479
|
+
.join(",\n");
|
|
480
|
+
return `# coding: utf-8
|
|
481
|
+
|
|
482
|
+
"""
|
|
483
|
+
${model.name} API
|
|
484
|
+
|
|
485
|
+
Generated from SwallowKit Zod model metadata.
|
|
486
|
+
|
|
487
|
+
The version of the OpenAPI document: 1.0.0
|
|
488
|
+
Generated by native SwallowKit schema compatibility layer
|
|
489
|
+
|
|
490
|
+
Do not edit the class manually.
|
|
491
|
+
""" # noqa: E501
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
from __future__ import annotations
|
|
495
|
+
import pprint
|
|
496
|
+
import re # noqa: F401
|
|
497
|
+
import json
|
|
498
|
+
|
|
499
|
+
from datetime import datetime
|
|
500
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictFloat, StrictInt, StrictStr, field_validator
|
|
501
|
+
from typing import Any, ClassVar, Dict, List, Optional, Set, Union
|
|
502
|
+
from typing_extensions import Self
|
|
503
|
+
|
|
504
|
+
${buildPythonModelImports(model)}class ${model.name}(BaseModel):
|
|
505
|
+
"""
|
|
506
|
+
${model.name}
|
|
507
|
+
""" # noqa: E501
|
|
508
|
+
${fieldDeclarations}
|
|
509
|
+
__properties: ClassVar[List[str]] = [${propertyNames}]
|
|
510
|
+
${validators ? `\n\n${validators}` : ""}
|
|
511
|
+
|
|
512
|
+
model_config = ConfigDict(
|
|
513
|
+
populate_by_name=True,
|
|
514
|
+
validate_assignment=True,
|
|
515
|
+
protected_namespaces=(),
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def to_str(self) -> str:
|
|
520
|
+
"""Returns the string representation of the model using alias"""
|
|
521
|
+
return pprint.pformat(self.model_dump(by_alias=True))
|
|
522
|
+
|
|
523
|
+
def to_json(self) -> str:
|
|
524
|
+
"""Returns the JSON representation of the model using alias"""
|
|
525
|
+
return json.dumps(self.to_dict())
|
|
526
|
+
|
|
527
|
+
@classmethod
|
|
528
|
+
def from_json(cls, json_str: str) -> Optional[Self]:
|
|
529
|
+
"""Create an instance of ${model.name} from a JSON string"""
|
|
530
|
+
return cls.from_dict(json.loads(json_str))
|
|
531
|
+
|
|
532
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
533
|
+
"""Return the dictionary representation of the model using alias."""
|
|
534
|
+
excluded_fields: Set[str] = set([
|
|
535
|
+
])
|
|
536
|
+
|
|
537
|
+
_dict = self.model_dump(
|
|
538
|
+
by_alias=True,
|
|
539
|
+
exclude=excluded_fields,
|
|
540
|
+
exclude_none=True,
|
|
541
|
+
)
|
|
542
|
+
return _dict
|
|
543
|
+
|
|
544
|
+
@classmethod
|
|
545
|
+
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
|
546
|
+
"""Create an instance of ${model.name} from a dict"""
|
|
547
|
+
if obj is None:
|
|
548
|
+
return None
|
|
549
|
+
|
|
550
|
+
if not isinstance(obj, dict):
|
|
551
|
+
return cls.model_validate(obj)
|
|
552
|
+
|
|
553
|
+
_obj = cls.model_validate({
|
|
554
|
+
${dictAssignments}
|
|
555
|
+
})
|
|
556
|
+
return _obj
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
`;
|
|
560
|
+
}
|
|
561
|
+
function buildGeneratedPythonPackageInitSource(models) {
|
|
562
|
+
return models.map((model) => `from .models.${toSnakeCase(model.name)} import ${model.name}`).join("\n") + "\n";
|
|
563
|
+
}
|
|
564
|
+
function buildGeneratedPythonModelsInitSource(models) {
|
|
565
|
+
return models.map((model) => `from .${toSnakeCase(model.name)} import ${model.name}`).join("\n") + "\n";
|
|
566
|
+
}
|
|
567
|
+
function ensureCSharpCodegenProjectFiles(functionsRoot) {
|
|
568
|
+
const toolManifestPath = path.join(functionsRoot, ".config", "dotnet-tools.json");
|
|
569
|
+
fs.mkdirSync(path.dirname(toolManifestPath), { recursive: true });
|
|
570
|
+
if (!fs.existsSync(toolManifestPath)) {
|
|
571
|
+
fs.writeFileSync(toolManifestPath, buildCSharpCodegenToolManifestSource(), "utf-8");
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function ensurePythonCodegenProjectFiles(functionsRoot) {
|
|
575
|
+
const requirementsPath = path.join(functionsRoot, "requirements.codegen.txt");
|
|
576
|
+
if (!fs.existsSync(requirementsPath)) {
|
|
577
|
+
fs.writeFileSync(requirementsPath, buildPythonCodegenRequirementsSource(), "utf-8");
|
|
578
|
+
}
|
|
579
|
+
return requirementsPath;
|
|
580
|
+
}
|
|
581
|
+
function getVirtualEnvPythonPath(venvDir) {
|
|
582
|
+
return process.platform === "win32"
|
|
583
|
+
? path.join(venvDir, "Scripts", "python.exe")
|
|
584
|
+
: path.join(venvDir, "bin", "python");
|
|
585
|
+
}
|
|
586
|
+
async function ensureProjectLocalUvCommand(projectRoot) {
|
|
587
|
+
const uvEnv = (0, python_uv_1.buildProjectLocalUvEnv)(process.env, projectRoot);
|
|
588
|
+
const { localUvExecutable } = (0, python_uv_1.getProjectLocalUvPaths)(projectRoot);
|
|
589
|
+
if (canRun("uv", ["--version"], projectRoot)) {
|
|
590
|
+
return { command: "uv", env: uvEnv };
|
|
591
|
+
}
|
|
592
|
+
if (fs.existsSync(localUvExecutable) && canRun(localUvExecutable, ["--version"], projectRoot)) {
|
|
593
|
+
return { command: localUvExecutable, env: uvEnv };
|
|
594
|
+
}
|
|
595
|
+
const installer = (0, python_uv_1.getProjectLocalUvInstallerCommand)();
|
|
596
|
+
await runCommand(installer.command, installer.args, projectRoot, "Failed to install project-local uv.", (0, python_uv_1.buildProjectLocalUvInstallerEnv)(process.env, projectRoot));
|
|
597
|
+
if (!(fs.existsSync(localUvExecutable) && canRun(localUvExecutable, ["--version"], projectRoot))) {
|
|
598
|
+
throw new Error("Failed to install project-local uv.");
|
|
599
|
+
}
|
|
600
|
+
return { command: localUvExecutable, env: uvEnv };
|
|
601
|
+
}
|
|
602
|
+
async function ensurePythonCodegenEnvironment(functionsRoot) {
|
|
603
|
+
const requirementsPath = ensurePythonCodegenProjectFiles(functionsRoot);
|
|
604
|
+
const projectRoot = (0, python_uv_1.getPythonProjectRoot)(functionsRoot);
|
|
605
|
+
const { command: uvCommand, env: uvEnv } = await ensureProjectLocalUvCommand(projectRoot);
|
|
606
|
+
const venvDir = path.join(functionsRoot, ".codegen-venv");
|
|
607
|
+
const venvPython = getVirtualEnvPythonPath(venvDir);
|
|
608
|
+
if (!canRun(venvPython, ["--version"], functionsRoot, uvEnv)) {
|
|
609
|
+
const venvArgs = (0, python_uv_1.buildUvVenvArgs)(venvDir);
|
|
610
|
+
if (fs.existsSync(venvDir)) {
|
|
611
|
+
venvArgs.push("--clear");
|
|
612
|
+
}
|
|
613
|
+
await runCommand(uvCommand, venvArgs, functionsRoot, "Failed to create the Python schema code generation virtual environment.", uvEnv);
|
|
614
|
+
}
|
|
615
|
+
if (!canRun(venvPython, ["-c", "import datamodel_code_generator"], functionsRoot, uvEnv)) {
|
|
616
|
+
await runCommand(uvCommand, (0, python_uv_1.buildUvPipInstallArgs)(venvPython, requirementsPath), functionsRoot, "Failed to install Python schema generation dependencies.", uvEnv);
|
|
617
|
+
}
|
|
618
|
+
return venvPython;
|
|
619
|
+
}
|
|
620
|
+
async function generateCSharpSchemaArtifacts(models, specPath, outputDir, functionsRoot) {
|
|
621
|
+
ensureCSharpCodegenProjectFiles(functionsRoot);
|
|
622
|
+
if (!canRun("dotnet", ["--version"], functionsRoot)) {
|
|
623
|
+
throw new Error("The .NET SDK is required to generate C# backend schema assets.\n" +
|
|
624
|
+
"Install the .NET 8 SDK and retry.");
|
|
625
|
+
}
|
|
626
|
+
const tempContractsPath = path.join(outputDir, ".native-temp", "Contracts.cs");
|
|
627
|
+
fs.mkdirSync(path.dirname(tempContractsPath), { recursive: true });
|
|
628
|
+
await runCommand("dotnet", ["tool", "restore"], functionsRoot, "Failed to restore the NSwag dotnet tool.");
|
|
629
|
+
await runCommand("dotnet", getCSharpNativeGeneratorArgs(specPath, tempContractsPath), functionsRoot, "NSwag failed to generate C# backend schema assets.");
|
|
630
|
+
fs.rmSync(path.dirname(tempContractsPath), { recursive: true, force: true });
|
|
631
|
+
const optionPath = getCSharpSchemaOptionPath(outputDir);
|
|
632
|
+
fs.mkdirSync(path.dirname(optionPath), { recursive: true });
|
|
633
|
+
fs.writeFileSync(optionPath, generateLegacyCompatibleOptionSource(), "utf-8");
|
|
634
|
+
for (const model of models) {
|
|
635
|
+
const modelPath = getCSharpSchemaModelPath(outputDir, model.name);
|
|
636
|
+
fs.mkdirSync(path.dirname(modelPath), { recursive: true });
|
|
637
|
+
fs.writeFileSync(modelPath, generateLegacyCompatibleCSharpModelSource(model), "utf-8");
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async function generatePythonSchemaArtifacts(models, specPath, outputDir, functionsRoot) {
|
|
641
|
+
const pythonExecutable = await ensurePythonCodegenEnvironment(functionsRoot);
|
|
642
|
+
const tempModelsPath = path.join(outputDir, ".native-temp", "models.py");
|
|
643
|
+
fs.mkdirSync(path.dirname(tempModelsPath), { recursive: true });
|
|
644
|
+
await runCommand(pythonExecutable, getPythonNativeGeneratorArgs(specPath, tempModelsPath), functionsRoot, "datamodel-code-generator failed to generate Python backend schema assets.");
|
|
645
|
+
fs.rmSync(path.dirname(tempModelsPath), { recursive: true, force: true });
|
|
646
|
+
const packageRoot = path.join(outputDir, "backend_models");
|
|
647
|
+
const modelsRoot = path.join(packageRoot, "models");
|
|
648
|
+
fs.mkdirSync(modelsRoot, { recursive: true });
|
|
649
|
+
fs.writeFileSync(path.join(packageRoot, "__init__.py"), buildGeneratedPythonPackageInitSource(models), "utf-8");
|
|
650
|
+
fs.writeFileSync(path.join(modelsRoot, "__init__.py"), buildGeneratedPythonModelsInitSource(models), "utf-8");
|
|
651
|
+
for (const model of models) {
|
|
652
|
+
const modelPath = getPythonSchemaModelPath(outputDir, model.name);
|
|
653
|
+
fs.mkdirSync(path.dirname(modelPath), { recursive: true });
|
|
654
|
+
fs.writeFileSync(modelPath, generateLegacyCompatiblePythonModelSource(model), "utf-8");
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async function generateLanguageSchemaArtifacts(models, rootModel, functionsDir, backendLanguage) {
|
|
658
|
+
console.log("\n𧬠Generating OpenAPI export and native schema assets...");
|
|
659
|
+
const projectRoot = process.cwd();
|
|
660
|
+
const functionsRoot = path.join(projectRoot, functionsDir);
|
|
661
|
+
const openApiDir = path.join(functionsRoot, "openapi");
|
|
662
|
+
fs.mkdirSync(openApiDir, { recursive: true });
|
|
663
|
+
const specPath = path.join(openApiDir, `${(0, model_parser_1.toKebabCase)(rootModel.name)}.openapi.json`);
|
|
664
|
+
fs.writeFileSync(specPath, (0, openapi_generator_1.generateOpenApiDocument)(models, rootModel), "utf-8");
|
|
665
|
+
console.log(`ā
Created: ${specPath}`);
|
|
666
|
+
const outputDir = path.join(functionsRoot, "generated", backendLanguage === "csharp" ? "csharp-models" : "python-models");
|
|
667
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
668
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
669
|
+
if (backendLanguage === "csharp") {
|
|
670
|
+
await generateCSharpSchemaArtifacts(models, specPath, outputDir, functionsRoot);
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
await generatePythonSchemaArtifacts(models, specPath, outputDir, functionsRoot);
|
|
674
|
+
}
|
|
675
|
+
console.log(`ā
Generated ${backendLanguage} schema assets: ${outputDir}`);
|
|
676
|
+
}
|
|
677
|
+
//# sourceMappingURL=native-schema-generator.js.map
|