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.
Files changed (159) hide show
  1. package/dist/cli/commands/add-auth.js +37 -6
  2. package/dist/cli/commands/add-auth.js.map +1 -1
  3. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  4. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  5. package/dist/cli/commands/configure-mcp.js +42 -0
  6. package/dist/cli/commands/configure-mcp.js.map +1 -0
  7. package/dist/cli/commands/init.d.ts +6 -1
  8. package/dist/cli/commands/init.d.ts.map +1 -1
  9. package/dist/cli/commands/init.js +19 -5
  10. package/dist/cli/commands/init.js.map +1 -1
  11. package/dist/cli/commands/migrations.d.ts.map +1 -1
  12. package/dist/cli/commands/migrations.js +25 -0
  13. package/dist/cli/commands/migrations.js.map +1 -1
  14. package/dist/cli/commands/provision.d.ts +2 -0
  15. package/dist/cli/commands/provision.d.ts.map +1 -1
  16. package/dist/cli/commands/provision.js +45 -16
  17. package/dist/cli/commands/provision.js.map +1 -1
  18. package/dist/cli/commands/scaffold.d.ts +4 -0
  19. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  20. package/dist/cli/commands/scaffold.js +85 -3
  21. package/dist/cli/commands/scaffold.js.map +1 -1
  22. package/dist/cli/index.d.ts +2 -0
  23. package/dist/cli/index.d.ts.map +1 -1
  24. package/dist/cli/index.js +29 -4
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/core/operations/create-model.d.ts +2 -0
  27. package/dist/core/operations/create-model.d.ts.map +1 -1
  28. package/dist/core/operations/create-model.js +60 -8
  29. package/dist/core/operations/create-model.js.map +1 -1
  30. package/dist/core/operations/model-migration.d.ts +22 -0
  31. package/dist/core/operations/model-migration.d.ts.map +1 -0
  32. package/dist/core/operations/model-migration.js +36 -0
  33. package/dist/core/operations/model-migration.js.map +1 -0
  34. package/dist/core/operations/provision-operations.d.ts +19 -1
  35. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  36. package/dist/core/operations/provision-operations.js +62 -35
  37. package/dist/core/operations/provision-operations.js.map +1 -1
  38. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  39. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  40. package/dist/core/operations/scaffold-apply.js +1 -0
  41. package/dist/core/operations/scaffold-apply.js.map +1 -1
  42. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  43. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  44. package/dist/core/operations/scaffold-machine.js.map +1 -1
  45. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  46. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  47. package/dist/core/operations/scaffold-plan.js +3 -1
  48. package/dist/core/operations/scaffold-plan.js.map +1 -1
  49. package/dist/core/project/boundaries.d.ts +5 -2
  50. package/dist/core/project/boundaries.d.ts.map +1 -1
  51. package/dist/core/project/boundaries.js +51 -20
  52. package/dist/core/project/boundaries.js.map +1 -1
  53. package/dist/core/project/capabilities.d.ts +2 -0
  54. package/dist/core/project/capabilities.d.ts.map +1 -1
  55. package/dist/core/project/capabilities.js +29 -5
  56. package/dist/core/project/capabilities.js.map +1 -1
  57. package/dist/core/project/mcp-config.d.ts +26 -0
  58. package/dist/core/project/mcp-config.d.ts.map +1 -0
  59. package/dist/core/project/mcp-config.js +117 -0
  60. package/dist/core/project/mcp-config.js.map +1 -0
  61. package/dist/core/project/react-project.js +1 -1
  62. package/dist/core/project/workflows.d.ts.map +1 -1
  63. package/dist/core/project/workflows.js +23 -5
  64. package/dist/core/project/workflows.js.map +1 -1
  65. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  66. package/dist/core/scaffold/auth-generator.js +18 -19
  67. package/dist/core/scaffold/auth-generator.js.map +1 -1
  68. package/dist/core/scaffold/model-parser.d.ts +23 -0
  69. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  70. package/dist/core/scaffold/model-parser.js +407 -137
  71. package/dist/core/scaffold/model-parser.js.map +1 -1
  72. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  73. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  74. package/dist/core/scaffold/native-schema-generator.js +289 -76
  75. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  76. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  77. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  78. package/dist/core/scaffold/openapi-generator.js +53 -3
  79. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  80. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  81. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  82. package/dist/core/scaffold/ui-generator.js +68 -7
  83. package/dist/core/scaffold/ui-generator.js.map +1 -1
  84. package/dist/core/verify/index.d.ts +4 -3
  85. package/dist/core/verify/index.d.ts.map +1 -1
  86. package/dist/core/verify/index.js +122 -5
  87. package/dist/core/verify/index.js.map +1 -1
  88. package/dist/machine/contracts.d.ts +1 -0
  89. package/dist/machine/contracts.d.ts.map +1 -1
  90. package/dist/machine/errors.d.ts +3 -2
  91. package/dist/machine/errors.d.ts.map +1 -1
  92. package/dist/machine/errors.js +2 -1
  93. package/dist/machine/errors.js.map +1 -1
  94. package/dist/machine/index.d.ts.map +1 -1
  95. package/dist/machine/index.js +42 -17
  96. package/dist/machine/index.js.map +1 -1
  97. package/dist/mcp/index.d.ts +4 -2
  98. package/dist/mcp/index.d.ts.map +1 -1
  99. package/dist/mcp/index.js +154 -15
  100. package/dist/mcp/index.js.map +1 -1
  101. package/dist/mcp/version-selection.d.ts +55 -0
  102. package/dist/mcp/version-selection.d.ts.map +1 -0
  103. package/dist/mcp/version-selection.js +152 -0
  104. package/dist/mcp/version-selection.js.map +1 -0
  105. package/dist/utils/azure-cli.d.ts +44 -0
  106. package/dist/utils/azure-cli.d.ts.map +1 -0
  107. package/dist/utils/azure-cli.js +164 -0
  108. package/dist/utils/azure-cli.js.map +1 -0
  109. package/dist/utils/semver.d.ts +14 -0
  110. package/dist/utils/semver.d.ts.map +1 -0
  111. package/dist/utils/semver.js +61 -0
  112. package/dist/utils/semver.js.map +1 -0
  113. package/package.json +6 -3
  114. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  115. package/src/__tests__/agent-loop-phase34.test.ts +124 -0
  116. package/src/__tests__/azure-cli.test.ts +199 -0
  117. package/src/__tests__/boundaries.test.ts +42 -0
  118. package/src/__tests__/csharp-schema.test.ts +261 -0
  119. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  120. package/src/__tests__/mcp-version.test.ts +262 -0
  121. package/src/__tests__/mcp.test.ts +64 -3
  122. package/src/__tests__/multi-auth.test.ts +115 -4
  123. package/src/__tests__/plan-apply.test.ts +170 -0
  124. package/src/__tests__/schema-analysis.test.ts +329 -0
  125. package/src/cli/commands/add-auth.ts +36 -6
  126. package/src/cli/commands/configure-mcp.ts +54 -0
  127. package/src/cli/commands/init.ts +23 -8
  128. package/src/cli/commands/migrations.ts +22 -0
  129. package/src/cli/commands/provision.ts +53 -17
  130. package/src/cli/commands/scaffold.ts +105 -3
  131. package/src/cli/index.ts +28 -5
  132. package/src/core/operations/create-model.ts +51 -13
  133. package/src/core/operations/model-migration.ts +52 -0
  134. package/src/core/operations/provision-operations.ts +86 -42
  135. package/src/core/operations/scaffold-apply.ts +2 -0
  136. package/src/core/operations/scaffold-machine.ts +1 -0
  137. package/src/core/operations/scaffold-plan.ts +5 -1
  138. package/src/core/project/agent-templates/AGENTS.md +67 -6
  139. package/src/core/project/agent-templates/CLAUDE.md +7 -1
  140. package/src/core/project/agent-templates/copilot-instructions.md +7 -1
  141. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  142. package/src/core/project/boundaries.ts +62 -20
  143. package/src/core/project/capabilities.ts +32 -5
  144. package/src/core/project/mcp-config.ts +84 -0
  145. package/src/core/project/react-project.ts +1 -1
  146. package/src/core/project/workflows.ts +23 -5
  147. package/src/core/scaffold/auth-generator.ts +18 -19
  148. package/src/core/scaffold/model-parser.ts +413 -129
  149. package/src/core/scaffold/native-schema-generator.ts +313 -78
  150. package/src/core/scaffold/openapi-generator.ts +63 -5
  151. package/src/core/scaffold/ui-generator.ts +70 -8
  152. package/src/core/verify/index.ts +118 -6
  153. package/src/machine/contracts.ts +1 -0
  154. package/src/machine/errors.ts +4 -2
  155. package/src/machine/index.ts +45 -20
  156. package/src/mcp/index.ts +173 -18
  157. package/src/mcp/version-selection.ts +163 -0
  158. package/src/utils/azure-cli.ts +154 -0
  159. 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
- function isDateLikeField(field) {
166
- return field.type === "date" || (field.type === "string" && field.name.toLowerCase().endsWith("at"));
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
- if (field.isArray) {
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
- switch (field.type) {
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) => `${getCSharpFieldBaseType(field)} ${field.name}`),
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(JsonStringEnumConverter))]
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
- yield break;
547
+ ${buildCSharpValidationBody(model)}
442
548
  }
443
549
  }
444
550
  }
445
551
  `;
446
552
  }
447
- function getPythonTypeName(field) {
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 isDateLikeField(field) ? "datetime" : "StrictStr";
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
- switch (field.type) {
474
- case "string":
475
- return isDateLikeField(field) ? "datetime" : "StrictStr";
476
- case "number":
477
- return "Union[StrictFloat, StrictInt]";
478
- case "boolean":
479
- return "StrictBool";
480
- case "date":
481
- return "datetime";
482
- case "object":
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 aliasSuffix = pythonName !== field.name ? `, alias="${field.name}"` : "";
492
- if (field.isOptional) {
493
- return `${pythonName}: ${typeName} = Field(default=None${aliasSuffix})`;
494
- }
495
- if (aliasSuffix) {
496
- return `${pythonName}: ${typeName} = Field(${aliasSuffix.slice(2)})`;
497
- }
498
- return `${pythonName}: ${typeName}`;
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 json.dumps(self.to_dict())
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(models, outputDir, functionsRoot, session, rootModel.name);
995
+ await generateCSharpSchemaArtifacts(nativeModels, outputDir, functionsRoot, session, rootModel.name);
783
996
  }
784
997
  else {
785
- await generatePythonSchemaArtifacts(models, outputDir, functionsRoot, session, rootModel.name);
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}`