swallowkit 2.0.0-alpha.2 → 2.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +2 -2
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +39 -27
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +6 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +43 -122
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts.map +1 -1
- package/dist/cli/commands/migrations.js +25 -0
- package/dist/cli/commands/migrations.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +2 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +45 -16
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +4 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +40 -2
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +27 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/model-migration.d.ts +22 -0
- package/dist/core/operations/model-migration.d.ts.map +1 -0
- package/dist/core/operations/model-migration.js +36 -0
- package/dist/core/operations/model-migration.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +19 -1
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +62 -35
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +4 -1
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +49 -18
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +1 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +20 -2
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/home-page.d.ts +4 -0
- package/dist/core/project/home-page.d.ts.map +1 -0
- package/dist/core/project/home-page.js +113 -0
- package/dist/core/project/home-page.js.map +1 -0
- package/dist/core/project/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/react-project.d.ts +1 -1
- package/dist/core/project/react-project.d.ts.map +1 -1
- package/dist/core/project/react-project.js +45 -24
- package/dist/core/project/react-project.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +9 -1
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.js +8 -8
- package/dist/core/scaffold/model-parser.d.ts +23 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +407 -137
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +11 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +279 -91
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +24 -1
- package/dist/core/verify/index.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +39 -15
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +3 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +137 -13
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/utils/azure-cli.d.ts +44 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +164 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/package-manager.js +1 -1
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +5 -7
- package/dist/utils/python-uv.d.ts.map +1 -1
- package/dist/utils/python-uv.js +50 -24
- package/dist/utils/python-uv.js.map +1 -1
- package/dist/utils/react-dev.d.ts +4 -0
- package/dist/utils/react-dev.d.ts.map +1 -0
- package/dist/utils/react-dev.js +35 -0
- package/dist/utils/react-dev.js.map +1 -0
- package/dist/utils/semver.d.ts +14 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +61 -0
- package/dist/utils/semver.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-loop-phase34.test.ts +29 -0
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/azure-cli.test.ts +199 -0
- package/src/__tests__/boundaries.test.ts +42 -0
- package/src/__tests__/csharp-schema.test.ts +261 -0
- package/src/__tests__/dev.test.ts +13 -6
- package/src/__tests__/home-page.test.ts +50 -0
- package/src/__tests__/init.test.ts +57 -0
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +49 -3
- package/src/__tests__/package-manager.test.ts +2 -2
- package/src/__tests__/plan-apply.test.ts +68 -0
- package/src/__tests__/python-uv.test.ts +50 -14
- package/src/__tests__/python-venv.test.ts +48 -0
- package/src/__tests__/react-dev.test.ts +72 -0
- package/src/__tests__/react.test.ts +19 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +40 -31
- package/src/cli/commands/init.ts +48 -125
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +53 -17
- package/src/cli/commands/scaffold.ts +40 -2
- package/src/cli/index.ts +26 -3
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-operations.ts +86 -42
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-instructions.ts +4 -1
- package/src/core/project/agent-templates/AGENTS.md +39 -3
- package/src/core/project/agent-templates/CLAUDE.md +3 -1
- package/src/core/project/agent-templates/copilot-instructions.md +3 -1
- package/src/core/project/boundaries.ts +60 -18
- package/src/core/project/capabilities.ts +22 -2
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +49 -27
- package/src/core/project/workflows.ts +9 -1
- package/src/core/scaffold/auth-generator.ts +8 -8
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +305 -105
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +23 -1
- package/src/machine/index.ts +42 -18
- package/src/mcp/index.ts +156 -14
- package/src/mcp/version-selection.ts +163 -0
- package/src/utils/azure-cli.ts +154 -0
- package/src/utils/package-manager.ts +1 -1
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- package/src/utils/semver.ts +59 -0
|
@@ -41,6 +41,8 @@ exports.getCSharpSchemaOptionPath = getCSharpSchemaOptionPath;
|
|
|
41
41
|
exports.getPythonSchemaModelPath = getPythonSchemaModelPath;
|
|
42
42
|
exports.getCSharpNativeGeneratorArgs = getCSharpNativeGeneratorArgs;
|
|
43
43
|
exports.getPythonNativeGeneratorArgs = getPythonNativeGeneratorArgs;
|
|
44
|
+
exports.toPythonRegex = toPythonRegex;
|
|
45
|
+
exports.getPythonRegexWarnings = getPythonRegexWarnings;
|
|
44
46
|
exports.validateNativeSchemaToolchain = validateNativeSchemaToolchain;
|
|
45
47
|
exports.generateLanguageSchemaArtifacts = generateLanguageSchemaArtifacts;
|
|
46
48
|
const fs = __importStar(require("fs"));
|
|
@@ -54,6 +56,8 @@ const python_uv_1 = require("../../utils/python-uv");
|
|
|
54
56
|
exports.NSWAG_CONSOLECORE_VERSION = "14.7.1";
|
|
55
57
|
exports.PYTHON_SCHEMA_CODEGEN_REQUIREMENT = "datamodel-code-generator>=0.44.0,<1.0.0";
|
|
56
58
|
const PYTHON_OUTPUT_MODEL_TYPE = "pydantic_v2.BaseModel";
|
|
59
|
+
// Preserve model syntax compatibility with existing deployments, not the tool's interpreter.
|
|
60
|
+
const PYTHON_MODEL_SYNTAX_VERSION = "3.11";
|
|
57
61
|
function getMachineAwareStdio() {
|
|
58
62
|
return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
|
|
59
63
|
}
|
|
@@ -160,8 +164,22 @@ function buildCSharpEnumMemberMap(values, enumType) {
|
|
|
160
164
|
function csharpStringLiteral(value) {
|
|
161
165
|
return JSON.stringify(value);
|
|
162
166
|
}
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
/** Date/time types are used only when the Zod schema declares them (z.iso.datetime(), z.iso.date(), z.date()). */
|
|
168
|
+
function getCSharpScalarType(field) {
|
|
169
|
+
switch (field.type) {
|
|
170
|
+
case "string":
|
|
171
|
+
return field.format === "date-time" ? "DateTime" : field.format === "date" ? "DateOnly" : "string";
|
|
172
|
+
case "number":
|
|
173
|
+
return field.isInteger ? "long" : "double";
|
|
174
|
+
case "boolean":
|
|
175
|
+
return "bool";
|
|
176
|
+
case "date":
|
|
177
|
+
return "DateTime";
|
|
178
|
+
case "object":
|
|
179
|
+
return "Dictionary<string, object>";
|
|
180
|
+
default:
|
|
181
|
+
return "object";
|
|
182
|
+
}
|
|
165
183
|
}
|
|
166
184
|
function getCSharpSchemaModelPath(outputDir, modelName) {
|
|
167
185
|
return path.join(outputDir, "src", "SwallowKitBackendModels", "Model", `${modelName}.cs`);
|
|
@@ -204,36 +222,24 @@ function getPythonNativeGeneratorArgs(specPath, outputPath) {
|
|
|
204
222
|
"--output-model-type",
|
|
205
223
|
PYTHON_OUTPUT_MODEL_TYPE,
|
|
206
224
|
"--target-python-version",
|
|
207
|
-
|
|
225
|
+
PYTHON_MODEL_SYNTAX_VERSION,
|
|
208
226
|
"--disable-timestamp",
|
|
209
227
|
"--use-union-operator",
|
|
210
228
|
"--collapse-root-models",
|
|
211
229
|
];
|
|
212
230
|
}
|
|
213
231
|
function getCSharpFieldBaseType(field) {
|
|
232
|
+
// Arrays first: arrays of nested models or enums are lists of the element type.
|
|
233
|
+
if (field.isArray) {
|
|
234
|
+
return `List<${getCSharpArrayElementType(field)}>`;
|
|
235
|
+
}
|
|
214
236
|
if (field.isNestedSchema && field.nestedModelName) {
|
|
215
237
|
return field.nestedModelName;
|
|
216
238
|
}
|
|
217
239
|
if (field.enumValues?.length) {
|
|
218
240
|
return `${toPascalIdentifier(field.name)}Enum`;
|
|
219
241
|
}
|
|
220
|
-
|
|
221
|
-
return `List<${getCSharpArrayElementType(field)}>`;
|
|
222
|
-
}
|
|
223
|
-
switch (field.type) {
|
|
224
|
-
case "string":
|
|
225
|
-
return isDateLikeField(field) ? "DateTime" : "string";
|
|
226
|
-
case "number":
|
|
227
|
-
return "decimal";
|
|
228
|
-
case "boolean":
|
|
229
|
-
return "bool";
|
|
230
|
-
case "date":
|
|
231
|
-
return "DateTime";
|
|
232
|
-
case "object":
|
|
233
|
-
return "Dictionary<string, object>";
|
|
234
|
-
default:
|
|
235
|
-
return "object";
|
|
236
|
-
}
|
|
242
|
+
return getCSharpScalarType(field);
|
|
237
243
|
}
|
|
238
244
|
function getCSharpArrayElementType(field) {
|
|
239
245
|
if (field.isNestedSchema && field.nestedModelName) {
|
|
@@ -242,24 +248,11 @@ function getCSharpArrayElementType(field) {
|
|
|
242
248
|
if (field.enumValues?.length) {
|
|
243
249
|
return `${toPascalIdentifier(field.name)}Enum`;
|
|
244
250
|
}
|
|
245
|
-
|
|
246
|
-
case "string":
|
|
247
|
-
return isDateLikeField(field) ? "DateTime" : "string";
|
|
248
|
-
case "number":
|
|
249
|
-
return "decimal";
|
|
250
|
-
case "boolean":
|
|
251
|
-
return "bool";
|
|
252
|
-
case "date":
|
|
253
|
-
return "DateTime";
|
|
254
|
-
case "object":
|
|
255
|
-
return "Dictionary<string, object>";
|
|
256
|
-
default:
|
|
257
|
-
return "object";
|
|
258
|
-
}
|
|
251
|
+
return getCSharpScalarType(field);
|
|
259
252
|
}
|
|
260
253
|
function getCSharpPropertyType(field) {
|
|
261
254
|
const baseType = getCSharpFieldBaseType(field);
|
|
262
|
-
return field.isOptional ? `${baseType}?` : baseType;
|
|
255
|
+
return field.isOptional || field.isNullable ? `${baseType}?` : baseType;
|
|
263
256
|
}
|
|
264
257
|
function getCSharpOptionType(field) {
|
|
265
258
|
return `Option<${getCSharpPropertyType(field)}>`;
|
|
@@ -316,11 +309,96 @@ function buildCSharpEnumToJsonCases(field, members) {
|
|
|
316
309
|
.map((member) => ` if (value == ${enumType}.${member.name})\n return ${csharpStringLiteral(member.value)};`)
|
|
317
310
|
.join("\n\n");
|
|
318
311
|
}
|
|
312
|
+
function csharpNumberLiteral(value) {
|
|
313
|
+
return Number.isInteger(value) ? `${value}` : `${value}d`;
|
|
314
|
+
}
|
|
315
|
+
function csharpRegexOptions(flags) {
|
|
316
|
+
const options = [
|
|
317
|
+
flags?.includes("i") ? "RegexOptions.IgnoreCase" : null,
|
|
318
|
+
flags?.includes("m") ? "RegexOptions.Multiline" : null,
|
|
319
|
+
flags?.includes("s") ? "RegexOptions.Singleline" : null,
|
|
320
|
+
].filter(Boolean);
|
|
321
|
+
return options.length > 0 ? options.join(" | ") : "RegexOptions.None";
|
|
322
|
+
}
|
|
323
|
+
function isCSharpReferenceType(field) {
|
|
324
|
+
if (field.isArray || field.isNestedSchema || field.type === "object")
|
|
325
|
+
return true;
|
|
326
|
+
if (field.enumValues?.length)
|
|
327
|
+
return false;
|
|
328
|
+
return field.type === "string" && field.format !== "date-time" && field.format !== "date";
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Translate Zod constraints into IValidatableObject checks. Regexes use search semantics like
|
|
332
|
+
* Zod's RegExp.test. String formats (email/uuid/url) and value-type presence stay with Zod.
|
|
333
|
+
*/
|
|
334
|
+
function buildCSharpValueChecks(field, accessor, member, optionalValue) {
|
|
335
|
+
const constraints = field.constraints ?? {};
|
|
336
|
+
const checks = [];
|
|
337
|
+
const add = (condition, message) => checks.push(`if (${condition})\n yield return new ValidationResult(${csharpStringLiteral(message)}, new[] { ${member} });`);
|
|
338
|
+
if (field.isNestedSchema && field.nestedModelName) {
|
|
339
|
+
// Pattern variables in an if condition stay in scope for the enclosing block, so keep names unique.
|
|
340
|
+
const variable = `validated${toPascalIdentifier(field.name)}${accessor === "item" ? "Item" : ""}`;
|
|
341
|
+
checks.push(`if (${accessor} is IValidatableObject ${variable})\n foreach (var ${variable}Result in ${variable}.Validate(new ValidationContext(${accessor})))\n yield return new ValidationResult(${variable}Result.ErrorMessage, ${variable}Result.MemberNames.Select(name => ${member} + "." + name).ToArray());`);
|
|
342
|
+
return checks;
|
|
343
|
+
}
|
|
344
|
+
if (field.enumValues?.length)
|
|
345
|
+
return checks;
|
|
346
|
+
if (field.type === "string" && field.format !== "date-time" && field.format !== "date") {
|
|
347
|
+
if (constraints.minLength !== undefined)
|
|
348
|
+
add(`${accessor} != null && ${accessor}.Length < ${constraints.minLength}`, `${field.name} must contain at least ${constraints.minLength} character(s).`);
|
|
349
|
+
if (constraints.maxLength !== undefined)
|
|
350
|
+
add(`${accessor} != null && ${accessor}.Length > ${constraints.maxLength}`, `${field.name} must contain at most ${constraints.maxLength} character(s).`);
|
|
351
|
+
if (constraints.pattern !== undefined) {
|
|
352
|
+
add(`${accessor} != null && !Regex.IsMatch(${accessor}, ${csharpStringLiteral(constraints.pattern)}, ${csharpRegexOptions(constraints.patternFlags)})`, `${field.name} must match /${constraints.pattern}/${constraints.patternFlags ?? ""}.`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (field.type === "number") {
|
|
356
|
+
const value = optionalValue ? `${accessor}.Value` : accessor;
|
|
357
|
+
const guard = optionalValue ? `${accessor}.HasValue && ` : "";
|
|
358
|
+
if (constraints.minimum !== undefined) {
|
|
359
|
+
add(`${guard}${value} ${constraints.exclusiveMinimum ? "<=" : "<"} ${csharpNumberLiteral(constraints.minimum)}`, `${field.name} must be ${constraints.exclusiveMinimum ? "greater than" : "greater than or equal to"} ${constraints.minimum}.`);
|
|
360
|
+
}
|
|
361
|
+
if (constraints.maximum !== undefined) {
|
|
362
|
+
add(`${guard}${value} ${constraints.exclusiveMaximum ? ">=" : ">"} ${csharpNumberLiteral(constraints.maximum)}`, `${field.name} must be ${constraints.exclusiveMaximum ? "less than" : "less than or equal to"} ${constraints.maximum}.`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return checks;
|
|
366
|
+
}
|
|
367
|
+
function buildCSharpValidationBody(model) {
|
|
368
|
+
const blocks = [];
|
|
369
|
+
for (const field of model.fields) {
|
|
370
|
+
const propertyName = toPascalIdentifier(field.name);
|
|
371
|
+
const member = csharpStringLiteral(field.name);
|
|
372
|
+
const constraints = field.constraints ?? {};
|
|
373
|
+
// .nullable() fields accept null in Zod, so only non-nullable required references are rejected.
|
|
374
|
+
if (!field.isOptional && !field.isNullable && isCSharpReferenceType(field)) {
|
|
375
|
+
blocks.push(`if (${propertyName} is null)\n yield return new ValidationResult(${csharpStringLiteral(`${field.name} is required.`)}, new[] { ${member} });`);
|
|
376
|
+
}
|
|
377
|
+
if (field.isArray) {
|
|
378
|
+
if (constraints.minItems !== undefined) {
|
|
379
|
+
blocks.push(`if (${propertyName} != null && ${propertyName}.Count < ${constraints.minItems})\n yield return new ValidationResult(${csharpStringLiteral(`${field.name} must contain at least ${constraints.minItems} item(s).`)}, new[] { ${member} });`);
|
|
380
|
+
}
|
|
381
|
+
if (constraints.maxItems !== undefined) {
|
|
382
|
+
blocks.push(`if (${propertyName} != null && ${propertyName}.Count > ${constraints.maxItems})\n yield return new ValidationResult(${csharpStringLiteral(`${field.name} must contain at most ${constraints.maxItems} item(s).`)}, new[] { ${member} });`);
|
|
383
|
+
}
|
|
384
|
+
const itemChecks = buildCSharpValueChecks({ ...field, isArray: false }, "item", `${member} + "[" + index + "]"`, false);
|
|
385
|
+
if (itemChecks.length > 0) {
|
|
386
|
+
blocks.push(`if (${propertyName} != null)\n {\n for (var index = 0; index < ${propertyName}.Count; index++)\n {\n var item = ${propertyName}[index];\n${itemChecks.map((check) => ` ${check.replace(/\n {16}/g, "\n ").replace(/\n {20}/g, "\n ")}`).join("\n")}\n }\n }`);
|
|
387
|
+
}
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const optionalValue = (field.isOptional || field.isNullable === true) && !isCSharpReferenceType(field);
|
|
391
|
+
blocks.push(...buildCSharpValueChecks(field, propertyName, member, optionalValue));
|
|
392
|
+
}
|
|
393
|
+
return blocks.length > 0
|
|
394
|
+
? blocks.map((block) => ` ${block}`).join("\n\n")
|
|
395
|
+
: " yield break;";
|
|
396
|
+
}
|
|
319
397
|
function generateLegacyCompatibleCSharpModelSource(model) {
|
|
320
398
|
const requiredFields = model.fields.filter((field) => !field.isOptional);
|
|
321
399
|
const optionalFields = model.fields.filter((field) => field.isOptional);
|
|
322
400
|
const constructorParams = [
|
|
323
|
-
...requiredFields.map((field) => `${
|
|
401
|
+
...requiredFields.map((field) => `${getCSharpPropertyType(field)} ${field.name}`),
|
|
324
402
|
...optionalFields.map((field) => `${getCSharpOptionType(field)} ${field.name} = default`),
|
|
325
403
|
].join(", ");
|
|
326
404
|
const constructorAssignments = [
|
|
@@ -336,7 +414,7 @@ function generateLegacyCompatibleCSharpModelSource(model) {
|
|
|
336
414
|
return ` /// <summary>
|
|
337
415
|
/// Defines ${toPascalIdentifier(field.name)}
|
|
338
416
|
/// </summary>
|
|
339
|
-
[JsonConverter(typeof(
|
|
417
|
+
[JsonConverter(typeof(${enumType}JsonConverter))]
|
|
340
418
|
public enum ${enumType}
|
|
341
419
|
{
|
|
342
420
|
${buildCSharpEnumMembers(enumMembers)}
|
|
@@ -357,6 +435,22 @@ ${buildCSharpEnumFromStringCases(field, enumMembers, true)}
|
|
|
357
435
|
${buildCSharpEnumToJsonCases(field, enumMembers)}
|
|
358
436
|
|
|
359
437
|
throw new NotImplementedException($"Value could not be handled: '{value}'");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/// <summary>
|
|
441
|
+
/// Reads and writes ${enumType} with the exact Zod enum values.
|
|
442
|
+
/// </summary>
|
|
443
|
+
public sealed class ${enumType}JsonConverter : JsonConverter<${enumType}>
|
|
444
|
+
{
|
|
445
|
+
public override ${enumType} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
446
|
+
{
|
|
447
|
+
var value = reader.GetString();
|
|
448
|
+
var parsed = value == null ? null : ${enumType}FromStringOrDefault(value);
|
|
449
|
+
return parsed ?? throw new JsonException($"Unsupported ${enumType} value: '{value}'");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
public override void Write(Utf8JsonWriter writer, ${enumType} value, JsonSerializerOptions options)
|
|
453
|
+
=> writer.WriteStringValue(${enumType}ToJsonValue(value));
|
|
360
454
|
}`;
|
|
361
455
|
})
|
|
362
456
|
.join("\n\n");
|
|
@@ -381,7 +475,8 @@ ${buildCSharpEnumToJsonCases(field, enumMembers)}
|
|
|
381
475
|
/// <summary>
|
|
382
476
|
/// Gets or Sets ${propertyName}
|
|
383
477
|
/// </summary>
|
|
384
|
-
[JsonPropertyName("${field.name}")]
|
|
478
|
+
[JsonPropertyName("${field.name}")]${field.isNullable ? "" : `
|
|
479
|
+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`}
|
|
385
480
|
public ${propertyType} ${propertyName} { get { return this.${propertyName}Option; } set { this.${propertyName}Option = new(value); } }`;
|
|
386
481
|
})
|
|
387
482
|
.join("\n\n");
|
|
@@ -403,8 +498,11 @@ ${buildCSharpEnumToJsonCases(field, enumMembers)}
|
|
|
403
498
|
using System;
|
|
404
499
|
using System.Collections.Generic;
|
|
405
500
|
using System.ComponentModel.DataAnnotations;
|
|
501
|
+
using System.Linq;
|
|
406
502
|
using System.Text;
|
|
503
|
+
using System.Text.Json;
|
|
407
504
|
using System.Text.Json.Serialization;
|
|
505
|
+
using System.Text.RegularExpressions;
|
|
408
506
|
using SwallowKitBackendModels.Client;
|
|
409
507
|
|
|
410
508
|
namespace SwallowKitBackendModels.Model
|
|
@@ -414,12 +512,22 @@ namespace SwallowKitBackendModels.Model
|
|
|
414
512
|
/// </summary>
|
|
415
513
|
public partial class ${model.name} : IValidatableObject
|
|
416
514
|
{
|
|
515
|
+
/// <summary>
|
|
516
|
+
/// Used by System.Text.Json. Optional values are marked as set only when present in the JSON.
|
|
517
|
+
/// </summary>
|
|
518
|
+
#pragma warning disable CS8618
|
|
417
519
|
[JsonConstructor]
|
|
520
|
+
public ${model.name}()
|
|
521
|
+
{
|
|
522
|
+
OnCreated();
|
|
523
|
+
}
|
|
524
|
+
#pragma warning restore CS8618
|
|
525
|
+
${constructorParams ? `
|
|
418
526
|
public ${model.name}(${constructorParams})
|
|
419
527
|
{
|
|
420
528
|
${constructorAssignments}
|
|
421
529
|
}
|
|
422
|
-
|
|
530
|
+
` : ""}
|
|
423
531
|
partial void OnCreated();
|
|
424
532
|
${enumBlocks ? `\n\n${enumBlocks}` : ""}
|
|
425
533
|
|
|
@@ -436,24 +544,18 @@ ${toStringBody}
|
|
|
436
544
|
|
|
437
545
|
IEnumerable<ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
|
|
438
546
|
{
|
|
439
|
-
|
|
547
|
+
${buildCSharpValidationBody(model)}
|
|
440
548
|
}
|
|
441
549
|
}
|
|
442
550
|
}
|
|
443
551
|
`;
|
|
444
552
|
}
|
|
445
|
-
function
|
|
446
|
-
if (field.isNestedSchema && field.nestedModelName) {
|
|
447
|
-
return field.nestedModelName;
|
|
448
|
-
}
|
|
449
|
-
if (field.isArray) {
|
|
450
|
-
return `List[${getPythonArrayElementType(field)}]`;
|
|
451
|
-
}
|
|
553
|
+
function getPythonScalarType(field) {
|
|
452
554
|
switch (field.type) {
|
|
453
555
|
case "string":
|
|
454
|
-
return
|
|
556
|
+
return field.format === "date-time" ? "datetime" : field.format === "date" ? "date" : "StrictStr";
|
|
455
557
|
case "number":
|
|
456
|
-
return "Union[StrictFloat, StrictInt]";
|
|
558
|
+
return field.isInteger ? "StrictInt" : "Union[StrictFloat, StrictInt]";
|
|
457
559
|
case "boolean":
|
|
458
560
|
return "StrictBool";
|
|
459
561
|
case "date":
|
|
@@ -464,36 +566,128 @@ function getPythonTypeName(field) {
|
|
|
464
566
|
return "Any";
|
|
465
567
|
}
|
|
466
568
|
}
|
|
569
|
+
/** pydantic Field bounds for a scalar value (string length or numeric range). */
|
|
570
|
+
function getPythonValueConstraintArgs(field) {
|
|
571
|
+
const constraints = field.constraints ?? {};
|
|
572
|
+
if (field.enumValues?.length || field.isNestedSchema)
|
|
573
|
+
return [];
|
|
574
|
+
const args = [];
|
|
575
|
+
if (field.type === "string" && field.format !== "date-time" && field.format !== "date") {
|
|
576
|
+
if (constraints.minLength !== undefined)
|
|
577
|
+
args.push(`min_length=${constraints.minLength}`);
|
|
578
|
+
if (constraints.maxLength !== undefined)
|
|
579
|
+
args.push(`max_length=${constraints.maxLength}`);
|
|
580
|
+
}
|
|
581
|
+
if (field.type === "number") {
|
|
582
|
+
if (constraints.minimum !== undefined)
|
|
583
|
+
args.push(`${constraints.exclusiveMinimum ? "gt" : "ge"}=${constraints.minimum}`);
|
|
584
|
+
if (constraints.maximum !== undefined)
|
|
585
|
+
args.push(`${constraints.exclusiveMaximum ? "lt" : "le"}=${constraints.maximum}`);
|
|
586
|
+
}
|
|
587
|
+
return args;
|
|
588
|
+
}
|
|
467
589
|
function getPythonArrayElementType(field) {
|
|
468
590
|
if (field.isNestedSchema && field.nestedModelName) {
|
|
469
591
|
return field.nestedModelName;
|
|
470
592
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
case "date":
|
|
479
|
-
return "datetime";
|
|
480
|
-
case "object":
|
|
481
|
-
return "Dict[str, Any]";
|
|
482
|
-
default:
|
|
483
|
-
return "Any";
|
|
593
|
+
const elementType = getPythonScalarType(field);
|
|
594
|
+
const elementArgs = getPythonValueConstraintArgs(field);
|
|
595
|
+
return elementArgs.length > 0 ? `Annotated[${elementType}, Field(${elementArgs.join(", ")})]` : elementType;
|
|
596
|
+
}
|
|
597
|
+
function getPythonTypeName(field) {
|
|
598
|
+
if (field.isArray) {
|
|
599
|
+
return `List[${getPythonArrayElementType(field)}]`;
|
|
484
600
|
}
|
|
601
|
+
if (field.isNestedSchema && field.nestedModelName) {
|
|
602
|
+
return field.nestedModelName;
|
|
603
|
+
}
|
|
604
|
+
return getPythonScalarType(field);
|
|
485
605
|
}
|
|
486
606
|
function buildPythonFieldDeclaration(field) {
|
|
487
607
|
const pythonName = toSnakeCase(field.name);
|
|
488
608
|
const typeName = field.isOptional ? `Optional[${getPythonTypeName(field)}]` : getPythonTypeName(field);
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
609
|
+
const fieldArgs = [
|
|
610
|
+
...(field.isOptional ? ["default=None"] : []),
|
|
611
|
+
...(pythonName !== field.name ? [`alias="${field.name}"`] : []),
|
|
612
|
+
...(field.isArray
|
|
613
|
+
? [
|
|
614
|
+
...(field.constraints?.minItems !== undefined ? [`min_length=${field.constraints.minItems}`] : []),
|
|
615
|
+
...(field.constraints?.maxItems !== undefined ? [`max_length=${field.constraints.maxItems}`] : []),
|
|
616
|
+
]
|
|
617
|
+
: getPythonValueConstraintArgs(field)),
|
|
618
|
+
];
|
|
619
|
+
return fieldArgs.length > 0
|
|
620
|
+
? `${pythonName}: ${typeName} = Field(${fieldArgs.join(", ")})`
|
|
621
|
+
: `${pythonName}: ${typeName}`;
|
|
622
|
+
}
|
|
623
|
+
function buildPythonRegexFlags(flags) {
|
|
624
|
+
const values = [
|
|
625
|
+
flags?.includes("i") ? "re.IGNORECASE" : null,
|
|
626
|
+
flags?.includes("m") ? "re.MULTILINE" : null,
|
|
627
|
+
flags?.includes("s") ? "re.DOTALL" : null,
|
|
628
|
+
].filter(Boolean);
|
|
629
|
+
return values.length > 0 ? values.join(" | ") : "0";
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Translates a JavaScript regex (as used by Zod) to Python `re` syntax. Constructs Python cannot
|
|
633
|
+
* express identically are reported as unsupported so the validator is skipped instead of breaking imports.
|
|
634
|
+
*/
|
|
635
|
+
function toPythonRegex(pattern, flags = "") {
|
|
636
|
+
const unsupportedFlags = [...flags].filter((flag) => !"imsgu".includes(flag));
|
|
637
|
+
if (unsupportedFlags.length > 0)
|
|
638
|
+
return { unsupported: `flag(s) ${unsupportedFlags.join("")} have no Python equivalent` };
|
|
639
|
+
const checks = [
|
|
640
|
+
[/\\[pP]\{/, "Unicode property escapes (\\p{...})"],
|
|
641
|
+
[/\\u\{/, "code point escapes (\\u{...})"],
|
|
642
|
+
[/\\c[A-Za-z]/, "control escapes (\\cX)"],
|
|
643
|
+
[/\(\?<[=!]/, "lookbehind (JavaScript allows variable width; Python does not)"],
|
|
644
|
+
[/\[\^?\]/, "empty character classes ([] / [^])"],
|
|
645
|
+
];
|
|
646
|
+
for (const [construct, label] of checks) {
|
|
647
|
+
if (construct.test(pattern))
|
|
648
|
+
return { unsupported: label };
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
pattern: pattern
|
|
652
|
+
.replace(/\(\?<([A-Za-z_][A-Za-z0-9_]*)>/g, "(?P<$1>")
|
|
653
|
+
.replace(/\\k<([A-Za-z_][A-Za-z0-9_]*)>/g, "(?P=$1)"),
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function pythonRegexFields(model) {
|
|
657
|
+
return model.fields
|
|
658
|
+
.filter((field) => field.type === "string" && !field.enumValues?.length && field.constraints?.pattern !== undefined)
|
|
659
|
+
.map((field) => ({ field, translation: toPythonRegex(field.constraints.pattern, field.constraints.patternFlags) }));
|
|
660
|
+
}
|
|
661
|
+
function getPythonRegexWarnings(model) {
|
|
662
|
+
return pythonRegexFields(model)
|
|
663
|
+
.filter((entry) => "unsupported" in entry.translation)
|
|
664
|
+
.map(({ field, translation }) => `python-regex-not-enforced: ${model.name}.${field.name} uses ${translation.unsupported}; the Python model does not enforce /${field.constraints.pattern}/. The BFF Zod schema still validates it.`);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Zod regex checks use RegExp.test (search), so validators use re.search instead of pydantic's anchored pattern.
|
|
668
|
+
* They run before type conversion so date/time strings are checked as the JSON text Zod sees; non-string
|
|
669
|
+
* values are left to the field's own type validation.
|
|
670
|
+
*/
|
|
671
|
+
function buildPythonPatternValidators(model) {
|
|
672
|
+
return pythonRegexFields(model)
|
|
673
|
+
.filter((entry) => "pattern" in entry.translation)
|
|
674
|
+
.map(({ field, translation }) => {
|
|
675
|
+
const pythonName = toSnakeCase(field.name);
|
|
676
|
+
const pattern = JSON.stringify(translation.pattern);
|
|
677
|
+
const flags = buildPythonRegexFlags(field.constraints.patternFlags);
|
|
678
|
+
const message = JSON.stringify(`${field.name} must match /${field.constraints.pattern}/${field.constraints.patternFlags ?? ""}`);
|
|
679
|
+
return ` @field_validator('${pythonName}', mode='before')
|
|
680
|
+
def ${pythonName}_validate_pattern(cls, value):
|
|
681
|
+
"""Validates the Zod regex constraint"""
|
|
682
|
+
if value is None:
|
|
683
|
+
return value
|
|
684
|
+
|
|
685
|
+
for item in (value if isinstance(value, list) else [value]):
|
|
686
|
+
if isinstance(item, str) and not re.search(${pattern}, item, ${flags}):
|
|
687
|
+
raise ValueError(${message})
|
|
688
|
+
return value`;
|
|
689
|
+
})
|
|
690
|
+
.join("\n\n");
|
|
497
691
|
}
|
|
498
692
|
function buildPythonEnumValidators(model) {
|
|
499
693
|
return model.fields
|
|
@@ -524,7 +718,7 @@ function buildPythonModelImports(model) {
|
|
|
524
718
|
function generateLegacyCompatiblePythonModelSource(model) {
|
|
525
719
|
const fieldDeclarations = model.fields.map((field) => ` ${buildPythonFieldDeclaration(field)}`).join("\n");
|
|
526
720
|
const propertyNames = model.fields.map((field) => `"${field.name}"`).join(", ");
|
|
527
|
-
const validators = buildPythonEnumValidators(model);
|
|
721
|
+
const validators = [buildPythonEnumValidators(model), buildPythonPatternValidators(model)].filter(Boolean).join("\n\n");
|
|
528
722
|
const dictAssignments = model.fields
|
|
529
723
|
.map((field) => ` "${field.name}": obj.get("${field.name}")`)
|
|
530
724
|
.join(",\n");
|
|
@@ -547,9 +741,9 @@ import pprint
|
|
|
547
741
|
import re # noqa: F401
|
|
548
742
|
import json
|
|
549
743
|
|
|
550
|
-
from datetime import datetime
|
|
744
|
+
from datetime import date, datetime
|
|
551
745
|
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictFloat, StrictInt, StrictStr, field_validator
|
|
552
|
-
from typing import Any, ClassVar, Dict, List, Optional, Set, Union
|
|
746
|
+
from typing import Annotated, Any, ClassVar, Dict, List, Optional, Set, Union
|
|
553
747
|
from typing_extensions import Self
|
|
554
748
|
|
|
555
749
|
${buildPythonModelImports(model)}class ${model.name}(BaseModel):
|
|
@@ -573,7 +767,7 @@ ${validators ? `\n\n${validators}` : ""}
|
|
|
573
767
|
|
|
574
768
|
def to_json(self) -> str:
|
|
575
769
|
"""Returns the JSON representation of the model using alias"""
|
|
576
|
-
return
|
|
770
|
+
return self.model_dump_json(by_alias=True, exclude_none=True)
|
|
577
771
|
|
|
578
772
|
@classmethod
|
|
579
773
|
def from_json(cls, json_str: str) -> Optional[Self]:
|
|
@@ -660,13 +854,8 @@ function ensurePythonCodegenProjectFiles(functionsRoot, session, sourceModel) {
|
|
|
660
854
|
}
|
|
661
855
|
return requirementsPath;
|
|
662
856
|
}
|
|
663
|
-
function getVirtualEnvPythonPath(venvDir) {
|
|
664
|
-
return process.platform === "win32"
|
|
665
|
-
? path.join(venvDir, "Scripts", "python.exe")
|
|
666
|
-
: path.join(venvDir, "bin", "python");
|
|
667
|
-
}
|
|
668
857
|
async function ensureProjectLocalUvCommand(projectRoot) {
|
|
669
|
-
const uvEnv =
|
|
858
|
+
const uvEnv = { ...process.env };
|
|
670
859
|
const { localUvExecutable } = (0, python_uv_1.getProjectLocalUvPaths)(projectRoot);
|
|
671
860
|
if (canRun("uv", ["--version"], projectRoot)) {
|
|
672
861
|
return { command: "uv", env: uvEnv };
|
|
@@ -685,16 +874,9 @@ async function ensurePythonCodegenEnvironment(functionsRoot, requirementsPath) {
|
|
|
685
874
|
const projectRoot = (0, python_uv_1.getPythonProjectRoot)(functionsRoot);
|
|
686
875
|
const { command: uvCommand, env: uvEnv } = await ensureProjectLocalUvCommand(projectRoot);
|
|
687
876
|
const venvDir = path.join(functionsRoot, ".codegen-venv");
|
|
688
|
-
const venvPython =
|
|
689
|
-
if (!canRun(venvPython, ["--version"], functionsRoot, uvEnv)) {
|
|
690
|
-
const venvArgs = (0, python_uv_1.buildUvVenvArgs)(venvDir);
|
|
691
|
-
if (fs.existsSync(venvDir)) {
|
|
692
|
-
venvArgs.push("--clear");
|
|
693
|
-
}
|
|
694
|
-
await runCommand(uvCommand, venvArgs, functionsRoot, "Failed to create the Python schema code generation virtual environment.", uvEnv);
|
|
695
|
-
}
|
|
877
|
+
const venvPython = await (0, python_uv_1.ensureUvVirtualEnvironment)(uvCommand, venvDir, functionsRoot, uvEnv);
|
|
696
878
|
if (!canRun(venvPython, ["-c", "import datamodel_code_generator"], functionsRoot, uvEnv)) {
|
|
697
|
-
await runCommand(uvCommand, (0, python_uv_1.buildUvPipInstallArgs)(venvPython, requirementsPath), functionsRoot, "Failed to install Python schema generation dependencies.", uvEnv);
|
|
879
|
+
await runCommand(uvCommand, (0, python_uv_1.buildUvPipInstallArgs)(venvPython, requirementsPath), functionsRoot, "Failed to install Python schema generation dependencies.", (0, python_uv_1.buildPythonCodegenInstallEnv)(uvEnv));
|
|
698
880
|
}
|
|
699
881
|
return venvPython;
|
|
700
882
|
}
|
|
@@ -764,6 +946,10 @@ async function generatePythonSchemaArtifacts(models, outputDir, functionsRoot, s
|
|
|
764
946
|
writeMergedPythonInitFile(path.join(packageRoot, "__init__.py"), buildGeneratedPythonPackageInitSource(models), session, sourceModel);
|
|
765
947
|
writeMergedPythonInitFile(path.join(modelsRoot, "__init__.py"), buildGeneratedPythonModelsInitSource(models), session, sourceModel);
|
|
766
948
|
for (const model of models) {
|
|
949
|
+
for (const warning of getPythonRegexWarnings(model)) {
|
|
950
|
+
session.addWarning(warning);
|
|
951
|
+
console.warn(`⚠️ ${warning}`);
|
|
952
|
+
}
|
|
767
953
|
const modelPath = getPythonSchemaModelPath(outputDir, model.name);
|
|
768
954
|
session.writeFile(modelPath, generateLegacyCompatiblePythonModelSource(model), {
|
|
769
955
|
ownership: "managed",
|
|
@@ -788,11 +974,13 @@ async function generateLanguageSchemaArtifacts(models, rootModel, functionsDir,
|
|
|
788
974
|
});
|
|
789
975
|
console.log(planning ? `\ud83d\udccb Will create: ${specPath}` : `\u2705 Created: ${specPath}`);
|
|
790
976
|
const outputDir = path.join(functionsRoot, "generated", backendLanguage === "csharp" ? "csharp-models" : "python-models");
|
|
977
|
+
// Inline z.object fields become named classes (e.g. ProbeItemNested) so their shape is preserved.
|
|
978
|
+
const nativeModels = (0, openapi_generator_1.expandInlineObjectModels)(models);
|
|
791
979
|
if (backendLanguage === "csharp") {
|
|
792
|
-
await generateCSharpSchemaArtifacts(
|
|
980
|
+
await generateCSharpSchemaArtifacts(nativeModels, outputDir, functionsRoot, session, rootModel.name);
|
|
793
981
|
}
|
|
794
982
|
else {
|
|
795
|
-
await generatePythonSchemaArtifacts(
|
|
983
|
+
await generatePythonSchemaArtifacts(nativeModels, outputDir, functionsRoot, session, rootModel.name);
|
|
796
984
|
}
|
|
797
985
|
console.log(planning
|
|
798
986
|
? `\ud83d\udccb Will generate ${backendLanguage} schema assets: ${outputDir}`
|