swallowkit 2.0.0-alpha.1 → 2.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +14 -0
- package/README.md +14 -0
- 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/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 +8 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +67 -132
- 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 +5 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +278 -55
- 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 +43 -5
- 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/migration-generator.d.ts.map +1 -1
- package/dist/core/operations/migration-generator.js +12 -1
- package/dist/core/operations/migration-generator.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-deployment.d.ts +52 -0
- package/dist/core/operations/provision-deployment.d.ts.map +1 -0
- package/dist/core/operations/provision-deployment.js +164 -0
- package/dist/core/operations/provision-deployment.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +62 -6
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +653 -115
- 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 +51 -20
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +10 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +38 -6
- 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 +46 -25
- package/dist/core/project/react-project.js.map +1 -1
- package/dist/core/project/state.d.ts +7 -0
- package/dist/core/project/state.d.ts.map +1 -1
- package/dist/core/project/state.js +86 -0
- package/dist/core/project/state.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +53 -7
- 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 +26 -27
- 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 +294 -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 +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 +59 -28
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +6 -4
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +210 -25
- 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 +56 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +326 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/azure-diagnostics.d.ts +16 -0
- package/dist/utils/azure-diagnostics.d.ts.map +1 -0
- package/dist/utils/azure-diagnostics.js +68 -0
- package/dist/utils/azure-diagnostics.js.map +1 -0
- package/dist/utils/package-manager.js +2 -2
- 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 +6 -3
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-instructions.test.ts +5 -0
- package/src/__tests__/agent-loop-phase34.test.ts +143 -8
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/azure-cli.test.ts +202 -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__/fixtures/az wrapper.cmd +2 -0
- package/src/__tests__/fixtures/azure-cli.cjs +153 -0
- package/src/__tests__/fixtures/provision-runner.cjs +28 -0
- package/src/__tests__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/home-page.test.ts +50 -0
- package/src/__tests__/init.test.ts +116 -0
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +120 -3
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/package-manager.test.ts +17 -2
- package/src/__tests__/plan-apply.test.ts +179 -0
- package/src/__tests__/provision-runtime.test.ts +722 -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/add-auth.ts +36 -6
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +40 -31
- package/src/cli/commands/init.ts +73 -138
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +294 -56
- package/src/cli/commands/scaffold.ts +105 -3
- package/src/cli/index.ts +40 -7
- package/src/core/operations/create-model.ts +51 -13
- package/src/core/operations/migration-generator.ts +14 -1
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-deployment.ts +183 -0
- package/src/core/operations/provision-operations.ts +662 -154
- 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 +89 -6
- package/src/core/project/agent-templates/CLAUDE.md +14 -1
- package/src/core/project/agent-templates/copilot-instructions.md +15 -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 +49 -6
- 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 +50 -28
- package/src/core/project/state.ts +70 -0
- package/src/core/project/workflows.ts +53 -7
- package/src/core/scaffold/auth-generator.ts +26 -27
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +319 -103
- 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 +65 -34
- package/src/mcp/index.ts +221 -31
- package/src/mcp/version-selection.ts +163 -0
- package/src/utils/azure-cli.ts +315 -0
- package/src/utils/azure-diagnostics.ts +75 -0
- package/src/utils/package-manager.ts +2 -2
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- package/src/utils/semver.ts +59 -0
|
@@ -5,12 +5,12 @@ import { spawn, spawnSync } from "child_process";
|
|
|
5
5
|
import { BackendLanguage } from "../../types";
|
|
6
6
|
import { FileOperationSession } from "../operations/file-session";
|
|
7
7
|
import { ModelInfo, toKebabCase } from "./model-parser";
|
|
8
|
-
import { generateOpenApiDocument } from "./openapi-generator";
|
|
8
|
+
import { expandInlineObjectModels, generateOpenApiDocument } from "./openapi-generator";
|
|
9
9
|
import {
|
|
10
|
-
buildProjectLocalUvEnv,
|
|
11
10
|
buildProjectLocalUvInstallerEnv,
|
|
12
11
|
buildUvPipInstallArgs,
|
|
13
|
-
|
|
12
|
+
buildPythonCodegenInstallEnv,
|
|
13
|
+
ensureUvVirtualEnvironment,
|
|
14
14
|
getProjectLocalUvInstallerCommand,
|
|
15
15
|
getProjectLocalUvPaths,
|
|
16
16
|
getPythonProjectRoot,
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
export const NSWAG_CONSOLECORE_VERSION = "14.7.1";
|
|
20
20
|
export const PYTHON_SCHEMA_CODEGEN_REQUIREMENT = "datamodel-code-generator>=0.44.0,<1.0.0";
|
|
21
21
|
const PYTHON_OUTPUT_MODEL_TYPE = "pydantic_v2.BaseModel";
|
|
22
|
+
// Preserve model syntax compatibility with existing deployments, not the tool's interpreter.
|
|
23
|
+
const PYTHON_MODEL_SYNTAX_VERSION = "3.11";
|
|
22
24
|
|
|
23
25
|
function getMachineAwareStdio(): "inherit" | "pipe" {
|
|
24
26
|
return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
|
|
@@ -152,8 +154,22 @@ function csharpStringLiteral(value: string): string {
|
|
|
152
154
|
return JSON.stringify(value);
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
/** Date/time types are used only when the Zod schema declares them (z.iso.datetime(), z.iso.date(), z.date()). */
|
|
158
|
+
function getCSharpScalarType(field: ModelInfo["fields"][number]): string {
|
|
159
|
+
switch (field.type) {
|
|
160
|
+
case "string":
|
|
161
|
+
return field.format === "date-time" ? "DateTime" : field.format === "date" ? "DateOnly" : "string";
|
|
162
|
+
case "number":
|
|
163
|
+
return field.isInteger ? "long" : "double";
|
|
164
|
+
case "boolean":
|
|
165
|
+
return "bool";
|
|
166
|
+
case "date":
|
|
167
|
+
return "DateTime";
|
|
168
|
+
case "object":
|
|
169
|
+
return "Dictionary<string, object>";
|
|
170
|
+
default:
|
|
171
|
+
return "object";
|
|
172
|
+
}
|
|
157
173
|
}
|
|
158
174
|
|
|
159
175
|
export function getCSharpSchemaModelPath(outputDir: string, modelName: string): string {
|
|
@@ -201,7 +217,7 @@ export function getPythonNativeGeneratorArgs(specPath: string, outputPath: strin
|
|
|
201
217
|
"--output-model-type",
|
|
202
218
|
PYTHON_OUTPUT_MODEL_TYPE,
|
|
203
219
|
"--target-python-version",
|
|
204
|
-
|
|
220
|
+
PYTHON_MODEL_SYNTAX_VERSION,
|
|
205
221
|
"--disable-timestamp",
|
|
206
222
|
"--use-union-operator",
|
|
207
223
|
"--collapse-root-models",
|
|
@@ -209,6 +225,11 @@ export function getPythonNativeGeneratorArgs(specPath: string, outputPath: strin
|
|
|
209
225
|
}
|
|
210
226
|
|
|
211
227
|
function getCSharpFieldBaseType(field: ModelInfo["fields"][number]): string {
|
|
228
|
+
// Arrays first: arrays of nested models or enums are lists of the element type.
|
|
229
|
+
if (field.isArray) {
|
|
230
|
+
return `List<${getCSharpArrayElementType(field)}>`;
|
|
231
|
+
}
|
|
232
|
+
|
|
212
233
|
if (field.isNestedSchema && field.nestedModelName) {
|
|
213
234
|
return field.nestedModelName;
|
|
214
235
|
}
|
|
@@ -217,24 +238,7 @@ function getCSharpFieldBaseType(field: ModelInfo["fields"][number]): string {
|
|
|
217
238
|
return `${toPascalIdentifier(field.name)}Enum`;
|
|
218
239
|
}
|
|
219
240
|
|
|
220
|
-
|
|
221
|
-
return `List<${getCSharpArrayElementType(field)}>`;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
switch (field.type) {
|
|
225
|
-
case "string":
|
|
226
|
-
return isDateLikeField(field) ? "DateTime" : "string";
|
|
227
|
-
case "number":
|
|
228
|
-
return "decimal";
|
|
229
|
-
case "boolean":
|
|
230
|
-
return "bool";
|
|
231
|
-
case "date":
|
|
232
|
-
return "DateTime";
|
|
233
|
-
case "object":
|
|
234
|
-
return "Dictionary<string, object>";
|
|
235
|
-
default:
|
|
236
|
-
return "object";
|
|
237
|
-
}
|
|
241
|
+
return getCSharpScalarType(field);
|
|
238
242
|
}
|
|
239
243
|
|
|
240
244
|
function getCSharpArrayElementType(field: ModelInfo["fields"][number]): string {
|
|
@@ -246,25 +250,12 @@ function getCSharpArrayElementType(field: ModelInfo["fields"][number]): string {
|
|
|
246
250
|
return `${toPascalIdentifier(field.name)}Enum`;
|
|
247
251
|
}
|
|
248
252
|
|
|
249
|
-
|
|
250
|
-
case "string":
|
|
251
|
-
return isDateLikeField(field) ? "DateTime" : "string";
|
|
252
|
-
case "number":
|
|
253
|
-
return "decimal";
|
|
254
|
-
case "boolean":
|
|
255
|
-
return "bool";
|
|
256
|
-
case "date":
|
|
257
|
-
return "DateTime";
|
|
258
|
-
case "object":
|
|
259
|
-
return "Dictionary<string, object>";
|
|
260
|
-
default:
|
|
261
|
-
return "object";
|
|
262
|
-
}
|
|
253
|
+
return getCSharpScalarType(field);
|
|
263
254
|
}
|
|
264
255
|
|
|
265
256
|
function getCSharpPropertyType(field: ModelInfo["fields"][number]): string {
|
|
266
257
|
const baseType = getCSharpFieldBaseType(field);
|
|
267
|
-
return field.isOptional ? `${baseType}?` : baseType;
|
|
258
|
+
return field.isOptional || field.isNullable ? `${baseType}?` : baseType;
|
|
268
259
|
}
|
|
269
260
|
|
|
270
261
|
function getCSharpOptionType(field: ModelInfo["fields"][number]): string {
|
|
@@ -331,12 +322,120 @@ function buildCSharpEnumToJsonCases(field: ModelInfo["fields"][number], members:
|
|
|
331
322
|
.join("\n\n");
|
|
332
323
|
}
|
|
333
324
|
|
|
325
|
+
function csharpNumberLiteral(value: number): string {
|
|
326
|
+
return Number.isInteger(value) ? `${value}` : `${value}d`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function csharpRegexOptions(flags: string | undefined): string {
|
|
330
|
+
const options = [
|
|
331
|
+
flags?.includes("i") ? "RegexOptions.IgnoreCase" : null,
|
|
332
|
+
flags?.includes("m") ? "RegexOptions.Multiline" : null,
|
|
333
|
+
flags?.includes("s") ? "RegexOptions.Singleline" : null,
|
|
334
|
+
].filter(Boolean);
|
|
335
|
+
return options.length > 0 ? options.join(" | ") : "RegexOptions.None";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function isCSharpReferenceType(field: ModelInfo["fields"][number]): boolean {
|
|
339
|
+
if (field.isArray || field.isNestedSchema || field.type === "object") return true;
|
|
340
|
+
if (field.enumValues?.length) return false;
|
|
341
|
+
return field.type === "string" && field.format !== "date-time" && field.format !== "date";
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Translate Zod constraints into IValidatableObject checks. Regexes use search semantics like
|
|
346
|
+
* Zod's RegExp.test. String formats (email/uuid/url) and value-type presence stay with Zod.
|
|
347
|
+
*/
|
|
348
|
+
function buildCSharpValueChecks(
|
|
349
|
+
field: ModelInfo["fields"][number],
|
|
350
|
+
accessor: string,
|
|
351
|
+
member: string,
|
|
352
|
+
optionalValue: boolean
|
|
353
|
+
): string[] {
|
|
354
|
+
const constraints = field.constraints ?? {};
|
|
355
|
+
const checks: string[] = [];
|
|
356
|
+
const add = (condition: string, message: string) =>
|
|
357
|
+
checks.push(`if (${condition})\n yield return new ValidationResult(${csharpStringLiteral(message)}, new[] { ${member} });`);
|
|
358
|
+
|
|
359
|
+
if (field.isNestedSchema && field.nestedModelName) {
|
|
360
|
+
// Pattern variables in an if condition stay in scope for the enclosing block, so keep names unique.
|
|
361
|
+
const variable = `validated${toPascalIdentifier(field.name)}${accessor === "item" ? "Item" : ""}`;
|
|
362
|
+
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());`);
|
|
363
|
+
return checks;
|
|
364
|
+
}
|
|
365
|
+
if (field.enumValues?.length) return checks;
|
|
366
|
+
|
|
367
|
+
if (field.type === "string" && field.format !== "date-time" && field.format !== "date") {
|
|
368
|
+
if (constraints.minLength !== undefined) add(`${accessor} != null && ${accessor}.Length < ${constraints.minLength}`, `${field.name} must contain at least ${constraints.minLength} character(s).`);
|
|
369
|
+
if (constraints.maxLength !== undefined) add(`${accessor} != null && ${accessor}.Length > ${constraints.maxLength}`, `${field.name} must contain at most ${constraints.maxLength} character(s).`);
|
|
370
|
+
if (constraints.pattern !== undefined) {
|
|
371
|
+
add(
|
|
372
|
+
`${accessor} != null && !Regex.IsMatch(${accessor}, ${csharpStringLiteral(constraints.pattern)}, ${csharpRegexOptions(constraints.patternFlags)})`,
|
|
373
|
+
`${field.name} must match /${constraints.pattern}/${constraints.patternFlags ?? ""}.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (field.type === "number") {
|
|
379
|
+
const value = optionalValue ? `${accessor}.Value` : accessor;
|
|
380
|
+
const guard = optionalValue ? `${accessor}.HasValue && ` : "";
|
|
381
|
+
if (constraints.minimum !== undefined) {
|
|
382
|
+
add(`${guard}${value} ${constraints.exclusiveMinimum ? "<=" : "<"} ${csharpNumberLiteral(constraints.minimum)}`,
|
|
383
|
+
`${field.name} must be ${constraints.exclusiveMinimum ? "greater than" : "greater than or equal to"} ${constraints.minimum}.`);
|
|
384
|
+
}
|
|
385
|
+
if (constraints.maximum !== undefined) {
|
|
386
|
+
add(`${guard}${value} ${constraints.exclusiveMaximum ? ">=" : ">"} ${csharpNumberLiteral(constraints.maximum)}`,
|
|
387
|
+
`${field.name} must be ${constraints.exclusiveMaximum ? "less than" : "less than or equal to"} ${constraints.maximum}.`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return checks;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function buildCSharpValidationBody(model: ModelInfo): string {
|
|
394
|
+
const blocks: string[] = [];
|
|
395
|
+
for (const field of model.fields) {
|
|
396
|
+
const propertyName = toPascalIdentifier(field.name);
|
|
397
|
+
const member = csharpStringLiteral(field.name);
|
|
398
|
+
const constraints = field.constraints ?? {};
|
|
399
|
+
|
|
400
|
+
// .nullable() fields accept null in Zod, so only non-nullable required references are rejected.
|
|
401
|
+
if (!field.isOptional && !field.isNullable && isCSharpReferenceType(field)) {
|
|
402
|
+
blocks.push(`if (${propertyName} is null)\n yield return new ValidationResult(${csharpStringLiteral(`${field.name} is required.`)}, new[] { ${member} });`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (field.isArray) {
|
|
406
|
+
if (constraints.minItems !== undefined) {
|
|
407
|
+
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} });`);
|
|
408
|
+
}
|
|
409
|
+
if (constraints.maxItems !== undefined) {
|
|
410
|
+
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} });`);
|
|
411
|
+
}
|
|
412
|
+
const itemChecks = buildCSharpValueChecks(
|
|
413
|
+
{ ...field, isArray: false },
|
|
414
|
+
"item",
|
|
415
|
+
`${member} + "[" + index + "]"`,
|
|
416
|
+
false
|
|
417
|
+
);
|
|
418
|
+
if (itemChecks.length > 0) {
|
|
419
|
+
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 }`);
|
|
420
|
+
}
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const optionalValue = (field.isOptional || field.isNullable === true) && !isCSharpReferenceType(field);
|
|
425
|
+
blocks.push(...buildCSharpValueChecks(field, propertyName, member, optionalValue));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return blocks.length > 0
|
|
429
|
+
? blocks.map((block) => ` ${block}`).join("\n\n")
|
|
430
|
+
: " yield break;";
|
|
431
|
+
}
|
|
432
|
+
|
|
334
433
|
function generateLegacyCompatibleCSharpModelSource(model: ModelInfo): string {
|
|
335
434
|
const requiredFields = model.fields.filter((field) => !field.isOptional);
|
|
336
435
|
const optionalFields = model.fields.filter((field) => field.isOptional);
|
|
337
436
|
|
|
338
437
|
const constructorParams = [
|
|
339
|
-
...requiredFields.map((field) => `${
|
|
438
|
+
...requiredFields.map((field) => `${getCSharpPropertyType(field)} ${field.name}`),
|
|
340
439
|
...optionalFields.map((field) => `${getCSharpOptionType(field)} ${field.name} = default`),
|
|
341
440
|
].join(", ");
|
|
342
441
|
|
|
@@ -354,7 +453,7 @@ function generateLegacyCompatibleCSharpModelSource(model: ModelInfo): string {
|
|
|
354
453
|
return ` /// <summary>
|
|
355
454
|
/// Defines ${toPascalIdentifier(field.name)}
|
|
356
455
|
/// </summary>
|
|
357
|
-
[JsonConverter(typeof(
|
|
456
|
+
[JsonConverter(typeof(${enumType}JsonConverter))]
|
|
358
457
|
public enum ${enumType}
|
|
359
458
|
{
|
|
360
459
|
${buildCSharpEnumMembers(enumMembers)}
|
|
@@ -375,6 +474,22 @@ ${buildCSharpEnumFromStringCases(field, enumMembers, true)}
|
|
|
375
474
|
${buildCSharpEnumToJsonCases(field, enumMembers)}
|
|
376
475
|
|
|
377
476
|
throw new NotImplementedException($"Value could not be handled: '{value}'");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/// <summary>
|
|
480
|
+
/// Reads and writes ${enumType} with the exact Zod enum values.
|
|
481
|
+
/// </summary>
|
|
482
|
+
public sealed class ${enumType}JsonConverter : JsonConverter<${enumType}>
|
|
483
|
+
{
|
|
484
|
+
public override ${enumType} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
485
|
+
{
|
|
486
|
+
var value = reader.GetString();
|
|
487
|
+
var parsed = value == null ? null : ${enumType}FromStringOrDefault(value);
|
|
488
|
+
return parsed ?? throw new JsonException($"Unsupported ${enumType} value: '{value}'");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
public override void Write(Utf8JsonWriter writer, ${enumType} value, JsonSerializerOptions options)
|
|
492
|
+
=> writer.WriteStringValue(${enumType}ToJsonValue(value));
|
|
378
493
|
}`;
|
|
379
494
|
})
|
|
380
495
|
.join("\n\n");
|
|
@@ -402,7 +517,8 @@ ${buildCSharpEnumToJsonCases(field, enumMembers)}
|
|
|
402
517
|
/// <summary>
|
|
403
518
|
/// Gets or Sets ${propertyName}
|
|
404
519
|
/// </summary>
|
|
405
|
-
[JsonPropertyName("${field.name}")]
|
|
520
|
+
[JsonPropertyName("${field.name}")]${field.isNullable ? "" : `
|
|
521
|
+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`}
|
|
406
522
|
public ${propertyType} ${propertyName} { get { return this.${propertyName}Option; } set { this.${propertyName}Option = new(value); } }`;
|
|
407
523
|
})
|
|
408
524
|
.join("\n\n");
|
|
@@ -426,8 +542,11 @@ ${buildCSharpEnumToJsonCases(field, enumMembers)}
|
|
|
426
542
|
using System;
|
|
427
543
|
using System.Collections.Generic;
|
|
428
544
|
using System.ComponentModel.DataAnnotations;
|
|
545
|
+
using System.Linq;
|
|
429
546
|
using System.Text;
|
|
547
|
+
using System.Text.Json;
|
|
430
548
|
using System.Text.Json.Serialization;
|
|
549
|
+
using System.Text.RegularExpressions;
|
|
431
550
|
using SwallowKitBackendModels.Client;
|
|
432
551
|
|
|
433
552
|
namespace SwallowKitBackendModels.Model
|
|
@@ -437,12 +556,22 @@ namespace SwallowKitBackendModels.Model
|
|
|
437
556
|
/// </summary>
|
|
438
557
|
public partial class ${model.name} : IValidatableObject
|
|
439
558
|
{
|
|
559
|
+
/// <summary>
|
|
560
|
+
/// Used by System.Text.Json. Optional values are marked as set only when present in the JSON.
|
|
561
|
+
/// </summary>
|
|
562
|
+
#pragma warning disable CS8618
|
|
440
563
|
[JsonConstructor]
|
|
564
|
+
public ${model.name}()
|
|
565
|
+
{
|
|
566
|
+
OnCreated();
|
|
567
|
+
}
|
|
568
|
+
#pragma warning restore CS8618
|
|
569
|
+
${constructorParams ? `
|
|
441
570
|
public ${model.name}(${constructorParams})
|
|
442
571
|
{
|
|
443
572
|
${constructorAssignments}
|
|
444
573
|
}
|
|
445
|
-
|
|
574
|
+
` : ""}
|
|
446
575
|
partial void OnCreated();
|
|
447
576
|
${enumBlocks ? `\n\n${enumBlocks}` : ""}
|
|
448
577
|
|
|
@@ -459,27 +588,19 @@ ${toStringBody}
|
|
|
459
588
|
|
|
460
589
|
IEnumerable<ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
|
|
461
590
|
{
|
|
462
|
-
|
|
591
|
+
${buildCSharpValidationBody(model)}
|
|
463
592
|
}
|
|
464
593
|
}
|
|
465
594
|
}
|
|
466
595
|
`;
|
|
467
596
|
}
|
|
468
597
|
|
|
469
|
-
function
|
|
470
|
-
if (field.isNestedSchema && field.nestedModelName) {
|
|
471
|
-
return field.nestedModelName;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
if (field.isArray) {
|
|
475
|
-
return `List[${getPythonArrayElementType(field)}]`;
|
|
476
|
-
}
|
|
477
|
-
|
|
598
|
+
function getPythonScalarType(field: ModelInfo["fields"][number]): string {
|
|
478
599
|
switch (field.type) {
|
|
479
600
|
case "string":
|
|
480
|
-
return
|
|
601
|
+
return field.format === "date-time" ? "datetime" : field.format === "date" ? "date" : "StrictStr";
|
|
481
602
|
case "number":
|
|
482
|
-
return "Union[StrictFloat, StrictInt]";
|
|
603
|
+
return field.isInteger ? "StrictInt" : "Union[StrictFloat, StrictInt]";
|
|
483
604
|
case "boolean":
|
|
484
605
|
return "StrictBool";
|
|
485
606
|
case "date":
|
|
@@ -491,43 +612,137 @@ function getPythonTypeName(field: ModelInfo["fields"][number]): string {
|
|
|
491
612
|
}
|
|
492
613
|
}
|
|
493
614
|
|
|
615
|
+
/** pydantic Field bounds for a scalar value (string length or numeric range). */
|
|
616
|
+
function getPythonValueConstraintArgs(field: ModelInfo["fields"][number]): string[] {
|
|
617
|
+
const constraints = field.constraints ?? {};
|
|
618
|
+
if (field.enumValues?.length || field.isNestedSchema) return [];
|
|
619
|
+
const args: string[] = [];
|
|
620
|
+
if (field.type === "string" && field.format !== "date-time" && field.format !== "date") {
|
|
621
|
+
if (constraints.minLength !== undefined) args.push(`min_length=${constraints.minLength}`);
|
|
622
|
+
if (constraints.maxLength !== undefined) args.push(`max_length=${constraints.maxLength}`);
|
|
623
|
+
}
|
|
624
|
+
if (field.type === "number") {
|
|
625
|
+
if (constraints.minimum !== undefined) args.push(`${constraints.exclusiveMinimum ? "gt" : "ge"}=${constraints.minimum}`);
|
|
626
|
+
if (constraints.maximum !== undefined) args.push(`${constraints.exclusiveMaximum ? "lt" : "le"}=${constraints.maximum}`);
|
|
627
|
+
}
|
|
628
|
+
return args;
|
|
629
|
+
}
|
|
630
|
+
|
|
494
631
|
function getPythonArrayElementType(field: ModelInfo["fields"][number]): string {
|
|
495
632
|
if (field.isNestedSchema && field.nestedModelName) {
|
|
496
633
|
return field.nestedModelName;
|
|
497
634
|
}
|
|
498
635
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
return "datetime";
|
|
508
|
-
case "object":
|
|
509
|
-
return "Dict[str, Any]";
|
|
510
|
-
default:
|
|
511
|
-
return "Any";
|
|
636
|
+
const elementType = getPythonScalarType(field);
|
|
637
|
+
const elementArgs = getPythonValueConstraintArgs(field);
|
|
638
|
+
return elementArgs.length > 0 ? `Annotated[${elementType}, Field(${elementArgs.join(", ")})]` : elementType;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function getPythonTypeName(field: ModelInfo["fields"][number]): string {
|
|
642
|
+
if (field.isArray) {
|
|
643
|
+
return `List[${getPythonArrayElementType(field)}]`;
|
|
512
644
|
}
|
|
645
|
+
|
|
646
|
+
if (field.isNestedSchema && field.nestedModelName) {
|
|
647
|
+
return field.nestedModelName;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return getPythonScalarType(field);
|
|
513
651
|
}
|
|
514
652
|
|
|
515
653
|
function buildPythonFieldDeclaration(field: ModelInfo["fields"][number]): string {
|
|
516
654
|
const pythonName = toSnakeCase(field.name);
|
|
517
655
|
const typeName = field.isOptional ? `Optional[${getPythonTypeName(field)}]` : getPythonTypeName(field);
|
|
518
|
-
const
|
|
656
|
+
const fieldArgs = [
|
|
657
|
+
...(field.isOptional ? ["default=None"] : []),
|
|
658
|
+
...(pythonName !== field.name ? [`alias="${field.name}"`] : []),
|
|
659
|
+
...(field.isArray
|
|
660
|
+
? [
|
|
661
|
+
...(field.constraints?.minItems !== undefined ? [`min_length=${field.constraints.minItems}`] : []),
|
|
662
|
+
...(field.constraints?.maxItems !== undefined ? [`max_length=${field.constraints.maxItems}`] : []),
|
|
663
|
+
]
|
|
664
|
+
: getPythonValueConstraintArgs(field)),
|
|
665
|
+
];
|
|
519
666
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
667
|
+
return fieldArgs.length > 0
|
|
668
|
+
? `${pythonName}: ${typeName} = Field(${fieldArgs.join(", ")})`
|
|
669
|
+
: `${pythonName}: ${typeName}`;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function buildPythonRegexFlags(flags: string | undefined): string {
|
|
673
|
+
const values = [
|
|
674
|
+
flags?.includes("i") ? "re.IGNORECASE" : null,
|
|
675
|
+
flags?.includes("m") ? "re.MULTILINE" : null,
|
|
676
|
+
flags?.includes("s") ? "re.DOTALL" : null,
|
|
677
|
+
].filter(Boolean);
|
|
678
|
+
return values.length > 0 ? values.join(" | ") : "0";
|
|
679
|
+
}
|
|
523
680
|
|
|
524
|
-
|
|
525
|
-
|
|
681
|
+
export type PythonRegexTranslation = { pattern: string } | { unsupported: string };
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Translates a JavaScript regex (as used by Zod) to Python `re` syntax. Constructs Python cannot
|
|
685
|
+
* express identically are reported as unsupported so the validator is skipped instead of breaking imports.
|
|
686
|
+
*/
|
|
687
|
+
export function toPythonRegex(pattern: string, flags = ""): PythonRegexTranslation {
|
|
688
|
+
const unsupportedFlags = [...flags].filter((flag) => !"imsgu".includes(flag));
|
|
689
|
+
if (unsupportedFlags.length > 0) return { unsupported: `flag(s) ${unsupportedFlags.join("")} have no Python equivalent` };
|
|
690
|
+
const checks: Array<[RegExp, string]> = [
|
|
691
|
+
[/\\[pP]\{/, "Unicode property escapes (\\p{...})"],
|
|
692
|
+
[/\\u\{/, "code point escapes (\\u{...})"],
|
|
693
|
+
[/\\c[A-Za-z]/, "control escapes (\\cX)"],
|
|
694
|
+
[/\(\?<[=!]/, "lookbehind (JavaScript allows variable width; Python does not)"],
|
|
695
|
+
[/\[\^?\]/, "empty character classes ([] / [^])"],
|
|
696
|
+
];
|
|
697
|
+
for (const [construct, label] of checks) {
|
|
698
|
+
if (construct.test(pattern)) return { unsupported: label };
|
|
526
699
|
}
|
|
700
|
+
return {
|
|
701
|
+
pattern: pattern
|
|
702
|
+
.replace(/\(\?<([A-Za-z_][A-Za-z0-9_]*)>/g, "(?P<$1>")
|
|
703
|
+
.replace(/\\k<([A-Za-z_][A-Za-z0-9_]*)>/g, "(?P=$1)"),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function pythonRegexFields(model: ModelInfo): Array<{ field: ModelInfo["fields"][number]; translation: PythonRegexTranslation }> {
|
|
708
|
+
return model.fields
|
|
709
|
+
.filter((field) => field.type === "string" && !field.enumValues?.length && field.constraints?.pattern !== undefined)
|
|
710
|
+
.map((field) => ({ field, translation: toPythonRegex(field.constraints!.pattern!, field.constraints!.patternFlags) }));
|
|
711
|
+
}
|
|
527
712
|
|
|
528
|
-
|
|
713
|
+
export function getPythonRegexWarnings(model: ModelInfo): string[] {
|
|
714
|
+
return pythonRegexFields(model)
|
|
715
|
+
.filter((entry): entry is { field: ModelInfo["fields"][number]; translation: { unsupported: string } } => "unsupported" in entry.translation)
|
|
716
|
+
.map(({ field, translation }) =>
|
|
717
|
+
`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.`);
|
|
529
718
|
}
|
|
530
719
|
|
|
720
|
+
/**
|
|
721
|
+
* Zod regex checks use RegExp.test (search), so validators use re.search instead of pydantic's anchored pattern.
|
|
722
|
+
* They run before type conversion so date/time strings are checked as the JSON text Zod sees; non-string
|
|
723
|
+
* values are left to the field's own type validation.
|
|
724
|
+
*/
|
|
725
|
+
function buildPythonPatternValidators(model: ModelInfo): string {
|
|
726
|
+
return pythonRegexFields(model)
|
|
727
|
+
.filter((entry): entry is { field: ModelInfo["fields"][number]; translation: { pattern: string } } => "pattern" in entry.translation)
|
|
728
|
+
.map(({ field, translation }) => {
|
|
729
|
+
const pythonName = toSnakeCase(field.name);
|
|
730
|
+
const pattern = JSON.stringify(translation.pattern);
|
|
731
|
+
const flags = buildPythonRegexFlags(field.constraints!.patternFlags);
|
|
732
|
+
const message = JSON.stringify(`${field.name} must match /${field.constraints!.pattern}/${field.constraints!.patternFlags ?? ""}`);
|
|
733
|
+
return ` @field_validator('${pythonName}', mode='before')
|
|
734
|
+
def ${pythonName}_validate_pattern(cls, value):
|
|
735
|
+
"""Validates the Zod regex constraint"""
|
|
736
|
+
if value is None:
|
|
737
|
+
return value
|
|
738
|
+
|
|
739
|
+
for item in (value if isinstance(value, list) else [value]):
|
|
740
|
+
if isinstance(item, str) and not re.search(${pattern}, item, ${flags}):
|
|
741
|
+
raise ValueError(${message})
|
|
742
|
+
return value`;
|
|
743
|
+
})
|
|
744
|
+
.join("\n\n");
|
|
745
|
+
}
|
|
531
746
|
function buildPythonEnumValidators(model: ModelInfo): string {
|
|
532
747
|
return model.fields
|
|
533
748
|
.filter((field) => field.enumValues?.length)
|
|
@@ -564,7 +779,7 @@ function buildPythonModelImports(model: ModelInfo): string {
|
|
|
564
779
|
function generateLegacyCompatiblePythonModelSource(model: ModelInfo): string {
|
|
565
780
|
const fieldDeclarations = model.fields.map((field) => ` ${buildPythonFieldDeclaration(field)}`).join("\n");
|
|
566
781
|
const propertyNames = model.fields.map((field) => `"${field.name}"`).join(", ");
|
|
567
|
-
const validators = buildPythonEnumValidators(model);
|
|
782
|
+
const validators = [buildPythonEnumValidators(model), buildPythonPatternValidators(model)].filter(Boolean).join("\n\n");
|
|
568
783
|
const dictAssignments = model.fields
|
|
569
784
|
.map((field) => ` "${field.name}": obj.get("${field.name}")`)
|
|
570
785
|
.join(",\n");
|
|
@@ -588,9 +803,9 @@ import pprint
|
|
|
588
803
|
import re # noqa: F401
|
|
589
804
|
import json
|
|
590
805
|
|
|
591
|
-
from datetime import datetime
|
|
806
|
+
from datetime import date, datetime
|
|
592
807
|
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictFloat, StrictInt, StrictStr, field_validator
|
|
593
|
-
from typing import Any, ClassVar, Dict, List, Optional, Set, Union
|
|
808
|
+
from typing import Annotated, Any, ClassVar, Dict, List, Optional, Set, Union
|
|
594
809
|
from typing_extensions import Self
|
|
595
810
|
|
|
596
811
|
${buildPythonModelImports(model)}class ${model.name}(BaseModel):
|
|
@@ -614,7 +829,7 @@ ${validators ? `\n\n${validators}` : ""}
|
|
|
614
829
|
|
|
615
830
|
def to_json(self) -> str:
|
|
616
831
|
"""Returns the JSON representation of the model using alias"""
|
|
617
|
-
return
|
|
832
|
+
return self.model_dump_json(by_alias=True, exclude_none=True)
|
|
618
833
|
|
|
619
834
|
@classmethod
|
|
620
835
|
def from_json(cls, json_str: str) -> Optional[Self]:
|
|
@@ -724,14 +939,23 @@ function ensurePythonCodegenProjectFiles(
|
|
|
724
939
|
return requirementsPath;
|
|
725
940
|
}
|
|
726
941
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
942
|
+
/** Generated backend_models import pydantic v2 at runtime, so the deployed Functions app must install it. */
|
|
943
|
+
export const PYTHON_SCHEMA_RUNTIME_REQUIREMENT = "pydantic>=2.6,<3";
|
|
944
|
+
|
|
945
|
+
function ensurePythonRuntimeRequirement(functionsRoot: string, session: FileOperationSession): void {
|
|
946
|
+
const requirementsPath = path.join(functionsRoot, "requirements.txt");
|
|
947
|
+
const current = session.fileExists(requirementsPath) ? session.readFile(requirementsPath) : "";
|
|
948
|
+
const declared = current.split(/\r?\n/).some((line) => line.trim().split(/[<>=!~;[ ]/)[0].toLowerCase() === "pydantic");
|
|
949
|
+
if (declared) return;
|
|
950
|
+
const prefix = current && !current.endsWith("\n") ? `${current}\n` : current;
|
|
951
|
+
session.writeFile(requirementsPath, `${prefix}${PYTHON_SCHEMA_RUNTIME_REQUIREMENT}\n`, {
|
|
952
|
+
ownership: "extension-point",
|
|
953
|
+
generator: "native-schema",
|
|
954
|
+
});
|
|
731
955
|
}
|
|
732
956
|
|
|
733
957
|
async function ensureProjectLocalUvCommand(projectRoot: string): Promise<{ command: string; env: NodeJS.ProcessEnv }> {
|
|
734
|
-
const uvEnv =
|
|
958
|
+
const uvEnv = { ...process.env };
|
|
735
959
|
const { localUvExecutable } = getProjectLocalUvPaths(projectRoot);
|
|
736
960
|
|
|
737
961
|
if (canRun("uv", ["--version"], projectRoot)) {
|
|
@@ -762,22 +986,7 @@ async function ensurePythonCodegenEnvironment(functionsRoot: string, requirement
|
|
|
762
986
|
const projectRoot = getPythonProjectRoot(functionsRoot);
|
|
763
987
|
const { command: uvCommand, env: uvEnv } = await ensureProjectLocalUvCommand(projectRoot);
|
|
764
988
|
const venvDir = path.join(functionsRoot, ".codegen-venv");
|
|
765
|
-
const venvPython =
|
|
766
|
-
|
|
767
|
-
if (!canRun(venvPython, ["--version"], functionsRoot, uvEnv)) {
|
|
768
|
-
const venvArgs = buildUvVenvArgs(venvDir);
|
|
769
|
-
if (fs.existsSync(venvDir)) {
|
|
770
|
-
venvArgs.push("--clear");
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
await runCommand(
|
|
774
|
-
uvCommand,
|
|
775
|
-
venvArgs,
|
|
776
|
-
functionsRoot,
|
|
777
|
-
"Failed to create the Python schema code generation virtual environment.",
|
|
778
|
-
uvEnv
|
|
779
|
-
);
|
|
780
|
-
}
|
|
989
|
+
const venvPython = await ensureUvVirtualEnvironment(uvCommand, venvDir, functionsRoot, uvEnv);
|
|
781
990
|
|
|
782
991
|
if (!canRun(venvPython, ["-c", "import datamodel_code_generator"], functionsRoot, uvEnv)) {
|
|
783
992
|
await runCommand(
|
|
@@ -785,7 +994,7 @@ async function ensurePythonCodegenEnvironment(functionsRoot: string, requirement
|
|
|
785
994
|
buildUvPipInstallArgs(venvPython, requirementsPath),
|
|
786
995
|
functionsRoot,
|
|
787
996
|
"Failed to install Python schema generation dependencies.",
|
|
788
|
-
uvEnv
|
|
997
|
+
buildPythonCodegenInstallEnv(uvEnv)
|
|
789
998
|
);
|
|
790
999
|
}
|
|
791
1000
|
|
|
@@ -888,6 +1097,7 @@ async function generatePythonSchemaArtifacts(
|
|
|
888
1097
|
sourceModel: string
|
|
889
1098
|
): Promise<void> {
|
|
890
1099
|
ensurePythonCodegenProjectFiles(functionsRoot, session, sourceModel);
|
|
1100
|
+
ensurePythonRuntimeRequirement(functionsRoot, session);
|
|
891
1101
|
|
|
892
1102
|
const packageRoot = path.join(outputDir, "backend_models");
|
|
893
1103
|
const modelsRoot = path.join(packageRoot, "models");
|
|
@@ -905,6 +1115,10 @@ async function generatePythonSchemaArtifacts(
|
|
|
905
1115
|
);
|
|
906
1116
|
|
|
907
1117
|
for (const model of models) {
|
|
1118
|
+
for (const warning of getPythonRegexWarnings(model)) {
|
|
1119
|
+
session.addWarning(warning);
|
|
1120
|
+
console.warn(`⚠️ ${warning}`);
|
|
1121
|
+
}
|
|
908
1122
|
const modelPath = getPythonSchemaModelPath(outputDir, model.name);
|
|
909
1123
|
session.writeFile(modelPath, generateLegacyCompatiblePythonModelSource(model), {
|
|
910
1124
|
ownership: "managed",
|
|
@@ -946,10 +1160,12 @@ export async function generateLanguageSchemaArtifacts(
|
|
|
946
1160
|
backendLanguage === "csharp" ? "csharp-models" : "python-models"
|
|
947
1161
|
);
|
|
948
1162
|
|
|
1163
|
+
// Inline z.object fields become named classes (e.g. ProbeItemNested) so their shape is preserved.
|
|
1164
|
+
const nativeModels = expandInlineObjectModels(models);
|
|
949
1165
|
if (backendLanguage === "csharp") {
|
|
950
|
-
await generateCSharpSchemaArtifacts(
|
|
1166
|
+
await generateCSharpSchemaArtifacts(nativeModels, outputDir, functionsRoot, session, rootModel.name);
|
|
951
1167
|
} else {
|
|
952
|
-
await generatePythonSchemaArtifacts(
|
|
1168
|
+
await generatePythonSchemaArtifacts(nativeModels, outputDir, functionsRoot, session, rootModel.name);
|
|
953
1169
|
}
|
|
954
1170
|
|
|
955
1171
|
console.log(
|