swallowkit 2.0.0-alpha.3 → 2.0.0-alpha.5
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/add-auth.js +37 -6
- package/dist/cli/commands/add-auth.js.map +1 -1
- 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/init.d.ts +6 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +19 -5
- 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 +85 -3
- 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 +29 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/create-model.d.ts +2 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +60 -8
- package/dist/core/operations/create-model.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/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +51 -20
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +2 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +29 -5
- package/dist/core/project/capabilities.js.map +1 -1
- 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.js +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +23 -5
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +18 -19
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- 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 +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +289 -76
- 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 +4 -3
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +122 -5
- package/dist/core/verify/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +1 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +3 -2
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +2 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +42 -17
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +4 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +154 -15
- 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/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 +6 -3
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-loop-phase34.test.ts +124 -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__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +64 -3
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/plan-apply.test.ts +170 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/add-auth.ts +36 -6
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/init.ts +23 -8
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +53 -17
- package/src/cli/commands/scaffold.ts +105 -3
- package/src/cli/index.ts +28 -5
- package/src/core/operations/create-model.ts +51 -13
- 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-templates/AGENTS.md +67 -6
- package/src/core/project/agent-templates/CLAUDE.md +7 -1
- package/src/core/project/agent-templates/copilot-instructions.md +7 -1
- package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
- package/src/core/project/boundaries.ts +62 -20
- package/src/core/project/capabilities.ts +32 -5
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +1 -1
- package/src/core/project/workflows.ts +23 -5
- package/src/core/scaffold/auth-generator.ts +18 -19
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +313 -78
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +118 -6
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +45 -20
- package/src/mcp/index.ts +173 -18
- package/src/mcp/version-selection.ts +163 -0
- package/src/utils/azure-cli.ts +154 -0
- package/src/utils/semver.ts +59 -0
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.PYTHON_SCHEMA_CODEGEN_REQUIREMENT = exports.NSWAG_CONSOLECORE_VERSION = void 0;
|
|
36
|
+
exports.PYTHON_SCHEMA_RUNTIME_REQUIREMENT = exports.PYTHON_SCHEMA_CODEGEN_REQUIREMENT = exports.NSWAG_CONSOLECORE_VERSION = void 0;
|
|
37
37
|
exports.buildCSharpCodegenToolManifestSource = buildCSharpCodegenToolManifestSource;
|
|
38
38
|
exports.buildPythonCodegenRequirementsSource = buildPythonCodegenRequirementsSource;
|
|
39
39
|
exports.getCSharpSchemaModelPath = getCSharpSchemaModelPath;
|
|
@@ -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"));
|
|
@@ -162,8 +164,22 @@ function buildCSharpEnumMemberMap(values, enumType) {
|
|
|
162
164
|
function csharpStringLiteral(value) {
|
|
163
165
|
return JSON.stringify(value);
|
|
164
166
|
}
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
}
|
|
167
183
|
}
|
|
168
184
|
function getCSharpSchemaModelPath(outputDir, modelName) {
|
|
169
185
|
return path.join(outputDir, "src", "SwallowKitBackendModels", "Model", `${modelName}.cs`);
|
|
@@ -213,29 +229,17 @@ function getPythonNativeGeneratorArgs(specPath, outputPath) {
|
|
|
213
229
|
];
|
|
214
230
|
}
|
|
215
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
|
+
}
|
|
216
236
|
if (field.isNestedSchema && field.nestedModelName) {
|
|
217
237
|
return field.nestedModelName;
|
|
218
238
|
}
|
|
219
239
|
if (field.enumValues?.length) {
|
|
220
240
|
return `${toPascalIdentifier(field.name)}Enum`;
|
|
221
241
|
}
|
|
222
|
-
|
|
223
|
-
return `List<${getCSharpArrayElementType(field)}>`;
|
|
224
|
-
}
|
|
225
|
-
switch (field.type) {
|
|
226
|
-
case "string":
|
|
227
|
-
return isDateLikeField(field) ? "DateTime" : "string";
|
|
228
|
-
case "number":
|
|
229
|
-
return "decimal";
|
|
230
|
-
case "boolean":
|
|
231
|
-
return "bool";
|
|
232
|
-
case "date":
|
|
233
|
-
return "DateTime";
|
|
234
|
-
case "object":
|
|
235
|
-
return "Dictionary<string, object>";
|
|
236
|
-
default:
|
|
237
|
-
return "object";
|
|
238
|
-
}
|
|
242
|
+
return getCSharpScalarType(field);
|
|
239
243
|
}
|
|
240
244
|
function getCSharpArrayElementType(field) {
|
|
241
245
|
if (field.isNestedSchema && field.nestedModelName) {
|
|
@@ -244,24 +248,11 @@ function getCSharpArrayElementType(field) {
|
|
|
244
248
|
if (field.enumValues?.length) {
|
|
245
249
|
return `${toPascalIdentifier(field.name)}Enum`;
|
|
246
250
|
}
|
|
247
|
-
|
|
248
|
-
case "string":
|
|
249
|
-
return isDateLikeField(field) ? "DateTime" : "string";
|
|
250
|
-
case "number":
|
|
251
|
-
return "decimal";
|
|
252
|
-
case "boolean":
|
|
253
|
-
return "bool";
|
|
254
|
-
case "date":
|
|
255
|
-
return "DateTime";
|
|
256
|
-
case "object":
|
|
257
|
-
return "Dictionary<string, object>";
|
|
258
|
-
default:
|
|
259
|
-
return "object";
|
|
260
|
-
}
|
|
251
|
+
return getCSharpScalarType(field);
|
|
261
252
|
}
|
|
262
253
|
function getCSharpPropertyType(field) {
|
|
263
254
|
const baseType = getCSharpFieldBaseType(field);
|
|
264
|
-
return field.isOptional ? `${baseType}?` : baseType;
|
|
255
|
+
return field.isOptional || field.isNullable ? `${baseType}?` : baseType;
|
|
265
256
|
}
|
|
266
257
|
function getCSharpOptionType(field) {
|
|
267
258
|
return `Option<${getCSharpPropertyType(field)}>`;
|
|
@@ -318,11 +309,96 @@ function buildCSharpEnumToJsonCases(field, members) {
|
|
|
318
309
|
.map((member) => ` if (value == ${enumType}.${member.name})\n return ${csharpStringLiteral(member.value)};`)
|
|
319
310
|
.join("\n\n");
|
|
320
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
|
+
}
|
|
321
397
|
function generateLegacyCompatibleCSharpModelSource(model) {
|
|
322
398
|
const requiredFields = model.fields.filter((field) => !field.isOptional);
|
|
323
399
|
const optionalFields = model.fields.filter((field) => field.isOptional);
|
|
324
400
|
const constructorParams = [
|
|
325
|
-
...requiredFields.map((field) => `${
|
|
401
|
+
...requiredFields.map((field) => `${getCSharpPropertyType(field)} ${field.name}`),
|
|
326
402
|
...optionalFields.map((field) => `${getCSharpOptionType(field)} ${field.name} = default`),
|
|
327
403
|
].join(", ");
|
|
328
404
|
const constructorAssignments = [
|
|
@@ -338,7 +414,7 @@ function generateLegacyCompatibleCSharpModelSource(model) {
|
|
|
338
414
|
return ` /// <summary>
|
|
339
415
|
/// Defines ${toPascalIdentifier(field.name)}
|
|
340
416
|
/// </summary>
|
|
341
|
-
[JsonConverter(typeof(
|
|
417
|
+
[JsonConverter(typeof(${enumType}JsonConverter))]
|
|
342
418
|
public enum ${enumType}
|
|
343
419
|
{
|
|
344
420
|
${buildCSharpEnumMembers(enumMembers)}
|
|
@@ -359,6 +435,22 @@ ${buildCSharpEnumFromStringCases(field, enumMembers, true)}
|
|
|
359
435
|
${buildCSharpEnumToJsonCases(field, enumMembers)}
|
|
360
436
|
|
|
361
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));
|
|
362
454
|
}`;
|
|
363
455
|
})
|
|
364
456
|
.join("\n\n");
|
|
@@ -383,7 +475,8 @@ ${buildCSharpEnumToJsonCases(field, enumMembers)}
|
|
|
383
475
|
/// <summary>
|
|
384
476
|
/// Gets or Sets ${propertyName}
|
|
385
477
|
/// </summary>
|
|
386
|
-
[JsonPropertyName("${field.name}")]
|
|
478
|
+
[JsonPropertyName("${field.name}")]${field.isNullable ? "" : `
|
|
479
|
+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`}
|
|
387
480
|
public ${propertyType} ${propertyName} { get { return this.${propertyName}Option; } set { this.${propertyName}Option = new(value); } }`;
|
|
388
481
|
})
|
|
389
482
|
.join("\n\n");
|
|
@@ -405,8 +498,11 @@ ${buildCSharpEnumToJsonCases(field, enumMembers)}
|
|
|
405
498
|
using System;
|
|
406
499
|
using System.Collections.Generic;
|
|
407
500
|
using System.ComponentModel.DataAnnotations;
|
|
501
|
+
using System.Linq;
|
|
408
502
|
using System.Text;
|
|
503
|
+
using System.Text.Json;
|
|
409
504
|
using System.Text.Json.Serialization;
|
|
505
|
+
using System.Text.RegularExpressions;
|
|
410
506
|
using SwallowKitBackendModels.Client;
|
|
411
507
|
|
|
412
508
|
namespace SwallowKitBackendModels.Model
|
|
@@ -416,12 +512,22 @@ namespace SwallowKitBackendModels.Model
|
|
|
416
512
|
/// </summary>
|
|
417
513
|
public partial class ${model.name} : IValidatableObject
|
|
418
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
|
|
419
519
|
[JsonConstructor]
|
|
520
|
+
public ${model.name}()
|
|
521
|
+
{
|
|
522
|
+
OnCreated();
|
|
523
|
+
}
|
|
524
|
+
#pragma warning restore CS8618
|
|
525
|
+
${constructorParams ? `
|
|
420
526
|
public ${model.name}(${constructorParams})
|
|
421
527
|
{
|
|
422
528
|
${constructorAssignments}
|
|
423
529
|
}
|
|
424
|
-
|
|
530
|
+
` : ""}
|
|
425
531
|
partial void OnCreated();
|
|
426
532
|
${enumBlocks ? `\n\n${enumBlocks}` : ""}
|
|
427
533
|
|
|
@@ -438,24 +544,18 @@ ${toStringBody}
|
|
|
438
544
|
|
|
439
545
|
IEnumerable<ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
|
|
440
546
|
{
|
|
441
|
-
|
|
547
|
+
${buildCSharpValidationBody(model)}
|
|
442
548
|
}
|
|
443
549
|
}
|
|
444
550
|
}
|
|
445
551
|
`;
|
|
446
552
|
}
|
|
447
|
-
function
|
|
448
|
-
if (field.isNestedSchema && field.nestedModelName) {
|
|
449
|
-
return field.nestedModelName;
|
|
450
|
-
}
|
|
451
|
-
if (field.isArray) {
|
|
452
|
-
return `List[${getPythonArrayElementType(field)}]`;
|
|
453
|
-
}
|
|
553
|
+
function getPythonScalarType(field) {
|
|
454
554
|
switch (field.type) {
|
|
455
555
|
case "string":
|
|
456
|
-
return
|
|
556
|
+
return field.format === "date-time" ? "datetime" : field.format === "date" ? "date" : "StrictStr";
|
|
457
557
|
case "number":
|
|
458
|
-
return "Union[StrictFloat, StrictInt]";
|
|
558
|
+
return field.isInteger ? "StrictInt" : "Union[StrictFloat, StrictInt]";
|
|
459
559
|
case "boolean":
|
|
460
560
|
return "StrictBool";
|
|
461
561
|
case "date":
|
|
@@ -466,36 +566,128 @@ function getPythonTypeName(field) {
|
|
|
466
566
|
return "Any";
|
|
467
567
|
}
|
|
468
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
|
+
}
|
|
469
589
|
function getPythonArrayElementType(field) {
|
|
470
590
|
if (field.isNestedSchema && field.nestedModelName) {
|
|
471
591
|
return field.nestedModelName;
|
|
472
592
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
return "Dict[str, Any]";
|
|
484
|
-
default:
|
|
485
|
-
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)}]`;
|
|
600
|
+
}
|
|
601
|
+
if (field.isNestedSchema && field.nestedModelName) {
|
|
602
|
+
return field.nestedModelName;
|
|
486
603
|
}
|
|
604
|
+
return getPythonScalarType(field);
|
|
487
605
|
}
|
|
488
606
|
function buildPythonFieldDeclaration(field) {
|
|
489
607
|
const pythonName = toSnakeCase(field.name);
|
|
490
608
|
const typeName = field.isOptional ? `Optional[${getPythonTypeName(field)}]` : getPythonTypeName(field);
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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");
|
|
499
691
|
}
|
|
500
692
|
function buildPythonEnumValidators(model) {
|
|
501
693
|
return model.fields
|
|
@@ -526,7 +718,7 @@ function buildPythonModelImports(model) {
|
|
|
526
718
|
function generateLegacyCompatiblePythonModelSource(model) {
|
|
527
719
|
const fieldDeclarations = model.fields.map((field) => ` ${buildPythonFieldDeclaration(field)}`).join("\n");
|
|
528
720
|
const propertyNames = model.fields.map((field) => `"${field.name}"`).join(", ");
|
|
529
|
-
const validators = buildPythonEnumValidators(model);
|
|
721
|
+
const validators = [buildPythonEnumValidators(model), buildPythonPatternValidators(model)].filter(Boolean).join("\n\n");
|
|
530
722
|
const dictAssignments = model.fields
|
|
531
723
|
.map((field) => ` "${field.name}": obj.get("${field.name}")`)
|
|
532
724
|
.join(",\n");
|
|
@@ -549,9 +741,9 @@ import pprint
|
|
|
549
741
|
import re # noqa: F401
|
|
550
742
|
import json
|
|
551
743
|
|
|
552
|
-
from datetime import datetime
|
|
744
|
+
from datetime import date, datetime
|
|
553
745
|
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictFloat, StrictInt, StrictStr, field_validator
|
|
554
|
-
from typing import Any, ClassVar, Dict, List, Optional, Set, Union
|
|
746
|
+
from typing import Annotated, Any, ClassVar, Dict, List, Optional, Set, Union
|
|
555
747
|
from typing_extensions import Self
|
|
556
748
|
|
|
557
749
|
${buildPythonModelImports(model)}class ${model.name}(BaseModel):
|
|
@@ -575,7 +767,7 @@ ${validators ? `\n\n${validators}` : ""}
|
|
|
575
767
|
|
|
576
768
|
def to_json(self) -> str:
|
|
577
769
|
"""Returns the JSON representation of the model using alias"""
|
|
578
|
-
return
|
|
770
|
+
return self.model_dump_json(by_alias=True, exclude_none=True)
|
|
579
771
|
|
|
580
772
|
@classmethod
|
|
581
773
|
def from_json(cls, json_str: str) -> Optional[Self]:
|
|
@@ -662,6 +854,20 @@ function ensurePythonCodegenProjectFiles(functionsRoot, session, sourceModel) {
|
|
|
662
854
|
}
|
|
663
855
|
return requirementsPath;
|
|
664
856
|
}
|
|
857
|
+
/** Generated backend_models import pydantic v2 at runtime, so the deployed Functions app must install it. */
|
|
858
|
+
exports.PYTHON_SCHEMA_RUNTIME_REQUIREMENT = "pydantic>=2.6,<3";
|
|
859
|
+
function ensurePythonRuntimeRequirement(functionsRoot, session) {
|
|
860
|
+
const requirementsPath = path.join(functionsRoot, "requirements.txt");
|
|
861
|
+
const current = session.fileExists(requirementsPath) ? session.readFile(requirementsPath) : "";
|
|
862
|
+
const declared = current.split(/\r?\n/).some((line) => line.trim().split(/[<>=!~;[ ]/)[0].toLowerCase() === "pydantic");
|
|
863
|
+
if (declared)
|
|
864
|
+
return;
|
|
865
|
+
const prefix = current && !current.endsWith("\n") ? `${current}\n` : current;
|
|
866
|
+
session.writeFile(requirementsPath, `${prefix}${exports.PYTHON_SCHEMA_RUNTIME_REQUIREMENT}\n`, {
|
|
867
|
+
ownership: "extension-point",
|
|
868
|
+
generator: "native-schema",
|
|
869
|
+
});
|
|
870
|
+
}
|
|
665
871
|
async function ensureProjectLocalUvCommand(projectRoot) {
|
|
666
872
|
const uvEnv = { ...process.env };
|
|
667
873
|
const { localUvExecutable } = (0, python_uv_1.getProjectLocalUvPaths)(projectRoot);
|
|
@@ -749,11 +955,16 @@ async function generateCSharpSchemaArtifacts(models, outputDir, functionsRoot, s
|
|
|
749
955
|
}
|
|
750
956
|
async function generatePythonSchemaArtifacts(models, outputDir, functionsRoot, session, sourceModel) {
|
|
751
957
|
ensurePythonCodegenProjectFiles(functionsRoot, session, sourceModel);
|
|
958
|
+
ensurePythonRuntimeRequirement(functionsRoot, session);
|
|
752
959
|
const packageRoot = path.join(outputDir, "backend_models");
|
|
753
960
|
const modelsRoot = path.join(packageRoot, "models");
|
|
754
961
|
writeMergedPythonInitFile(path.join(packageRoot, "__init__.py"), buildGeneratedPythonPackageInitSource(models), session, sourceModel);
|
|
755
962
|
writeMergedPythonInitFile(path.join(modelsRoot, "__init__.py"), buildGeneratedPythonModelsInitSource(models), session, sourceModel);
|
|
756
963
|
for (const model of models) {
|
|
964
|
+
for (const warning of getPythonRegexWarnings(model)) {
|
|
965
|
+
session.addWarning(warning);
|
|
966
|
+
console.warn(`⚠️ ${warning}`);
|
|
967
|
+
}
|
|
757
968
|
const modelPath = getPythonSchemaModelPath(outputDir, model.name);
|
|
758
969
|
session.writeFile(modelPath, generateLegacyCompatiblePythonModelSource(model), {
|
|
759
970
|
ownership: "managed",
|
|
@@ -778,11 +989,13 @@ async function generateLanguageSchemaArtifacts(models, rootModel, functionsDir,
|
|
|
778
989
|
});
|
|
779
990
|
console.log(planning ? `\ud83d\udccb Will create: ${specPath}` : `\u2705 Created: ${specPath}`);
|
|
780
991
|
const outputDir = path.join(functionsRoot, "generated", backendLanguage === "csharp" ? "csharp-models" : "python-models");
|
|
992
|
+
// Inline z.object fields become named classes (e.g. ProbeItemNested) so their shape is preserved.
|
|
993
|
+
const nativeModels = (0, openapi_generator_1.expandInlineObjectModels)(models);
|
|
781
994
|
if (backendLanguage === "csharp") {
|
|
782
|
-
await generateCSharpSchemaArtifacts(
|
|
995
|
+
await generateCSharpSchemaArtifacts(nativeModels, outputDir, functionsRoot, session, rootModel.name);
|
|
783
996
|
}
|
|
784
997
|
else {
|
|
785
|
-
await generatePythonSchemaArtifacts(
|
|
998
|
+
await generatePythonSchemaArtifacts(nativeModels, outputDir, functionsRoot, session, rootModel.name);
|
|
786
999
|
}
|
|
787
1000
|
console.log(planning
|
|
788
1001
|
? `\ud83d\udccb Will generate ${backendLanguage} schema assets: ${outputDir}`
|