@nifrajs/cli 3.5.0 → 4.0.0
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.md +42 -3
- package/dist/app-files.d.ts +11 -0
- package/dist/app-files.d.ts.map +1 -0
- package/dist/app-files.js +34 -0
- package/dist/app-files.js.map +1 -0
- package/dist/assure-hydration.d.ts +7 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +128 -43
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +16 -2
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +5 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +32 -10
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/cdn-check.d.ts +41 -0
- package/dist/cdn-check.d.ts.map +1 -0
- package/dist/cdn-check.js +207 -0
- package/dist/cdn-check.js.map +1 -0
- package/dist/check-diagnostics.d.ts +4 -0
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +13 -0
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +49 -7
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +291 -62
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +99 -63
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +42 -16
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +262 -82
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +57 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +270 -26
- package/dist/command-catalog.js.map +1 -1
- package/dist/config-target.d.ts +8 -0
- package/dist/config-target.d.ts.map +1 -0
- package/dist/config-target.js +45 -0
- package/dist/config-target.js.map +1 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +52 -13
- package/dist/contracts.js.map +1 -1
- package/dist/db-audit.d.ts +37 -0
- package/dist/db-audit.d.ts.map +1 -0
- package/dist/db-audit.js +99 -0
- package/dist/db-audit.js.map +1 -0
- package/dist/db-child.d.ts +64 -0
- package/dist/db-child.d.ts.map +1 -0
- package/dist/db-child.js +213 -0
- package/dist/db-child.js.map +1 -0
- package/dist/db-config.d.ts +60 -0
- package/dist/db-config.d.ts.map +1 -0
- package/dist/db-config.js +176 -0
- package/dist/db-config.js.map +1 -0
- package/dist/db-tool.d.ts +84 -0
- package/dist/db-tool.d.ts.map +1 -0
- package/dist/db-tool.js +537 -0
- package/dist/db-tool.js.map +1 -0
- package/dist/dev-bun-config.d.ts +20 -13
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +98 -41
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +106 -0
- package/dist/dev-feed-tool.d.ts.map +1 -0
- package/dist/dev-feed-tool.js +713 -0
- package/dist/dev-feed-tool.js.map +1 -0
- package/dist/dev-server-client.d.ts +67 -0
- package/dist/dev-server-client.d.ts.map +1 -0
- package/dist/dev-server-client.js +195 -0
- package/dist/dev-server-client.js.map +1 -0
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +6 -5
- package/dist/diff-tool.js.map +1 -1
- package/dist/docs-search.d.ts.map +1 -1
- package/dist/docs-search.js +12 -4
- package/dist/docs-search.js.map +1 -1
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +76 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +15 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +73 -1
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +41 -21
- package/dist/fix-recipes.js.map +1 -1
- package/dist/frontend-guidance.js +5 -5
- package/dist/frontend-guidance.js.map +1 -1
- package/dist/i18n-check.d.ts +16 -0
- package/dist/i18n-check.d.ts.map +1 -0
- package/dist/i18n-check.js +154 -0
- package/dist/i18n-check.js.map +1 -0
- package/dist/init-agents.d.ts +49 -13
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +239 -34
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/script-kind.d.ts +7 -0
- package/dist/internal/script-kind.d.ts.map +1 -0
- package/dist/internal/script-kind.js +12 -0
- package/dist/internal/script-kind.js.map +1 -0
- package/dist/internal/source-facts.d.ts +5 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +61 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +49 -24
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +507 -67
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +5 -1
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +92 -27
- package/dist/introspect.js.map +1 -1
- package/dist/learn.js +3 -3
- package/dist/learn.js.map +1 -1
- package/dist/levels-tool.d.ts +10 -0
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -3
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +67 -17
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +78 -18
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +3 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-app-tool.d.ts.map +1 -1
- package/dist/mcp-app-tool.js +2 -0
- package/dist/mcp-app-tool.js.map +1 -1
- package/dist/mcp-context.js +1 -1
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +57 -0
- package/dist/mcp-delegate.d.ts.map +1 -0
- package/dist/mcp-delegate.js +130 -0
- package/dist/mcp-delegate.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +23 -5
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-exec.d.ts +2 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +132 -108
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +17 -5
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +14 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +45 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts +48 -0
- package/dist/mcp-isolate.d.ts.map +1 -0
- package/dist/mcp-isolate.js +222 -0
- package/dist/mcp-isolate.js.map +1 -0
- package/dist/mcp-project-child.d.ts +74 -0
- package/dist/mcp-project-child.d.ts.map +1 -0
- package/dist/mcp-project-child.js +130 -0
- package/dist/mcp-project-child.js.map +1 -0
- package/dist/mcp-render.js +2 -2
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp-root.d.ts +37 -9
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +110 -23
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-run.d.ts +18 -1
- package/dist/mcp-run.d.ts.map +1 -1
- package/dist/mcp-run.js +82 -9
- package/dist/mcp-run.js.map +1 -1
- package/dist/mcp-ws.js +4 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts +16 -3
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +81 -53
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +44 -0
- package/dist/migrate-layout.d.ts.map +1 -0
- package/dist/migrate-layout.js +982 -0
- package/dist/migrate-layout.js.map +1 -0
- package/dist/openapi-tool.d.ts +2 -2
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js.map +1 -1
- package/dist/openapi-types.d.ts +2 -1
- package/dist/openapi-types.d.ts.map +1 -1
- package/dist/openapi-types.js +240 -12
- package/dist/openapi-types.js.map +1 -1
- package/dist/pipeline-report.d.ts +3 -2
- package/dist/pipeline-report.d.ts.map +1 -1
- package/dist/pipeline-report.js +19 -16
- package/dist/pipeline-report.js.map +1 -1
- package/dist/port.d.ts +5 -4
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +46 -29
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +5 -0
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/4.0.0.d.ts +9 -0
- package/dist/recipes/4.0.0.d.ts.map +1 -0
- package/dist/recipes/4.0.0.js +34 -0
- package/dist/recipes/4.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/release-verification.d.ts +11 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +47 -4
- package/dist/release-verification.js.map +1 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +12 -8
- package/dist/review.js.map +1 -1
- package/dist/route-types.d.ts +28 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +106 -0
- package/dist/route-types.js.map +1 -0
- package/dist/rules/codes.d.ts +9 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +9 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/data-guard.d.ts +12 -0
- package/dist/rules/data-guard.d.ts.map +1 -0
- package/dist/rules/data-guard.js +213 -0
- package/dist/rules/data-guard.js.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +17 -5
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/islands.d.ts.map +1 -1
- package/dist/rules/islands.js +7 -4
- package/dist/rules/islands.js.map +1 -1
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +71 -38
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/nano.d.ts.map +1 -1
- package/dist/rules/nano.js +15 -6
- package/dist/rules/nano.js.map +1 -1
- package/dist/rules/pages.d.ts +15 -0
- package/dist/rules/pages.d.ts.map +1 -0
- package/dist/rules/pages.js +88 -0
- package/dist/rules/pages.js.map +1 -0
- package/dist/rules/routes.d.ts +11 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +180 -2
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.d.ts +10 -0
- package/dist/rules/secrets.d.ts.map +1 -0
- package/dist/rules/secrets.js +169 -0
- package/dist/rules/secrets.js.map +1 -0
- package/dist/rules/security.d.ts +7 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +171 -65
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/typescript.d.ts +16 -0
- package/dist/rules/typescript.d.ts.map +1 -0
- package/dist/rules/typescript.js +59 -0
- package/dist/rules/typescript.js.map +1 -0
- package/dist/rules/zones.d.ts +17 -0
- package/dist/rules/zones.d.ts.map +1 -0
- package/dist/rules/zones.js +174 -0
- package/dist/rules/zones.js.map +1 -0
- package/dist/scaffold.d.ts +25 -12
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +132 -87
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts +17 -3
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +941 -153
- package/dist/sdk.js.map +1 -1
- package/dist/search-terms.d.ts +2 -0
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +23 -8
- package/dist/search-terms.js.map +1 -1
- package/dist/smoke.d.ts +79 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +494 -0
- package/dist/smoke.js.map +1 -0
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +2 -1
- package/dist/sync-manifest.js.map +1 -1
- package/dist/sync-routes.d.ts +4 -3
- package/dist/sync-routes.d.ts.map +1 -1
- package/dist/sync-routes.js +4 -3
- package/dist/sync-routes.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +30 -11
- package/dist/test-tool.js.map +1 -1
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +11 -5
- package/dist/types-search.js.map +1 -1
- package/dist/upgrade.d.ts +36 -6
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +167 -29
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +4 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/verification.d.ts.map +1 -1
- package/dist/verification.js +45 -12
- package/dist/verification.js.map +1 -1
- package/dist/web-app-options.d.ts +19 -0
- package/dist/web-app-options.d.ts.map +1 -0
- package/dist/web-app-options.js +49 -0
- package/dist/web-app-options.js.map +1 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +29 -13
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +185 -29
- package/docs/llms-full.txt +3807 -580
- package/docs/types.json +14229 -10939
- package/package.json +19 -12
- package/src/app-files.ts +40 -0
- package/src/assure-hydration.ts +139 -46
- package/src/assure.ts +17 -2
- package/src/capabilities-tool.ts +35 -9
- package/src/cdn-check.ts +268 -0
- package/src/check-diagnostics.ts +17 -0
- package/src/check-scan.ts +358 -63
- package/src/check.ts +97 -62
- package/src/cli.ts +296 -87
- package/src/command-catalog.ts +374 -38
- package/src/config-target.ts +47 -0
- package/src/contracts.ts +54 -12
- package/src/db-audit.ts +128 -0
- package/src/db-child.ts +285 -0
- package/src/db-config.ts +224 -0
- package/src/db-tool.ts +690 -0
- package/src/dev-bun-config.ts +116 -40
- package/src/dev-feed-tool.ts +912 -0
- package/src/dev-server-client.ts +267 -0
- package/src/diff-tool.ts +6 -5
- package/src/docs-search.ts +10 -3
- package/src/doctor.ts +98 -15
- package/src/env-file.ts +78 -1
- package/src/fix-recipes.ts +44 -31
- package/src/frontend-guidance.ts +5 -5
- package/src/i18n-check.ts +179 -0
- package/src/init-agents.ts +301 -38
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/script-kind.ts +11 -0
- package/src/internal/source-facts.ts +96 -22
- package/src/internal/typescript-import.ts +663 -68
- package/src/introspect.ts +90 -27
- package/src/learn.ts +3 -3
- package/src/levels-tool.ts +47 -2
- package/src/load.ts +140 -33
- package/src/manifest-tool.ts +3 -2
- package/src/mcp-app-tool.ts +2 -0
- package/src/mcp-context.ts +1 -1
- package/src/mcp-delegate.ts +162 -0
- package/src/mcp-docs-tools.ts +23 -5
- package/src/mcp-exec.ts +160 -132
- package/src/mcp-http.ts +19 -5
- package/src/mcp-io.ts +57 -0
- package/src/mcp-isolate.ts +287 -0
- package/src/mcp-project-child.ts +182 -0
- package/src/mcp-render.ts +2 -2
- package/src/mcp-root.ts +117 -22
- package/src/mcp-run.ts +104 -10
- package/src/mcp-ws.ts +4 -1
- package/src/mcp.ts +106 -64
- package/src/migrate-layout.ts +1121 -0
- package/src/openapi-tool.ts +7 -3
- package/src/openapi-types.ts +315 -13
- package/src/pipeline-report.ts +22 -18
- package/src/port.ts +53 -30
- package/src/project-facts.ts +5 -0
- package/src/recipes/4.0.0.ts +35 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +74 -5
- package/src/review.ts +13 -8
- package/src/route-types.ts +133 -0
- package/src/rules/codes.ts +9 -0
- package/src/rules/data-guard.ts +230 -0
- package/src/rules/index.ts +18 -5
- package/src/rules/islands.ts +12 -4
- package/src/rules/legacy.ts +71 -39
- package/src/rules/nano.ts +18 -6
- package/src/rules/pages.ts +99 -0
- package/src/rules/routes.ts +181 -2
- package/src/rules/secrets.ts +174 -0
- package/src/rules/security.ts +198 -70
- package/src/rules/typescript.ts +77 -0
- package/src/rules/zones.ts +195 -0
- package/src/scaffold.ts +147 -95
- package/src/sdk.ts +1215 -158
- package/src/search-terms.ts +24 -7
- package/src/smoke.ts +607 -0
- package/src/sync-manifest.ts +2 -1
- package/src/sync-routes.ts +4 -3
- package/src/test-tool.ts +32 -11
- package/src/types-search.ts +11 -4
- package/src/upgrade.ts +205 -25
- package/src/verification-plan.ts +13 -0
- package/src/verification.ts +50 -12
- package/src/web-app-options.ts +57 -0
- package/src/work-graph.ts +27 -13
package/dist/sdk.js
CHANGED
|
@@ -6,14 +6,34 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
-
/** Deterministic
|
|
9
|
+
/** Deterministic typed SDK generation from the backend's OpenAPI contract. */
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
11
|
import { basename, resolve } from "node:path";
|
|
12
12
|
import { toOpenAPI } from "@nifrajs/schema/openapi";
|
|
13
|
+
import { BACKEND_APP_FILE } from "./app-files.js";
|
|
14
|
+
import { codeUnitOrder } from "./internal/code-unit-order.js";
|
|
15
|
+
export class SdkGenerationError extends Error {
|
|
16
|
+
issues;
|
|
17
|
+
constructor(issues) {
|
|
18
|
+
super("[nifra] strict SDK generation found " +
|
|
19
|
+
issues.length +
|
|
20
|
+
" opaque contract " +
|
|
21
|
+
(issues.length === 1 ? "portion" : "portions") +
|
|
22
|
+
":\n" +
|
|
23
|
+
issues.map((issue) => `- ${issue.operation} ${issue.location}: ${issue.reason}`).join("\n"));
|
|
24
|
+
this.name = "SdkGenerationError";
|
|
25
|
+
this.issues = Object.freeze([...issues]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
13
28
|
const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
|
|
29
|
+
const SUCCESS_STATUS = /^[23][0-9]{2}$/;
|
|
30
|
+
const ERROR_STATUS = /^[45][0-9]{2}$/;
|
|
31
|
+
const recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
32
|
+
? value
|
|
33
|
+
: undefined;
|
|
14
34
|
function operations(document) {
|
|
15
35
|
const result = [];
|
|
16
|
-
for (const [path, item] of Object.entries(document.paths).sort(([a], [b]) => a
|
|
36
|
+
for (const [path, item] of Object.entries(document.paths).sort(([a], [b]) => codeUnitOrder(a, b))) {
|
|
17
37
|
for (const method of HTTP_METHODS) {
|
|
18
38
|
const operation = item[method];
|
|
19
39
|
if (operation !== null && typeof operation === "object") {
|
|
@@ -24,14 +44,60 @@ function operations(document) {
|
|
|
24
44
|
return result;
|
|
25
45
|
}
|
|
26
46
|
function identifier(value, fallback) {
|
|
27
|
-
const startsWithDigit = /^\d/.test(value);
|
|
28
47
|
const normalized = value
|
|
29
48
|
.replace(/[^A-Za-z0-9_]+/g, "_")
|
|
30
49
|
.replace(/_+/g, "_")
|
|
31
50
|
.replace(/^_+|_+$/g, "");
|
|
32
|
-
const safe =
|
|
51
|
+
const safe = /^\d/.test(normalized) ? `_${normalized}` : normalized;
|
|
33
52
|
return safe === "" || safe === "_" ? fallback : safe;
|
|
34
53
|
}
|
|
54
|
+
const PYTHON_KEYWORDS = new Set([
|
|
55
|
+
"and",
|
|
56
|
+
"as",
|
|
57
|
+
"assert",
|
|
58
|
+
"async",
|
|
59
|
+
"await",
|
|
60
|
+
"break",
|
|
61
|
+
"case",
|
|
62
|
+
"class",
|
|
63
|
+
"continue",
|
|
64
|
+
"def",
|
|
65
|
+
"del",
|
|
66
|
+
"elif",
|
|
67
|
+
"else",
|
|
68
|
+
"except",
|
|
69
|
+
"False",
|
|
70
|
+
"finally",
|
|
71
|
+
"for",
|
|
72
|
+
"from",
|
|
73
|
+
"global",
|
|
74
|
+
"if",
|
|
75
|
+
"import",
|
|
76
|
+
"in",
|
|
77
|
+
"is",
|
|
78
|
+
"lambda",
|
|
79
|
+
"None",
|
|
80
|
+
"nonlocal",
|
|
81
|
+
"not",
|
|
82
|
+
"or",
|
|
83
|
+
"pass",
|
|
84
|
+
"raise",
|
|
85
|
+
"return",
|
|
86
|
+
"True",
|
|
87
|
+
"try",
|
|
88
|
+
"while",
|
|
89
|
+
"with",
|
|
90
|
+
"yield",
|
|
91
|
+
]);
|
|
92
|
+
function pythonIdentifier(value, fallback) {
|
|
93
|
+
const result = identifier(value, fallback);
|
|
94
|
+
return PYTHON_KEYWORDS.has(result) ? `${result}_` : result;
|
|
95
|
+
}
|
|
96
|
+
function pascal(value, fallback) {
|
|
97
|
+
const words = identifier(value, fallback).split("_").filter(Boolean);
|
|
98
|
+
const result = words.map((word) => word[0].toUpperCase() + word.slice(1)).join("");
|
|
99
|
+
return result === "" ? fallback : result;
|
|
100
|
+
}
|
|
35
101
|
function operationName(operation, method, path) {
|
|
36
102
|
return identifier(operation.operationId ?? `${method}_${path}`, `${method}_request`);
|
|
37
103
|
}
|
|
@@ -46,184 +112,906 @@ function uniqueNames(items) {
|
|
|
46
112
|
function pathParameters(path) {
|
|
47
113
|
return [...path.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]).filter(Boolean);
|
|
48
114
|
}
|
|
49
|
-
function
|
|
115
|
+
function firstSchema(container) {
|
|
116
|
+
return Object.values(container?.content ?? {})[0]?.schema;
|
|
117
|
+
}
|
|
118
|
+
function responseSchema(response) {
|
|
119
|
+
return firstSchema(response);
|
|
120
|
+
}
|
|
121
|
+
function schemaRef(schema) {
|
|
122
|
+
const record = recordOf(schema);
|
|
123
|
+
return typeof record?.$ref === "string" ? record.$ref : undefined;
|
|
124
|
+
}
|
|
125
|
+
function schemaType(schema) {
|
|
126
|
+
const record = recordOf(schema);
|
|
127
|
+
return typeof record?.type === "string" ? record.type : undefined;
|
|
128
|
+
}
|
|
129
|
+
function schemaProperties(schema) {
|
|
130
|
+
const properties = recordOf(recordOf(schema)?.properties);
|
|
131
|
+
return properties;
|
|
132
|
+
}
|
|
133
|
+
function schemaRequired(schema) {
|
|
134
|
+
const required = recordOf(schema)?.required;
|
|
135
|
+
return new Set(Array.isArray(required)
|
|
136
|
+
? required.filter((value) => typeof value === "string")
|
|
137
|
+
: []);
|
|
138
|
+
}
|
|
139
|
+
function schemaUnion(schema) {
|
|
140
|
+
const record = recordOf(schema);
|
|
141
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
142
|
+
const value = record?.[key];
|
|
143
|
+
if (Array.isArray(value) &&
|
|
144
|
+
value.every((item) => typeof item === "boolean" || recordOf(item) !== undefined)) {
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
function schemaIsNullable(schema) {
|
|
151
|
+
return schemaUnion(schema)?.some((item) => schemaType(item) === "null") ?? false;
|
|
152
|
+
}
|
|
153
|
+
function isObjectModel(schema) {
|
|
154
|
+
const record = recordOf(schema);
|
|
155
|
+
const type = schemaType(schema);
|
|
156
|
+
if (type !== "object" && schemaProperties(schema) === undefined)
|
|
157
|
+
return false;
|
|
158
|
+
const additional = record?.additionalProperties;
|
|
159
|
+
return additional === undefined || additional === false;
|
|
160
|
+
}
|
|
161
|
+
function operationLabel(entry) {
|
|
162
|
+
return `${entry.method.toUpperCase()} ${entry.path}`;
|
|
163
|
+
}
|
|
164
|
+
class SchemaCatalog {
|
|
165
|
+
models = new Map();
|
|
166
|
+
componentNames = new Map();
|
|
167
|
+
namesBySchema = new Map();
|
|
168
|
+
usedNames = new Set();
|
|
169
|
+
constructor(components = {}) {
|
|
170
|
+
for (const name of Object.keys(components).sort()) {
|
|
171
|
+
this.componentNames.set(name, this.uniqueName(pascal(name, "Model")));
|
|
172
|
+
}
|
|
173
|
+
for (const [name, schema] of Object.entries(components)) {
|
|
174
|
+
const className = this.componentNames.get(name);
|
|
175
|
+
if (className !== undefined)
|
|
176
|
+
this.models.set(className, schema);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
uniqueName(preferred) {
|
|
180
|
+
let name = preferred || "Model";
|
|
181
|
+
let suffix = 2;
|
|
182
|
+
while (this.usedNames.has(name))
|
|
183
|
+
name = `${preferred}_${suffix++}`;
|
|
184
|
+
this.usedNames.add(name);
|
|
185
|
+
return name;
|
|
186
|
+
}
|
|
187
|
+
model(schema, hint) {
|
|
188
|
+
const known = this.namesBySchema.get(schema);
|
|
189
|
+
if (known !== undefined)
|
|
190
|
+
return known;
|
|
191
|
+
const name = this.uniqueName(pascal(hint, "Model"));
|
|
192
|
+
this.namesBySchema.set(schema, name);
|
|
193
|
+
this.models.set(name, schema);
|
|
194
|
+
return name;
|
|
195
|
+
}
|
|
196
|
+
refName(ref) {
|
|
197
|
+
const prefix = "#/components/schemas/";
|
|
198
|
+
return ref.startsWith(prefix) ? this.componentNames.get(ref.slice(prefix.length)) : undefined;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
class OpaqueTracker {
|
|
202
|
+
issues = [];
|
|
203
|
+
add(operation, location, reason) {
|
|
204
|
+
this.issues.push({ operation, location, reason });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function literal(value) {
|
|
208
|
+
if (typeof value === "string")
|
|
209
|
+
return JSON.stringify(value);
|
|
210
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
211
|
+
return String(value);
|
|
212
|
+
return "None";
|
|
213
|
+
}
|
|
214
|
+
function pythonType(schema, hint, catalog, tracker, operation, location) {
|
|
215
|
+
if (schema === undefined) {
|
|
216
|
+
tracker.add(operation, location, "schema is not declared");
|
|
217
|
+
return "Any";
|
|
218
|
+
}
|
|
219
|
+
if (typeof schema === "boolean") {
|
|
220
|
+
if (schema)
|
|
221
|
+
tracker.add(operation, location, "boolean true schema has no usable static type");
|
|
222
|
+
return schema ? "Any" : "Never";
|
|
223
|
+
}
|
|
224
|
+
const ref = schemaRef(schema);
|
|
225
|
+
if (ref !== undefined) {
|
|
226
|
+
const name = catalog.refName(ref);
|
|
227
|
+
if (name !== undefined)
|
|
228
|
+
return name;
|
|
229
|
+
tracker.add(operation, location, `unresolved schema reference ${ref}`);
|
|
230
|
+
return "Any";
|
|
231
|
+
}
|
|
232
|
+
const union = schemaUnion(schema);
|
|
233
|
+
if (union !== undefined) {
|
|
234
|
+
const members = union
|
|
235
|
+
.filter((item) => schemaType(item) !== "null")
|
|
236
|
+
.map((item, index) => pythonType(item, hint + (index + 1), catalog, tracker, operation, location));
|
|
237
|
+
if (members.length === 0)
|
|
238
|
+
return "None";
|
|
239
|
+
const rendered = [...new Set(members)].join(" | ");
|
|
240
|
+
return schemaIsNullable(schema) && !rendered.includes("None") ? `${rendered} | None` : rendered;
|
|
241
|
+
}
|
|
242
|
+
const properties = schemaProperties(schema);
|
|
243
|
+
const type = schemaType(schema);
|
|
244
|
+
if (type === "object" || properties !== undefined) {
|
|
245
|
+
if (properties === undefined &&
|
|
246
|
+
recordOf(schema)?.additionalProperties !== undefined &&
|
|
247
|
+
recordOf(schema)?.additionalProperties !== false) {
|
|
248
|
+
const extra = recordOf(schema)?.additionalProperties;
|
|
249
|
+
const extraType = typeof extra === "boolean"
|
|
250
|
+
? "Any"
|
|
251
|
+
: pythonType(extra, `${hint}Value`, catalog, tracker, operation, location);
|
|
252
|
+
return `dict[str, ${extraType}]`;
|
|
253
|
+
}
|
|
254
|
+
return catalog.model(schema, hint);
|
|
255
|
+
}
|
|
256
|
+
if (type === "array") {
|
|
257
|
+
const item = recordOf(schema)?.items;
|
|
258
|
+
return ("list[" +
|
|
259
|
+
pythonType(item, `${hint}Item`, catalog, tracker, operation, `${location}.items`) +
|
|
260
|
+
"]");
|
|
261
|
+
}
|
|
262
|
+
if (type === "string") {
|
|
263
|
+
const enumValues = recordOf(schema)?.enum;
|
|
264
|
+
if (Array.isArray(enumValues) && enumValues.length > 0) {
|
|
265
|
+
return `Literal[${enumValues.map(literal).join(", ")}]`;
|
|
266
|
+
}
|
|
267
|
+
return "str";
|
|
268
|
+
}
|
|
269
|
+
if (type === "integer")
|
|
270
|
+
return "int";
|
|
271
|
+
if (type === "number")
|
|
272
|
+
return "float";
|
|
273
|
+
if (type === "boolean")
|
|
274
|
+
return "bool";
|
|
275
|
+
if (type === "null")
|
|
276
|
+
return "None";
|
|
277
|
+
const constant = recordOf(schema)?.const;
|
|
278
|
+
if (constant !== undefined)
|
|
279
|
+
return `Literal[${literal(constant)}]`;
|
|
280
|
+
const allOf = recordOf(schema)?.allOf;
|
|
281
|
+
if (Array.isArray(allOf) && allOf.length > 0) {
|
|
282
|
+
return pythonType(allOf[0], hint, catalog, tracker, operation, location);
|
|
283
|
+
}
|
|
284
|
+
tracker.add(operation, location, "schema shape is opaque or unsupported");
|
|
285
|
+
return "Any";
|
|
286
|
+
}
|
|
287
|
+
function pythonModelFields(schema, name, catalog, tracker) {
|
|
288
|
+
const properties = schemaProperties(schema);
|
|
289
|
+
if (properties === undefined)
|
|
290
|
+
return " pass";
|
|
291
|
+
const required = schemaRequired(schema);
|
|
292
|
+
const used = new Set();
|
|
293
|
+
const fields = [];
|
|
294
|
+
for (const [jsonName, property] of Object.entries(properties)) {
|
|
295
|
+
let fieldName = pythonIdentifier(jsonName, "field");
|
|
296
|
+
while (used.has(fieldName))
|
|
297
|
+
fieldName += "_";
|
|
298
|
+
used.add(fieldName);
|
|
299
|
+
let type = pythonType(property, name + pascal(jsonName, "Field"), catalog, tracker, "schema", `model ${name}.${jsonName}`);
|
|
300
|
+
const optional = !required.has(jsonName);
|
|
301
|
+
if (optional && !type.includes("None"))
|
|
302
|
+
type += " | None";
|
|
303
|
+
if (fieldName === jsonName) {
|
|
304
|
+
fields.push(` ${fieldName}: ${type}${optional ? " = None" : ""}`);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
const metadata = `{"json_name": ${JSON.stringify(jsonName)}}`;
|
|
308
|
+
fields.push(" " +
|
|
309
|
+
fieldName +
|
|
310
|
+
": " +
|
|
311
|
+
type +
|
|
312
|
+
" = field(" +
|
|
313
|
+
(optional ? "default=None, " : "") +
|
|
314
|
+
"metadata=" +
|
|
315
|
+
metadata +
|
|
316
|
+
")");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return fields.length === 0 ? " pass" : fields.join("\n");
|
|
320
|
+
}
|
|
321
|
+
function renderPythonModels(catalog, tracker) {
|
|
322
|
+
const rendered = [];
|
|
323
|
+
let index = 0;
|
|
324
|
+
while (index < catalog.models.size) {
|
|
325
|
+
const entries = [...catalog.models.entries()];
|
|
326
|
+
const [name, schema] = entries[index++];
|
|
327
|
+
if (isObjectModel(schema)) {
|
|
328
|
+
rendered.push("@dataclass(frozen=True)\nclass " +
|
|
329
|
+
name +
|
|
330
|
+
":\n" +
|
|
331
|
+
pythonModelFields(schema, name, catalog, tracker));
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
rendered.push(name +
|
|
335
|
+
": TypeAlias = " +
|
|
336
|
+
pythonType(schema, name, catalog, tracker, "schema", `model ${name}`));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return rendered.join("\n\n");
|
|
340
|
+
}
|
|
341
|
+
function pythonErrorAliases(entries, names, catalog, tracker) {
|
|
342
|
+
const aliases = [];
|
|
343
|
+
entries.forEach((entry, index) => {
|
|
344
|
+
const types = [];
|
|
345
|
+
for (const [status, response] of Object.entries(entry.operation.responses ?? {}).sort(([a], [b]) => codeUnitOrder(a, b))) {
|
|
346
|
+
if (!ERROR_STATUS.test(status))
|
|
347
|
+
continue;
|
|
348
|
+
const schema = responseSchema(response);
|
|
349
|
+
types.push(schema === undefined
|
|
350
|
+
? "None"
|
|
351
|
+
: pythonType(schema, `${pascal(names[index], "Request")}Error${status}`, catalog, tracker, operationLabel(entry), `response ${status}`));
|
|
352
|
+
}
|
|
353
|
+
if (types.length > 0) {
|
|
354
|
+
aliases.push(`type ${pascal(names[index], "Request")}Error = ${[...new Set(types)].join(" | ")}`);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
return aliases.join("\n");
|
|
358
|
+
}
|
|
359
|
+
function pythonSdk(document, options) {
|
|
50
360
|
const entries = operations(document);
|
|
51
361
|
const names = uniqueNames(entries.map(({ method, path, operation }) => operationName(operation, method, path)));
|
|
362
|
+
const catalog = new SchemaCatalog(document.components?.schemas);
|
|
363
|
+
const tracker = new OpaqueTracker();
|
|
52
364
|
const methods = [];
|
|
53
|
-
entries.forEach((
|
|
365
|
+
entries.forEach((entry, index) => {
|
|
366
|
+
const { method, path, operation } = entry;
|
|
54
367
|
const name = names[index];
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
368
|
+
const label = operationLabel(entry);
|
|
369
|
+
const pathNames = pathParameters(path);
|
|
370
|
+
const pathDefinitions = pathNames.map((value) => (operation.parameters ?? []).find((parameter) => parameter.in === "path" && parameter.name === value));
|
|
371
|
+
const queryParameters = (operation.parameters ?? []).filter((parameter) => parameter.in === "query");
|
|
372
|
+
const used = new Set(["self", "body", "headers", ...pathNames]);
|
|
373
|
+
const queryNames = queryParameters.map((parameter) => {
|
|
374
|
+
let value = pythonIdentifier(parameter.name, "query");
|
|
60
375
|
while (used.has(value))
|
|
61
376
|
value += "_";
|
|
62
377
|
used.add(value);
|
|
63
378
|
return value;
|
|
64
379
|
});
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
380
|
+
const required = [];
|
|
381
|
+
const optional = [];
|
|
382
|
+
queryParameters.forEach((parameter, queryIndex) => {
|
|
383
|
+
const type = pythonType(parameter.schema, `${pascal(name, "Request")}Query${pascal(parameter.name, "Value")}`, catalog, tracker, label, `query ${parameter.name}`);
|
|
384
|
+
const value = `${queryNames[queryIndex]}: ${parameter.required ? type : `${type} | None`}`;
|
|
385
|
+
if (parameter.required)
|
|
386
|
+
required.push(value);
|
|
387
|
+
else
|
|
388
|
+
optional.push(`${value} = None`);
|
|
69
389
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
390
|
+
const bodySchema = firstSchema(operation.requestBody);
|
|
391
|
+
if (operation.requestBody !== undefined) {
|
|
392
|
+
const bodyType = pythonType(bodySchema, `${pascal(name, "Request")}Body`, catalog, tracker, label, "request body");
|
|
393
|
+
if (operation.requestBody.required === true)
|
|
394
|
+
required.push(`body: ${bodyType}`);
|
|
395
|
+
else
|
|
396
|
+
optional.push(`body: ${bodyType} = None`);
|
|
397
|
+
}
|
|
398
|
+
optional.push("headers: Mapping[str, str] | None = None");
|
|
399
|
+
const signature = [
|
|
400
|
+
...pathDefinitions.map((parameter, parameterIndex) => {
|
|
401
|
+
const type = pythonType(parameter?.schema, `${pascal(name, "Request")}Path${parameterIndex + 1}`, catalog, tracker, label, `path ${pathNames[parameterIndex]}`);
|
|
402
|
+
return `${pythonIdentifier(pathNames[parameterIndex], "param")}: ${type}`;
|
|
403
|
+
}),
|
|
404
|
+
"*",
|
|
405
|
+
...required,
|
|
406
|
+
...optional,
|
|
407
|
+
].join(", ");
|
|
73
408
|
const query = queryNames.length === 0
|
|
74
409
|
? "None"
|
|
75
|
-
:
|
|
76
|
-
|
|
410
|
+
: "{" +
|
|
411
|
+
queryNames
|
|
412
|
+
.map((value, queryIndex) => `${JSON.stringify(queryParameters[queryIndex].name)}: ${value}`)
|
|
413
|
+
.join(", ") +
|
|
414
|
+
"}";
|
|
415
|
+
const paths = pathNames.length === 0
|
|
416
|
+
? "None"
|
|
417
|
+
: "{" +
|
|
418
|
+
pathNames
|
|
419
|
+
.map((value) => `${JSON.stringify(value)}: ${pythonIdentifier(value, "param")}`)
|
|
420
|
+
.join(", ") +
|
|
421
|
+
"}";
|
|
422
|
+
const success = Object.entries(operation.responses ?? {}).find(([status]) => SUCCESS_STATUS.test(status));
|
|
423
|
+
const responseType = success === undefined
|
|
77
424
|
? "None"
|
|
78
|
-
:
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
425
|
+
: pythonType(responseSchema(success[1]), `${pascal(name, "Request")}Response`, catalog, tracker, label, `response ${success[0]}`);
|
|
426
|
+
const errorType = Object.keys(operation.responses ?? {}).some((status) => ERROR_STATUS.test(status))
|
|
427
|
+
? `${pascal(name, "Request")}Error`
|
|
428
|
+
: "Any";
|
|
429
|
+
methods.push(" def " +
|
|
430
|
+
name +
|
|
431
|
+
"(self" +
|
|
432
|
+
(signature ? `, ${signature}` : "") +
|
|
433
|
+
") -> " +
|
|
434
|
+
responseType +
|
|
435
|
+
":\n" +
|
|
436
|
+
" return self._request(" +
|
|
437
|
+
JSON.stringify(method.toUpperCase()) +
|
|
438
|
+
", " +
|
|
439
|
+
JSON.stringify(path) +
|
|
440
|
+
", path_params=" +
|
|
441
|
+
paths +
|
|
442
|
+
", query=" +
|
|
443
|
+
query +
|
|
444
|
+
", body=" +
|
|
445
|
+
(operation.requestBody === undefined ? "None" : "body") +
|
|
446
|
+
", headers=headers, response_type=" +
|
|
447
|
+
responseType +
|
|
448
|
+
", error_type=" +
|
|
449
|
+
errorType +
|
|
450
|
+
")");
|
|
82
451
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
import
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
self
|
|
103
|
-
|
|
104
|
-
self.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
452
|
+
const aliases = pythonErrorAliases(entries, names, catalog, tracker);
|
|
453
|
+
const models = renderPythonModels(catalog, tracker);
|
|
454
|
+
if (options.strict === true && tracker.issues.length > 0)
|
|
455
|
+
throw new SdkGenerationError(tracker.issues);
|
|
456
|
+
return [
|
|
457
|
+
"# Generated by nifra. Regenerate with: nifra sdk --lang python",
|
|
458
|
+
"from __future__ import annotations",
|
|
459
|
+
"",
|
|
460
|
+
"import json",
|
|
461
|
+
"import types",
|
|
462
|
+
"import urllib.error",
|
|
463
|
+
"import urllib.parse",
|
|
464
|
+
"import urllib.request",
|
|
465
|
+
"from dataclasses import dataclass, field, fields, is_dataclass",
|
|
466
|
+
"from typing import Any, Generic, Literal, Mapping, Never, TypeAlias, TypeVar, get_args, get_origin, get_type_hints",
|
|
467
|
+
"",
|
|
468
|
+
'T = TypeVar("T")',
|
|
469
|
+
"",
|
|
470
|
+
"class NifraApiError(RuntimeError, Generic[T]):",
|
|
471
|
+
" def __init__(self, status: int, body: T):",
|
|
472
|
+
' super().__init__(f"Nifra API request failed with HTTP {status}")',
|
|
473
|
+
" self.status = status",
|
|
474
|
+
" self.body = body",
|
|
475
|
+
"",
|
|
476
|
+
"def _decode(value: Any, target: Any) -> Any:",
|
|
477
|
+
" if target is None or target is Any or target is object:",
|
|
478
|
+
" return value",
|
|
479
|
+
" origin = get_origin(target)",
|
|
480
|
+
" args = get_args(target)",
|
|
481
|
+
" if origin in (list, tuple, set):",
|
|
482
|
+
" item = args[0] if args else Any",
|
|
483
|
+
" values = [_decode(item_value, item) for item_value in (value or [])]",
|
|
484
|
+
" return values if origin is list else tuple(values) if origin is tuple else set(values)",
|
|
485
|
+
" if origin is dict:",
|
|
486
|
+
" key_type, value_type = args if len(args) == 2 else (str, Any)",
|
|
487
|
+
" return {_decode(key, key_type): _decode(item, value_type) for key, item in (value or {}).items()}",
|
|
488
|
+
' if origin in (types.UnionType, getattr(__import__("typing"), "Union", object)):',
|
|
489
|
+
" for option in args:",
|
|
490
|
+
" if option is type(None) and value is None:",
|
|
491
|
+
" return None",
|
|
492
|
+
" if option is type(None):",
|
|
493
|
+
" continue",
|
|
494
|
+
" try:",
|
|
495
|
+
" return _decode(value, option)",
|
|
496
|
+
" except (TypeError, ValueError, KeyError):",
|
|
497
|
+
" pass",
|
|
498
|
+
" return value",
|
|
499
|
+
" if isinstance(target, type) and is_dataclass(target) and isinstance(value, dict):",
|
|
500
|
+
" hints = get_type_hints(target)",
|
|
501
|
+
" result = {}",
|
|
502
|
+
" for item in fields(target):",
|
|
503
|
+
' json_name = item.metadata.get("json_name", item.name)',
|
|
504
|
+
" if json_name in value:",
|
|
505
|
+
" result[item.name] = _decode(value[json_name], hints.get(item.name, Any))",
|
|
506
|
+
" return target(**result)",
|
|
507
|
+
" if target in (str, int, float, bool) and value is not None:",
|
|
508
|
+
" return target(value)",
|
|
509
|
+
" return value",
|
|
510
|
+
"",
|
|
511
|
+
models || "# No object models were found.",
|
|
512
|
+
"",
|
|
513
|
+
aliases || "# No typed error bodies were declared.",
|
|
514
|
+
"",
|
|
515
|
+
"class Client:",
|
|
516
|
+
" def __init__(self, base_url: str, headers: Mapping[str, str] | None = None, timeout: float = 30.0):",
|
|
517
|
+
' self.base_url = base_url.rstrip("/")',
|
|
518
|
+
" self.headers = dict(headers or {})",
|
|
519
|
+
" self.timeout = timeout",
|
|
520
|
+
"",
|
|
521
|
+
" def _request(self, method: str, path: str, *, path_params: Mapping[str, Any] | None, query: Mapping[str, Any] | None, body: Any, headers: Mapping[str, str] | None, response_type: Any, error_type: Any) -> Any:",
|
|
522
|
+
" route = path",
|
|
523
|
+
" for key, value in (path_params or {}).items():",
|
|
524
|
+
' route = route.replace("{" + key + "}", urllib.parse.quote(str(value), safe=""))',
|
|
525
|
+
" # A `.` or `..` segment is a step to another path, never a value: anything between this",
|
|
526
|
+
" # client and the server may resolve it, so the call is refused instead of sent elsewhere.",
|
|
527
|
+
' if any(segment in (".", "..") for segment in route.split("/")):',
|
|
528
|
+
" raise ValueError(\"a path parameter cannot make a '.' or '..' path segment\")",
|
|
529
|
+
" params = [(key, value) for key, value in (query or {}).items() if value is not None]",
|
|
530
|
+
" url = self.base_url + route",
|
|
531
|
+
" if params:",
|
|
532
|
+
' url += ("&" if "?" in url else "?") + urllib.parse.urlencode(params, doseq=True)',
|
|
533
|
+
' payload = None if body is None else json.dumps(body).encode("utf-8")',
|
|
534
|
+
" request = urllib.request.Request(url, data=payload, method=method, headers={**self.headers, **dict(headers or {})})",
|
|
535
|
+
" if payload is not None:",
|
|
536
|
+
' request.add_header("Content-Type", "application/json")',
|
|
537
|
+
" try:",
|
|
538
|
+
" with urllib.request.urlopen(request, timeout=self.timeout) as response:",
|
|
539
|
+
" raw = response.read()",
|
|
540
|
+
" return _decode(json.loads(raw) if raw else None, response_type)",
|
|
541
|
+
" except urllib.error.HTTPError as error:",
|
|
542
|
+
" raw = error.read()",
|
|
543
|
+
" try:",
|
|
544
|
+
" value = json.loads(raw) if raw else None",
|
|
545
|
+
" except json.JSONDecodeError:",
|
|
546
|
+
' value = raw.decode("utf-8", errors="replace")',
|
|
547
|
+
" raise NifraApiError(error.code, _decode(value, error_type)) from error",
|
|
548
|
+
"",
|
|
549
|
+
methods.length === 0 ? " pass" : methods.join("\n\n"),
|
|
550
|
+
"",
|
|
551
|
+
].join("\n");
|
|
552
|
+
}
|
|
553
|
+
function goType(schema, hint, catalog, tracker, operation, location) {
|
|
554
|
+
if (schema === undefined) {
|
|
555
|
+
tracker.add(operation, location, "schema is not declared");
|
|
556
|
+
return "any";
|
|
557
|
+
}
|
|
558
|
+
if (typeof schema === "boolean") {
|
|
559
|
+
if (schema)
|
|
560
|
+
tracker.add(operation, location, "boolean true schema has no usable static type");
|
|
561
|
+
return schema ? "any" : "struct{}";
|
|
562
|
+
}
|
|
563
|
+
const ref = schemaRef(schema);
|
|
564
|
+
if (ref !== undefined) {
|
|
565
|
+
const name = catalog.refName(ref);
|
|
566
|
+
if (name !== undefined)
|
|
567
|
+
return name;
|
|
568
|
+
tracker.add(operation, location, `unresolved schema reference ${ref}`);
|
|
569
|
+
return "any";
|
|
570
|
+
}
|
|
571
|
+
const union = schemaUnion(schema);
|
|
572
|
+
if (union !== undefined) {
|
|
573
|
+
const members = union.filter((item) => schemaType(item) !== "null");
|
|
574
|
+
if (members.length === 1 && schemaIsNullable(schema)) {
|
|
575
|
+
const type = goType(members[0], hint, catalog, tracker, operation, location);
|
|
576
|
+
return type.startsWith("*") ? type : `*${type}`;
|
|
577
|
+
}
|
|
578
|
+
tracker.add(operation, location, "non-nullable unions have no direct Go representation");
|
|
579
|
+
return "any";
|
|
580
|
+
}
|
|
581
|
+
const properties = schemaProperties(schema);
|
|
582
|
+
const type = schemaType(schema);
|
|
583
|
+
if (type === "object" || properties !== undefined) {
|
|
584
|
+
if (properties === undefined &&
|
|
585
|
+
recordOf(schema)?.additionalProperties !== undefined &&
|
|
586
|
+
recordOf(schema)?.additionalProperties !== false) {
|
|
587
|
+
const extra = recordOf(schema)?.additionalProperties;
|
|
588
|
+
const extraType = typeof extra === "boolean"
|
|
589
|
+
? "any"
|
|
590
|
+
: goType(extra, `${hint}Value`, catalog, tracker, operation, location);
|
|
591
|
+
return `map[string]${extraType}`;
|
|
592
|
+
}
|
|
593
|
+
return catalog.model(schema, hint);
|
|
594
|
+
}
|
|
595
|
+
if (type === "array") {
|
|
596
|
+
return ("[]" +
|
|
597
|
+
goType(recordOf(schema)?.items, `${hint}Item`, catalog, tracker, operation, `${location}.items`));
|
|
598
|
+
}
|
|
599
|
+
if (type === "string")
|
|
600
|
+
return "string";
|
|
601
|
+
if (type === "integer")
|
|
602
|
+
return "int64";
|
|
603
|
+
if (type === "number")
|
|
604
|
+
return "float64";
|
|
605
|
+
if (type === "boolean")
|
|
606
|
+
return "bool";
|
|
607
|
+
if (type === "null")
|
|
608
|
+
return "struct{}";
|
|
609
|
+
const allOf = recordOf(schema)?.allOf;
|
|
610
|
+
if (Array.isArray(allOf) && allOf.length > 0)
|
|
611
|
+
return goType(allOf[0], hint, catalog, tracker, operation, location);
|
|
612
|
+
tracker.add(operation, location, "schema shape is opaque or unsupported");
|
|
613
|
+
return "any";
|
|
614
|
+
}
|
|
615
|
+
const GO_KEYWORDS = new Set([
|
|
616
|
+
"break",
|
|
617
|
+
"default",
|
|
618
|
+
"func",
|
|
619
|
+
"interface",
|
|
620
|
+
"select",
|
|
621
|
+
"case",
|
|
622
|
+
"defer",
|
|
623
|
+
"go",
|
|
624
|
+
"map",
|
|
625
|
+
"struct",
|
|
626
|
+
"chan",
|
|
627
|
+
"else",
|
|
628
|
+
"goto",
|
|
629
|
+
"package",
|
|
630
|
+
"switch",
|
|
631
|
+
"const",
|
|
632
|
+
"fallthrough",
|
|
633
|
+
"if",
|
|
634
|
+
"range",
|
|
635
|
+
"type",
|
|
636
|
+
"continue",
|
|
637
|
+
"for",
|
|
638
|
+
"import",
|
|
639
|
+
"return",
|
|
640
|
+
"var",
|
|
641
|
+
]);
|
|
642
|
+
// encoding/json reads a tag name made only of letters, decimal digits and this punctuation. A key
|
|
643
|
+
// with a quote, backslash, backtick, comma or control character has no struct-tag spelling, and
|
|
644
|
+
// writing it raw would end the tag (or the struct) early.
|
|
645
|
+
const GO_JSON_TAG_NAME = /^[\p{L}\p{Nd}!#$%&()*+\-./:;<=>?@[\]^_{|}~ ]+$/u;
|
|
646
|
+
function goFieldName(value) {
|
|
647
|
+
const result = pascal(value, "Field");
|
|
648
|
+
return GO_KEYWORDS.has(result.toLowerCase()) ? `${result}Value` : result;
|
|
649
|
+
}
|
|
650
|
+
function goModelFields(schema, name, catalog, tracker) {
|
|
651
|
+
const properties = schemaProperties(schema);
|
|
652
|
+
if (properties === undefined)
|
|
653
|
+
return "";
|
|
654
|
+
const required = schemaRequired(schema);
|
|
655
|
+
const used = new Set();
|
|
656
|
+
const tag = String.fromCharCode(96);
|
|
657
|
+
const fields = [];
|
|
658
|
+
for (const [jsonName, property] of Object.entries(properties)) {
|
|
659
|
+
if (!GO_JSON_TAG_NAME.test(jsonName)) {
|
|
660
|
+
tracker.add("schema", `model ${name}.${JSON.stringify(jsonName)}`, "property name cannot be spelled as a Go struct tag");
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
let fieldName = goFieldName(jsonName);
|
|
664
|
+
while (used.has(fieldName))
|
|
665
|
+
fieldName += "Value";
|
|
666
|
+
used.add(fieldName);
|
|
667
|
+
let type = goType(property, name + pascal(jsonName, "Field"), catalog, tracker, "schema", `model ${name}.${jsonName}`);
|
|
668
|
+
if (!required.has(jsonName) && type !== "any" && !type.startsWith("*") && type !== "struct{}")
|
|
669
|
+
type = `*${type}`;
|
|
670
|
+
fields.push("\t" +
|
|
671
|
+
fieldName +
|
|
672
|
+
" " +
|
|
673
|
+
type +
|
|
674
|
+
" " +
|
|
675
|
+
tag +
|
|
676
|
+
'json:"' +
|
|
677
|
+
jsonName +
|
|
678
|
+
// A bare "-" tag omits the field; "-," names a property that is literally "-".
|
|
679
|
+
(required.has(jsonName) ? (jsonName === "-" ? "," : "") : ",omitempty") +
|
|
680
|
+
'"' +
|
|
681
|
+
tag);
|
|
682
|
+
}
|
|
683
|
+
return fields.join("\n");
|
|
684
|
+
}
|
|
685
|
+
function renderGoModels(catalog, tracker) {
|
|
686
|
+
const rendered = [];
|
|
687
|
+
let index = 0;
|
|
688
|
+
while (index < catalog.models.size) {
|
|
689
|
+
const entries = [...catalog.models.entries()];
|
|
690
|
+
const [name, schema] = entries[index++];
|
|
691
|
+
const fields = goModelFields(schema, name, catalog, tracker);
|
|
692
|
+
if (isObjectModel(schema)) {
|
|
693
|
+
rendered.push(`type ${name} struct {\n${fields}\n}`);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
rendered.push(`type ${name} = ${goType(schema, name, catalog, tracker, "schema", `model ${name}`)}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return rendered.join("\n\n");
|
|
700
|
+
}
|
|
701
|
+
function goErrorInfo(entry, name, catalog, tracker) {
|
|
702
|
+
const errors = Object.entries(entry.operation.responses ?? {})
|
|
703
|
+
.filter(([status]) => ERROR_STATUS.test(status))
|
|
704
|
+
.sort(([a], [b]) => codeUnitOrder(a, b));
|
|
705
|
+
if (errors.length === 0)
|
|
706
|
+
return undefined;
|
|
707
|
+
const operationNameValue = pascal(name, "Request");
|
|
708
|
+
const interfaceName = `${operationNameValue}ErrorBody`;
|
|
709
|
+
const types = [];
|
|
710
|
+
const definitions = [];
|
|
711
|
+
const markers = [];
|
|
712
|
+
for (const [status, response] of errors) {
|
|
713
|
+
const schema = responseSchema(response);
|
|
714
|
+
let type = "struct{}";
|
|
715
|
+
if (schema !== undefined) {
|
|
716
|
+
type = goType(schema, `${operationNameValue}Error${status}`, catalog, tracker, operationLabel(entry), `response ${status}`);
|
|
717
|
+
if (type === "string" ||
|
|
718
|
+
type === "int64" ||
|
|
719
|
+
type === "float64" ||
|
|
720
|
+
type === "bool" ||
|
|
721
|
+
type.startsWith("[]") ||
|
|
722
|
+
type.startsWith("map[")) {
|
|
723
|
+
const alias = `${operationNameValue}Error${status}`;
|
|
724
|
+
definitions.push(`type ${alias} ${type}`);
|
|
725
|
+
type = alias;
|
|
726
|
+
}
|
|
727
|
+
if (type !== "any")
|
|
728
|
+
markers.push(`func (v ${type}) is${interfaceName}() {}`);
|
|
729
|
+
}
|
|
730
|
+
types.push({ status, type });
|
|
731
|
+
}
|
|
732
|
+
const hasUnknown = types.some((item) => item.type === "any");
|
|
733
|
+
const bodyInterface = hasUnknown ? "any" : interfaceName;
|
|
734
|
+
if (!hasUnknown) {
|
|
735
|
+
definitions.unshift(`type ${interfaceName} interface {\n\tis${interfaceName}()\n}`);
|
|
736
|
+
definitions.push(...markers);
|
|
737
|
+
}
|
|
738
|
+
definitions.unshift("type " +
|
|
739
|
+
operationNameValue +
|
|
740
|
+
"Error struct {\n\tStatus int\n\tBody " +
|
|
741
|
+
bodyInterface +
|
|
742
|
+
"\n\tCause error\n}\n\nfunc (e *" +
|
|
743
|
+
operationNameValue +
|
|
744
|
+
'Error) Error() string {\n\treturn fmt.Sprintf("nifra API request failed with HTTP %d", e.Status)\n}');
|
|
745
|
+
const decoderLines = [
|
|
746
|
+
"func decode" +
|
|
747
|
+
operationNameValue +
|
|
748
|
+
"Error(status int, raw []byte, cause error) *" +
|
|
749
|
+
operationNameValue +
|
|
750
|
+
"Error {",
|
|
751
|
+
`\tout := &${operationNameValue}Error{Status: status, Cause: cause}`,
|
|
752
|
+
"\tswitch status {",
|
|
753
|
+
];
|
|
754
|
+
for (const item of types) {
|
|
755
|
+
if (item.type === "struct{}" || item.type === "any")
|
|
756
|
+
continue;
|
|
757
|
+
decoderLines.push(`\tcase ${item.status}:`, `\t\tvar body ${item.type}`, "\t\tif len(raw) > 0 { if err := json.Unmarshal(raw, &body); err != nil { out.Cause = err } }", "\t\tout.Body = body");
|
|
758
|
+
}
|
|
759
|
+
decoderLines.push("\t}", "\treturn out", "}");
|
|
760
|
+
return {
|
|
761
|
+
name: `${operationNameValue}Error`,
|
|
762
|
+
definitions: definitions.join("\n\n"),
|
|
763
|
+
decoders: decoderLines.join("\n"),
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function goQueryDefinition(entry, name, catalog, tracker) {
|
|
767
|
+
const parameters = (entry.operation.parameters ?? []).filter((parameter) => parameter.in === "query");
|
|
768
|
+
if (parameters.length === 0)
|
|
769
|
+
return undefined;
|
|
770
|
+
const type = `${pascal(name, "Request")}Query`;
|
|
771
|
+
const fields = [];
|
|
772
|
+
const values = [];
|
|
773
|
+
for (const parameter of parameters) {
|
|
774
|
+
let fieldType = goType(parameter.schema, type + pascal(parameter.name, "Value"), catalog, tracker, operationLabel(entry), `query ${parameter.name}`);
|
|
775
|
+
if (!parameter.required &&
|
|
776
|
+
fieldType !== "any" &&
|
|
777
|
+
!fieldType.startsWith("*") &&
|
|
778
|
+
fieldType !== "struct{}")
|
|
779
|
+
fieldType = `*${fieldType}`;
|
|
780
|
+
const field = goFieldName(parameter.name);
|
|
781
|
+
fields.push(`\t${field} ${fieldType}`);
|
|
782
|
+
const value = parameter.required ? `fmt.Sprint(q.${field})` : `fmt.Sprint(*q.${field})`;
|
|
783
|
+
values.push(parameter.required
|
|
784
|
+
? `\tv.Set(${JSON.stringify(parameter.name)}, ${value})`
|
|
785
|
+
: "\tif q." +
|
|
786
|
+
field +
|
|
787
|
+
" != nil {\n\t\tv.Set(" +
|
|
788
|
+
JSON.stringify(parameter.name) +
|
|
789
|
+
", " +
|
|
790
|
+
value +
|
|
791
|
+
")\n\t}");
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
type,
|
|
795
|
+
source: "type " +
|
|
796
|
+
type +
|
|
797
|
+
" struct {\n" +
|
|
798
|
+
fields.join("\n") +
|
|
799
|
+
"\n}\n\nfunc (q " +
|
|
800
|
+
type +
|
|
801
|
+
") values() url.Values {\n\tv := make(url.Values)\n" +
|
|
802
|
+
values.join("\n") +
|
|
803
|
+
"\n\treturn v\n}",
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function goSdk(document, options) {
|
|
138
807
|
const entries = operations(document);
|
|
139
808
|
const names = uniqueNames(entries.map(({ method, path, operation }) => operationName(operation, method, path)));
|
|
809
|
+
const catalog = new SchemaCatalog(document.components?.schemas);
|
|
810
|
+
const tracker = new OpaqueTracker();
|
|
140
811
|
const methods = [];
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
812
|
+
const queries = [];
|
|
813
|
+
const errors = [];
|
|
814
|
+
const decoders = [];
|
|
815
|
+
const errorNames = new Map();
|
|
816
|
+
const queryTypes = new Map();
|
|
817
|
+
entries.forEach((entry, index) => {
|
|
818
|
+
const query = goQueryDefinition(entry, names[index], catalog, tracker);
|
|
819
|
+
if (query !== undefined) {
|
|
820
|
+
queries.push(query.source);
|
|
821
|
+
queryTypes.set(index, query.type);
|
|
822
|
+
}
|
|
823
|
+
const error = goErrorInfo(entry, names[index], catalog, tracker);
|
|
824
|
+
if (error !== undefined) {
|
|
825
|
+
errors.push(error.definitions);
|
|
826
|
+
decoders.push(error.decoders);
|
|
827
|
+
errorNames.set(index, error.name);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
entries.forEach((entry, index) => {
|
|
831
|
+
const { method, path, operation } = entry;
|
|
832
|
+
const name = names[index];
|
|
833
|
+
const label = operationLabel(entry);
|
|
834
|
+
const pathNames = pathParameters(path);
|
|
835
|
+
const args = [];
|
|
836
|
+
for (const parameterName of pathNames) {
|
|
837
|
+
const parameter = (operation.parameters ?? []).find((item) => item.in === "path" && item.name === parameterName);
|
|
838
|
+
const type = goType(parameter?.schema, `${pascal(name, "Request")}Path${pascal(parameterName, "Value")}`, catalog, tracker, label, `path ${parameterName}`);
|
|
839
|
+
args.push(`${goFieldName(parameterName)} ${type}`);
|
|
840
|
+
}
|
|
841
|
+
const queryType = queryTypes.get(index);
|
|
842
|
+
if (queryType !== undefined)
|
|
843
|
+
args.push(`query ${queryType}`);
|
|
844
|
+
const bodySchema = firstSchema(operation.requestBody);
|
|
845
|
+
const bodyType = operation.requestBody === undefined
|
|
846
|
+
? undefined
|
|
847
|
+
: goType(bodySchema, `${pascal(name, "Request")}Body`, catalog, tracker, label, "request body");
|
|
848
|
+
if (bodyType !== undefined)
|
|
849
|
+
args.push(`body ${bodyType}`);
|
|
850
|
+
const success = Object.entries(operation.responses ?? {}).find(([status]) => SUCCESS_STATUS.test(status));
|
|
851
|
+
const responseType = success === undefined
|
|
852
|
+
? "struct{}"
|
|
853
|
+
: goType(responseSchema(success[1]), `${pascal(name, "Request")}Response`, catalog, tracker, label, `response ${success[0]}`);
|
|
854
|
+
const errorName = errorNames.get(index);
|
|
855
|
+
const errorType = errorName === undefined ? "error" : `*${errorName}`;
|
|
856
|
+
const pathMap = pathNames.length === 0
|
|
146
857
|
? "nil"
|
|
147
|
-
:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
858
|
+
: "map[string]string{" +
|
|
859
|
+
pathNames
|
|
860
|
+
.map((value) => `${JSON.stringify(value)}: fmt.Sprint(${goFieldName(value)})`)
|
|
861
|
+
.join(", ") +
|
|
862
|
+
"}";
|
|
863
|
+
const queryValue = queryType === undefined ? "nil" : "query.values()";
|
|
864
|
+
const bodyValue = bodyType === undefined ? "nil" : "body";
|
|
865
|
+
const zeroError = errorName === undefined ? "err" : `&${errorName}{Status: 0, Cause: err}`;
|
|
866
|
+
const statusError = errorName === undefined
|
|
867
|
+
? "&NifraApiError{Status: status, Body: string(raw)}"
|
|
868
|
+
: `decode${errorName}(status, raw, nil)`;
|
|
869
|
+
const unmarshalError = errorName === undefined ? "err" : `&${errorName}{Status: status, Cause: err}`;
|
|
870
|
+
methods.push("func (c *Client) " +
|
|
871
|
+
pascal(name, "Request") +
|
|
872
|
+
"(" +
|
|
873
|
+
args.join(", ") +
|
|
874
|
+
") (" +
|
|
875
|
+
responseType +
|
|
876
|
+
", " +
|
|
877
|
+
errorType +
|
|
878
|
+
") {\n" +
|
|
879
|
+
"\tstatus, raw, err := c.request(http.Method" +
|
|
880
|
+
method[0].toUpperCase() +
|
|
881
|
+
method.slice(1) +
|
|
882
|
+
", " +
|
|
883
|
+
JSON.stringify(path) +
|
|
884
|
+
", " +
|
|
885
|
+
pathMap +
|
|
886
|
+
", " +
|
|
887
|
+
queryValue +
|
|
888
|
+
", " +
|
|
889
|
+
bodyValue +
|
|
890
|
+
")\n" +
|
|
891
|
+
"\tvar value " +
|
|
892
|
+
responseType +
|
|
893
|
+
"\n\tif err != nil {\n\t\treturn value, " +
|
|
894
|
+
zeroError +
|
|
895
|
+
"\n\t}\n" +
|
|
896
|
+
"\tif status < 200 || status >= 300 {\n\t\treturn value, " +
|
|
897
|
+
statusError +
|
|
898
|
+
"\n\t}\n" +
|
|
899
|
+
"\tif len(raw) > 0 {\n\t\tif err := json.Unmarshal(raw, &value); err != nil {\n\t\t\treturn value, " +
|
|
900
|
+
unmarshalError +
|
|
901
|
+
"\n\t\t}\n\t}\n\treturn value, nil\n}");
|
|
151
902
|
});
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
func (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
903
|
+
const models = renderGoModels(catalog, tracker);
|
|
904
|
+
if (options.strict === true && tracker.issues.length > 0)
|
|
905
|
+
throw new SdkGenerationError(tracker.issues);
|
|
906
|
+
return [
|
|
907
|
+
"// Code generated by nifra. Regenerate with: nifra sdk --lang go",
|
|
908
|
+
"package nifrasdk",
|
|
909
|
+
"",
|
|
910
|
+
"import (",
|
|
911
|
+
'\t"bytes"',
|
|
912
|
+
'\t"encoding/json"',
|
|
913
|
+
'\t"fmt"',
|
|
914
|
+
'\t"io"',
|
|
915
|
+
'\t"net/http"',
|
|
916
|
+
'\t"net/url"',
|
|
917
|
+
'\t"strings"',
|
|
918
|
+
")",
|
|
919
|
+
"",
|
|
920
|
+
"type Client struct {",
|
|
921
|
+
"\tBaseURL string",
|
|
922
|
+
"\tHTTP *http.Client",
|
|
923
|
+
"\tHeaders http.Header",
|
|
924
|
+
"}",
|
|
925
|
+
"",
|
|
926
|
+
"func NewClient(baseURL string) *Client {",
|
|
927
|
+
'\treturn &Client{BaseURL: strings.TrimRight(baseURL, "/"), HTTP: http.DefaultClient, Headers: make(http.Header)}',
|
|
928
|
+
"}",
|
|
929
|
+
"",
|
|
930
|
+
"type NifraApiError struct { Status int; Body string }",
|
|
931
|
+
'func (e *NifraApiError) Error() string { return fmt.Sprintf("nifra API request failed with HTTP %d", e.Status) }',
|
|
932
|
+
"",
|
|
933
|
+
"func (c *Client) request(method, path string, pathParams map[string]string, query url.Values, body any) (int, []byte, error) {",
|
|
934
|
+
"\troute := path",
|
|
935
|
+
'\tfor key, value := range pathParams { route = strings.ReplaceAll(route, "{"+key+"}", url.PathEscape(value)) }',
|
|
936
|
+
"\t// A `.` or `..` segment is a step to another path, never a value: anything between this client",
|
|
937
|
+
"\t// and the server may resolve it, so the call is refused instead of sent elsewhere.",
|
|
938
|
+
'\tfor _, segment := range strings.Split(route, "/") { if segment == "." || segment == ".." { return 0, nil, fmt.Errorf("a path parameter cannot make a %q or %q path segment", ".", "..") } }',
|
|
939
|
+
"\ttarget := c.BaseURL + route",
|
|
940
|
+
"\tparsed, err := url.Parse(target)",
|
|
941
|
+
"\tif err != nil { return 0, nil, err }",
|
|
942
|
+
"\tif query != nil { parsed.RawQuery = query.Encode() }",
|
|
943
|
+
"\tvar reader io.Reader",
|
|
944
|
+
"\tif body != nil { payload, err := json.Marshal(body); if err != nil { return 0, nil, err }; reader = bytes.NewReader(payload) }",
|
|
945
|
+
"\trequest, err := http.NewRequest(method, parsed.String(), reader)",
|
|
946
|
+
"\tif err != nil { return 0, nil, err }",
|
|
947
|
+
"\tfor key, values := range c.Headers { for _, value := range values { request.Header.Add(key, value) } }",
|
|
948
|
+
'\tif body != nil { request.Header.Set("Content-Type", "application/json") }',
|
|
949
|
+
"\tresponse, err := c.HTTP.Do(request)",
|
|
950
|
+
"\tif err != nil { return 0, nil, err }",
|
|
951
|
+
"\tdefer response.Body.Close()",
|
|
952
|
+
"\traw, err := io.ReadAll(response.Body)",
|
|
953
|
+
"\treturn response.StatusCode, raw, err",
|
|
954
|
+
"}",
|
|
955
|
+
"",
|
|
956
|
+
models || "// No object models were found.",
|
|
957
|
+
"",
|
|
958
|
+
queries.join("\n\n") || "// No typed query models were needed.",
|
|
959
|
+
"",
|
|
960
|
+
errors.join("\n\n") || "// No typed error bodies were declared.",
|
|
961
|
+
"",
|
|
962
|
+
decoders.join("\n\n"),
|
|
963
|
+
"",
|
|
964
|
+
methods.join("\n\n") || "// No operations were found.",
|
|
965
|
+
"",
|
|
966
|
+
].join("\n");
|
|
967
|
+
}
|
|
968
|
+
/** Render a generated SDK. Strict mode refuses opaque request/response portions. */
|
|
969
|
+
export function renderSdk(document, language, options = {}) {
|
|
970
|
+
return language === "python" ? pythonSdk(document, options) : goSdk(document, options);
|
|
971
|
+
}
|
|
972
|
+
function unsupportedTransports(backend) {
|
|
973
|
+
const routes = backend.routes;
|
|
974
|
+
if (typeof routes !== "function")
|
|
975
|
+
return [];
|
|
976
|
+
const result = [];
|
|
977
|
+
for (const route of routes.call(backend)) {
|
|
978
|
+
const method = typeof route.method === "string" ? route.method : "?";
|
|
979
|
+
const path = typeof route.path === "string" ? route.path : "?";
|
|
980
|
+
if (route.schema?.sse !== undefined) {
|
|
981
|
+
result.push(`${method} ${path} uses SSE; generated SDKs currently cover JSON HTTP only`);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return result;
|
|
985
|
+
}
|
|
986
|
+
/** Load backend/app.ts, generate an SDK, and write it to the project. */
|
|
212
987
|
export async function runSdk(cwd, options) {
|
|
213
|
-
const backendPath = resolve(cwd,
|
|
988
|
+
const backendPath = resolve(cwd, BACKEND_APP_FILE);
|
|
214
989
|
if (!existsSync(backendPath)) {
|
|
215
|
-
throw new Error(`[nifra] no
|
|
990
|
+
throw new Error(`[nifra] no ${BACKEND_APP_FILE} in ${cwd} - SDK generation needs the API contract.`);
|
|
216
991
|
}
|
|
217
992
|
const backend = (await import(__rewriteRelativeImportExtension(backendPath))).backend;
|
|
218
993
|
if (backend === undefined)
|
|
219
|
-
throw new Error(`[nifra] ${backendPath} does not export
|
|
994
|
+
throw new Error(`[nifra] ${backendPath} does not export backend.`);
|
|
995
|
+
const transportIssues = unsupportedTransports(backend);
|
|
996
|
+
if (options.strict === true && transportIssues.length > 0) {
|
|
997
|
+
throw new SdkGenerationError(transportIssues.map((reason) => ({ operation: "transport", location: "SSE", reason })));
|
|
998
|
+
}
|
|
999
|
+
for (const warning of transportIssues)
|
|
1000
|
+
console.warn(`[nifra] SDK warning: ${warning}`);
|
|
220
1001
|
const document = toOpenAPI(backend, {
|
|
221
1002
|
title: `${basename(cwd)} API`,
|
|
222
1003
|
version: "1.0.0",
|
|
223
1004
|
});
|
|
224
1005
|
const extension = options.language === "python" ? "py" : "go";
|
|
225
1006
|
const out = resolve(cwd, options.out ?? `nifra_sdk.${extension}`);
|
|
226
|
-
await Bun.write(out, renderSdk(document, options.language));
|
|
227
|
-
console.log(
|
|
1007
|
+
await Bun.write(out, renderSdk(document, options.language, options));
|
|
1008
|
+
console.log("[nifra] wrote " +
|
|
1009
|
+
options.language +
|
|
1010
|
+
" SDK with " +
|
|
1011
|
+
Object.keys(document.paths).length +
|
|
1012
|
+
" path" +
|
|
1013
|
+
(Object.keys(document.paths).length === 1 ? "" : "s") +
|
|
1014
|
+
" to " +
|
|
1015
|
+
out);
|
|
228
1016
|
}
|
|
229
1017
|
//# sourceMappingURL=sdk.js.map
|