@intentius/chant-lexicon-render 0.53.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 +89 -0
- package/dist/catalog.d.ts +100 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/codegen/docs-cli.d.ts +3 -0
- package/dist/codegen/docs-cli.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts +21 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/generate-cli.d.ts +6 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate-lexicon.d.ts +18 -0
- package/dist/codegen/generate-lexicon.d.ts.map +1 -0
- package/dist/codegen/generate-typescript.d.ts +12 -0
- package/dist/codegen/generate-typescript.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts +15 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/naming.d.ts +14 -0
- package/dist/codegen/naming.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +17 -0
- package/dist/codegen/package.d.ts.map +1 -0
- package/dist/composites/render-deploy.d.ts +88 -0
- package/dist/composites/render-deploy.d.ts.map +1 -0
- package/dist/coverage.d.ts +15 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +60 -0
- package/dist/describe-resources.d.ts.map +1 -0
- package/dist/detect.d.ts +12 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/generated/index.d.ts +38 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/runtime.d.ts +5 -0
- package/dist/generated/runtime.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/integrity.json +18 -0
- package/dist/lint/audit-catalog.d.ts +13 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts +3 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -0
- package/dist/lint/post-synth/ren010-native-runtime-requires-commands.d.ts +12 -0
- package/dist/lint/post-synth/ren010-native-runtime-requires-commands.d.ts.map +1 -0
- package/dist/lint/post-synth/ren011-service-requires-source.d.ts +12 -0
- package/dist/lint/post-synth/ren011-service-requires-source.d.ts.map +1 -0
- package/dist/lint/post-synth/ren012-free-plan-cannot-scale.d.ts +11 -0
- package/dist/lint/post-synth/ren012-free-plan-cannot-scale.d.ts.map +1 -0
- package/dist/lint/post-synth/render-helpers.d.ts +19 -0
- package/dist/lint/post-synth/render-helpers.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +7 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lint/rules/no-secret-literals.d.ts +13 -0
- package/dist/lint/rules/no-secret-literals.d.ts.map +1 -0
- package/dist/lint/rules/valid-cron-schedule.d.ts +12 -0
- package/dist/lint/rules/valid-cron-schedule.d.ts.map +1 -0
- package/dist/lint/rules/valid-region.d.ts +16 -0
- package/dist/lint/rules/valid-region.d.ts.map +1 -0
- package/dist/lsp/completions.d.ts +8 -0
- package/dist/lsp/completions.d.ts.map +1 -0
- package/dist/lsp/hover.d.ts +6 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/manifest.json +6 -0
- package/dist/meta.json +304 -0
- package/dist/okf/index.md +29 -0
- package/dist/okf/rules/REN001.md +11 -0
- package/dist/okf/rules/REN002.md +15 -0
- package/dist/okf/rules/REN003.md +15 -0
- package/dist/okf/rules/REN010.md +11 -0
- package/dist/okf/rules/REN011.md +11 -0
- package/dist/okf/rules/REN012.md +11 -0
- package/dist/okf/types/BackgroundWorker.md +24 -0
- package/dist/okf/types/CronJob.md +28 -0
- package/dist/okf/types/CustomDomain.md +14 -0
- package/dist/okf/types/Disk.md +16 -0
- package/dist/okf/types/EnvGroup.md +22 -0
- package/dist/okf/types/Environment.md +17 -0
- package/dist/okf/types/KeyValue.md +20 -0
- package/dist/okf/types/Postgres.md +29 -0
- package/dist/okf/types/PrivateService.md +24 -0
- package/dist/okf/types/Project.md +15 -0
- package/dist/okf/types/RegistryCredential.md +17 -0
- package/dist/okf/types/StaticSite.md +24 -0
- package/dist/okf/types/WebService.md +24 -0
- package/dist/okf/types/Webhook.md +17 -0
- package/dist/op/activities/fake-render.d.ts +52 -0
- package/dist/op/activities/fake-render.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +10 -0
- package/dist/op/activities/index.d.ts.map +1 -0
- package/dist/op/activities/render-apply.d.ts +229 -0
- package/dist/op/activities/render-apply.d.ts.map +1 -0
- package/dist/ownership.d.ts +32 -0
- package/dist/ownership.d.ts.map +1 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/pseudo.d.ts +21 -0
- package/dist/pseudo.d.ts.map +1 -0
- package/dist/rules/no-secret-literals.ts +73 -0
- package/dist/rules/ren010-native-runtime-requires-commands.ts +47 -0
- package/dist/rules/ren011-service-requires-source.ts +50 -0
- package/dist/rules/ren012-free-plan-cannot-scale.ts +43 -0
- package/dist/rules/render-helpers.ts +38 -0
- package/dist/rules/valid-cron-schedule.ts +51 -0
- package/dist/rules/valid-region.ts +49 -0
- package/dist/serializer.d.ts +99 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/skills/chant-render-patterns.md +126 -0
- package/dist/skills/chant-render.md +112 -0
- package/dist/spec/fetch.d.ts +21 -0
- package/dist/spec/fetch.d.ts.map +1 -0
- package/dist/spec/parse.d.ts +120 -0
- package/dist/spec/parse.d.ts.map +1 -0
- package/dist/types/index.d.ts +573 -0
- package/dist/validate-cli.d.ts +3 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate.d.ts +15 -0
- package/dist/validate.d.ts.map +1 -0
- package/package.json +73 -0
- package/src/catalog.ts +286 -0
- package/src/codegen/docs-cli.ts +7 -0
- package/src/codegen/docs.ts +120 -0
- package/src/codegen/generate-cli.ts +36 -0
- package/src/codegen/generate-lexicon.ts +52 -0
- package/src/codegen/generate-typescript.ts +70 -0
- package/src/codegen/generate.ts +106 -0
- package/src/codegen/naming.ts +55 -0
- package/src/codegen/package.ts +30 -0
- package/src/composites/render-deploy.ts +133 -0
- package/src/coverage.ts +49 -0
- package/src/describe-resources.test.ts +143 -0
- package/src/describe-resources.ts +366 -0
- package/src/detect.ts +29 -0
- package/src/generated/index.d.ts +573 -0
- package/src/generated/index.ts +43 -0
- package/src/generated/lexicon-render.json +304 -0
- package/src/generated/runtime.ts +4 -0
- package/src/index.ts +30 -0
- package/src/lint/audit-catalog.ts +47 -0
- package/src/lint/post-synth/index.ts +11 -0
- package/src/lint/post-synth/post-synth.test.ts +151 -0
- package/src/lint/post-synth/ren010-native-runtime-requires-commands.ts +47 -0
- package/src/lint/post-synth/ren011-service-requires-source.ts +50 -0
- package/src/lint/post-synth/ren012-free-plan-cannot-scale.ts +43 -0
- package/src/lint/post-synth/render-helpers.ts +38 -0
- package/src/lint/rules/index.ts +11 -0
- package/src/lint/rules/no-secret-literals.ts +73 -0
- package/src/lint/rules/rules.test.ts +73 -0
- package/src/lint/rules/valid-cron-schedule.ts +51 -0
- package/src/lint/rules/valid-region.ts +49 -0
- package/src/lsp/completions.test.ts +28 -0
- package/src/lsp/completions.ts +22 -0
- package/src/lsp/hover.test.ts +32 -0
- package/src/lsp/hover.ts +50 -0
- package/src/op/activities/fake-render.ts +275 -0
- package/src/op/activities/index.ts +51 -0
- package/src/op/activities/render-apply.test.ts +403 -0
- package/src/op/activities/render-apply.ts +1042 -0
- package/src/ownership.ts +44 -0
- package/src/package-cli.ts +23 -0
- package/src/plugin.test.ts +54 -0
- package/src/plugin.ts +258 -0
- package/src/pseudo.ts +26 -0
- package/src/serializer.test.ts +221 -0
- package/src/serializer.ts +286 -0
- package/src/skills/chant-render-patterns.md +126 -0
- package/src/skills/chant-render.md +112 -0
- package/src/spec/fetch.ts +33 -0
- package/src/spec/parse.ts +721 -0
- package/src/validate-cli.ts +5 -0
- package/src/validate.ts +47 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render Public API OpenAPI 3.0 parser.
|
|
3
|
+
*
|
|
4
|
+
* Render's spec is a REST API description — request/response DTOs, not a
|
|
5
|
+
* resource catalog — so, like fly, we generate a *curated* set of resources
|
|
6
|
+
* rather than every schema. Each curated resource pairs a create-request
|
|
7
|
+
* schema (the writable authoring surface) with a response schema (read-only
|
|
8
|
+
* attributes). The named object schemas reachable from the request schemas
|
|
9
|
+
* (`webServiceDetailsPOST`, `image`, `serviceDisk`, `cidrBlockAndDescription`,
|
|
10
|
+
* ...) become standalone property-type classes so nested config is typed all
|
|
11
|
+
* the way down; anonymous inline objects (Render leans on `#/paths/...` JSON
|
|
12
|
+
* pointers for a few of these — autoscaling, env vars, secret files) are
|
|
13
|
+
* emitted as inline TypeScript object literals, and enums inline as
|
|
14
|
+
* string-literal unions.
|
|
15
|
+
*
|
|
16
|
+
* Services are split by type. Render's `POST /services` takes a `type`
|
|
17
|
+
* discriminator plus a `serviceDetails` oneOf; authoring `new WebService({...})`
|
|
18
|
+
* with `serviceDetails` typed as `WebServiceDetails` is both tighter and more
|
|
19
|
+
* honest than one `Service` class with a five-way union, so each service type
|
|
20
|
+
* is its own resource with `type` fixed and `serviceDetails` narrowed.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
extractConstraints as coreExtractConstraints,
|
|
25
|
+
primaryType,
|
|
26
|
+
isEnumDefinition,
|
|
27
|
+
type JsonSchemaProperty,
|
|
28
|
+
type PropertyConstraints,
|
|
29
|
+
} from "@intentius/chant/codegen/json-schema";
|
|
30
|
+
|
|
31
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export type { PropertyConstraints };
|
|
34
|
+
|
|
35
|
+
export interface ParsedProperty {
|
|
36
|
+
name: string;
|
|
37
|
+
tsType: string;
|
|
38
|
+
required: boolean;
|
|
39
|
+
description?: string;
|
|
40
|
+
constraints: PropertyConstraints;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ParsedAttribute {
|
|
44
|
+
name: string;
|
|
45
|
+
tsType: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ParsedResource {
|
|
49
|
+
typeName: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
properties: ParsedProperty[];
|
|
52
|
+
attributes: ParsedAttribute[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RenderParseResult {
|
|
56
|
+
resource: ParsedResource;
|
|
57
|
+
/** Always empty — render emits property types as standalone results. */
|
|
58
|
+
propertyTypes: Array<{ name: string; defType: string }>;
|
|
59
|
+
/** Always empty — enums are inlined as string-literal unions. */
|
|
60
|
+
enums: Array<{ name: string; values: string[] }>;
|
|
61
|
+
/** Whether this entity is a property type (nested config shape). */
|
|
62
|
+
isProperty?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── OpenAPI types ──────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
interface OpenAPISchema extends JsonSchemaProperty {
|
|
68
|
+
allOf?: OpenAPISchema[];
|
|
69
|
+
oneOf?: OpenAPISchema[];
|
|
70
|
+
anyOf?: OpenAPISchema[];
|
|
71
|
+
additionalProperties?: boolean | OpenAPISchema;
|
|
72
|
+
items?: OpenAPISchema;
|
|
73
|
+
properties?: Record<string, OpenAPISchema>;
|
|
74
|
+
title?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface OpenAPISpec {
|
|
78
|
+
components?: { schemas?: Record<string, OpenAPISchema> };
|
|
79
|
+
paths?: Record<string, unknown>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Curated manifest ───────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/** The type-name namespace every render entity lives under. */
|
|
85
|
+
export const NAMESPACE = "Render";
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* How a curated resource maps onto the spec. `request`/`response` are schema
|
|
89
|
+
* locators: either a `#/components/schemas/X` name (bare `X`) or a JSON
|
|
90
|
+
* pointer into `#/paths/...` for the handful of bodies Render leaves inline.
|
|
91
|
+
*/
|
|
92
|
+
export interface ResourceSpec {
|
|
93
|
+
typeName: string;
|
|
94
|
+
request: string;
|
|
95
|
+
response: string;
|
|
96
|
+
/**
|
|
97
|
+
* Request properties chant fixes and therefore hides from the authoring
|
|
98
|
+
* surface — the service `type` discriminator. The serializer re-injects them.
|
|
99
|
+
*/
|
|
100
|
+
fixed?: Record<string, unknown>;
|
|
101
|
+
/** Property → schema locator that replaces the request schema's own type (narrowing `serviceDetails`). */
|
|
102
|
+
overrides?: Record<string, string>;
|
|
103
|
+
/**
|
|
104
|
+
* Properties the API requires but chant can default — `ownerId` resolves from
|
|
105
|
+
* the `Render.OwnerId` pseudo-parameter (`RENDER_OWNER_ID`) at build time.
|
|
106
|
+
*/
|
|
107
|
+
optional?: string[];
|
|
108
|
+
/** Synthetic properties not in the request schema (path parameters such as CustomDomain's `serviceId`). */
|
|
109
|
+
extraProperties?: ParsedProperty[];
|
|
110
|
+
/** Synthetic attributes not in the response schema (datastore connection strings, read from a side endpoint). */
|
|
111
|
+
extraAttributes?: ParsedAttribute[];
|
|
112
|
+
/**
|
|
113
|
+
* Property → additional TS union members. Render's cross-resource fields are
|
|
114
|
+
* opaque ids (`serviceId`, `projectId`, `environmentId`); widening them to
|
|
115
|
+
* accept the declared resource lets `serviceId: web` reference another entity
|
|
116
|
+
* and have the applier substitute the live id after it exists.
|
|
117
|
+
*/
|
|
118
|
+
widen?: Record<string, string>;
|
|
119
|
+
/** Property → full TS type replacement (array-valued references, where a plain union would not typecheck). */
|
|
120
|
+
retype?: Record<string, string>;
|
|
121
|
+
/**
|
|
122
|
+
* Properties the API marks optional but that are required in practice —
|
|
123
|
+
* `serviceDetails` on every runtime-bearing service (its `runtime` is
|
|
124
|
+
* required; a WebService without it is rejected by the API).
|
|
125
|
+
*/
|
|
126
|
+
require?: string[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const SERVICES = "Services";
|
|
130
|
+
const DATASTORES = "Datastores";
|
|
131
|
+
const CONFIG = "Config";
|
|
132
|
+
const PROJECTS = "Projects";
|
|
133
|
+
|
|
134
|
+
const OWNER_OPTIONAL = ["ownerId"];
|
|
135
|
+
|
|
136
|
+
const SERVICE_REF = "WebService | PrivateService | BackgroundWorker | CronJob | StaticSite";
|
|
137
|
+
|
|
138
|
+
/** Common shape of the five service resources — the `servicePOST` body with `type` fixed and `serviceDetails` narrowed. */
|
|
139
|
+
function service(name: string, type: string, details: string, requireDetails = true): ResourceSpec {
|
|
140
|
+
return {
|
|
141
|
+
typeName: `${NAMESPACE}::${SERVICES}::${name}`,
|
|
142
|
+
request: "servicePOST",
|
|
143
|
+
response: "service",
|
|
144
|
+
fixed: { type },
|
|
145
|
+
overrides: { serviceDetails: details },
|
|
146
|
+
optional: OWNER_OPTIONAL,
|
|
147
|
+
require: requireDetails ? ["serviceDetails"] : [],
|
|
148
|
+
widen: { environmentId: "Environment" },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Required fields on property-type schemas that chant defaults at build time,
|
|
154
|
+
* so authors need not repeat them: `image.ownerId` follows the owning service's
|
|
155
|
+
* `ownerId` (Render requires the two to match anyway).
|
|
156
|
+
*/
|
|
157
|
+
const PROPERTY_OPTIONAL: Record<string, string[]> = {
|
|
158
|
+
image: ["ownerId"],
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Cross-resource id fields on property-type schemas, widened to accept the
|
|
163
|
+
* declared resource (see `ResourceSpec.widen`).
|
|
164
|
+
*/
|
|
165
|
+
const PROPERTY_WIDEN: Record<string, Record<string, string>> = {
|
|
166
|
+
image: { registryCredentialId: "RegistryCredential" },
|
|
167
|
+
dockerDetailsPOST: { registryCredentialId: "RegistryCredential" },
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const CONNECTION_STRING_ATTRS: ParsedAttribute[] = [
|
|
171
|
+
{ name: "internalConnectionString", tsType: "string" },
|
|
172
|
+
{ name: "externalConnectionString", tsType: "string" },
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
export const RESOURCES: ResourceSpec[] = [
|
|
176
|
+
service("WebService", "web_service", "webServiceDetailsPOST"),
|
|
177
|
+
service("StaticSite", "static_site", "staticSiteDetailsPOST", false),
|
|
178
|
+
service("PrivateService", "private_service", "privateServiceDetailsPOST"),
|
|
179
|
+
service("BackgroundWorker", "background_worker", "backgroundWorkerDetailsPOST"),
|
|
180
|
+
service("CronJob", "cron_job", "cronJobDetailsPOST"),
|
|
181
|
+
{
|
|
182
|
+
typeName: `${NAMESPACE}::${DATASTORES}::Postgres`,
|
|
183
|
+
request: "postgresPOSTInput",
|
|
184
|
+
response: "postgresDetail",
|
|
185
|
+
optional: OWNER_OPTIONAL,
|
|
186
|
+
widen: { environmentId: "Environment" },
|
|
187
|
+
extraAttributes: [...CONNECTION_STRING_ATTRS, { name: "psqlCommand", tsType: "string" }],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
typeName: `${NAMESPACE}::${DATASTORES}::KeyValue`,
|
|
191
|
+
request: "keyValuePOSTInput",
|
|
192
|
+
response: "keyValueDetail",
|
|
193
|
+
optional: OWNER_OPTIONAL,
|
|
194
|
+
widen: { environmentId: "Environment" },
|
|
195
|
+
extraAttributes: [...CONNECTION_STRING_ATTRS, { name: "cliCommand", tsType: "string" }],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
typeName: `${NAMESPACE}::${CONFIG}::EnvGroup`,
|
|
199
|
+
request: "envGroupPOSTInput",
|
|
200
|
+
response: "envGroup",
|
|
201
|
+
optional: OWNER_OPTIONAL,
|
|
202
|
+
widen: { environmentId: "Environment" },
|
|
203
|
+
retype: { serviceIds: `(string | ${SERVICE_REF})[]` },
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
typeName: `${NAMESPACE}::${CONFIG}::RegistryCredential`,
|
|
207
|
+
request: "#/paths/~1registrycredentials/post/requestBody/content/application~1json/schema",
|
|
208
|
+
response: "registryCredential",
|
|
209
|
+
optional: OWNER_OPTIONAL,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
typeName: `${NAMESPACE}::${CONFIG}::Webhook`,
|
|
213
|
+
request: "#/paths/~1webhooks/post/requestBody/content/application~1json/schema",
|
|
214
|
+
response: "#/paths/~1webhooks/post/responses/201/content/application~1json/schema",
|
|
215
|
+
optional: OWNER_OPTIONAL,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
typeName: `${NAMESPACE}::${PROJECTS}::Project`,
|
|
219
|
+
request: "projectPOSTInput",
|
|
220
|
+
response: "project",
|
|
221
|
+
optional: OWNER_OPTIONAL,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
typeName: `${NAMESPACE}::${PROJECTS}::Environment`,
|
|
225
|
+
request: "environmentPOSTInput",
|
|
226
|
+
response: "environment",
|
|
227
|
+
widen: { projectId: "Project" },
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
typeName: `${NAMESPACE}::${SERVICES}::Disk`,
|
|
231
|
+
request: "#/paths/~1disks/post/requestBody/content/application~1json/schema",
|
|
232
|
+
response: "#/paths/~1disks/post/responses/201/content/application~1json/schema",
|
|
233
|
+
widen: { serviceId: "WebService | PrivateService | BackgroundWorker" },
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
typeName: `${NAMESPACE}::${SERVICES}::CustomDomain`,
|
|
237
|
+
request: "#/paths/~1services~1{serviceId}~1custom-domains/post/requestBody/content/application~1json/schema",
|
|
238
|
+
response: "customDomain",
|
|
239
|
+
extraProperties: [
|
|
240
|
+
{
|
|
241
|
+
name: "serviceId",
|
|
242
|
+
tsType: `string | WebService | StaticSite`,
|
|
243
|
+
required: true,
|
|
244
|
+
description: "The service the domain is attached to — an id, or a declared WebService/StaticSite.",
|
|
245
|
+
constraints: {},
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Schema-name aliases: the spec's `cronJobDetailsPOST.envSpecificDetails`
|
|
253
|
+
* points at the response-shaped `envSpecificDetails` rather than the POST one
|
|
254
|
+
* every other service type uses. Route it to the POST shape so the lexicon has
|
|
255
|
+
* one `EnvSpecificDetails` union, not two colliding classes.
|
|
256
|
+
*/
|
|
257
|
+
const SCHEMA_ALIASES: Record<string, string> = {
|
|
258
|
+
envSpecificDetails: "envSpecificDetailsPOST",
|
|
259
|
+
dockerDetails: "dockerDetailsPOST",
|
|
260
|
+
nativeEnvironmentDetails: "nativeEnvironmentDetailsPOST",
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Anonymous inline schemas Render leaves at `#/paths/...` pointers that deserve
|
|
265
|
+
* a class of their own — env var entries, secret files, autoscaling — hoisted
|
|
266
|
+
* into `components.schemas` under these names before parsing, so authors write
|
|
267
|
+
* `new EnvVar({ key, value })` and `new Autoscaling({...})` like every other
|
|
268
|
+
* property type (chant's COR001 wants each config value to be a Declarable,
|
|
269
|
+
* not an inline object). Innermost first: an outer hoist then sees its inner
|
|
270
|
+
* one as a `$ref`.
|
|
271
|
+
*/
|
|
272
|
+
const HOISTED: Array<{ pointer: string; name: string }> = [
|
|
273
|
+
{
|
|
274
|
+
pointer: "#/paths/~1services~1{serviceId}~1env-vars/put/requestBody/content/application~1json/schema/items/oneOf/0",
|
|
275
|
+
name: "EnvVar",
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
pointer: "#/paths/~1services~1{serviceId}~1env-vars/put/requestBody/content/application~1json/schema/items/oneOf/1",
|
|
279
|
+
name: "GeneratedEnvVar",
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
pointer: "#/paths/~1services~1{serviceId}~1secret-files/put/requestBody/content/application~1json/schema/items",
|
|
283
|
+
name: "SecretFile",
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
pointer: "#/paths/~1services~1{serviceId}~1autoscaling/put/requestBody/content/application~1json/schema/properties/criteria/properties/cpu",
|
|
287
|
+
name: "AutoscalingCriterion",
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
pointer: "#/paths/~1services~1{serviceId}~1autoscaling/put/requestBody/content/application~1json/schema/properties/criteria/properties/memory",
|
|
291
|
+
name: "AutoscalingCriterion",
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
pointer: "#/paths/~1services~1{serviceId}~1autoscaling/put/requestBody/content/application~1json/schema/properties/criteria",
|
|
295
|
+
name: "AutoscalingCriteria",
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
pointer: "#/paths/~1services~1{serviceId}~1autoscaling/put/requestBody/content/application~1json/schema",
|
|
299
|
+
name: "Autoscaling",
|
|
300
|
+
},
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
/** Class-name overrides where the suffix-stripping rule below misfires. */
|
|
304
|
+
const CLASS_NAME_OVERRIDES: Record<string, string> = {
|
|
305
|
+
projectPOSTEnvironmentInput: "ProjectEnvironment",
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const COMPONENT_PREFIX = "#/components/schemas/";
|
|
309
|
+
|
|
310
|
+
// ── Parser ─────────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Parse the Render OpenAPI spec into the curated resources and the property
|
|
314
|
+
* types reachable from their request schemas.
|
|
315
|
+
*/
|
|
316
|
+
export function parseRenderOpenAPI(data: string | Buffer): RenderParseResult[] {
|
|
317
|
+
const spec: OpenAPISpec = JSON.parse(typeof data === "string" ? data : data.toString("utf-8"));
|
|
318
|
+
const schemas = spec.components?.schemas ?? {};
|
|
319
|
+
const ctx = new ParseContext(spec, schemas);
|
|
320
|
+
ctx.hoist(HOISTED);
|
|
321
|
+
|
|
322
|
+
// Phase 1: discover the emitted property-type set — named object schemas
|
|
323
|
+
// reachable (transitively) from the request schemas' properties.
|
|
324
|
+
ctx.collectPropertyTypes();
|
|
325
|
+
|
|
326
|
+
const results: RenderParseResult[] = [];
|
|
327
|
+
|
|
328
|
+
// Phase 2: resources.
|
|
329
|
+
for (const rspec of RESOURCES) {
|
|
330
|
+
const req = ctx.locate(rspec.request);
|
|
331
|
+
const res = ctx.locate(rspec.response);
|
|
332
|
+
const reqProps = ctx.flattenProperties(req);
|
|
333
|
+
const requiredSet = new Set(ctx.flattenRequired(req));
|
|
334
|
+
const fixed = rspec.fixed ?? {};
|
|
335
|
+
const optional = new Set(rspec.optional ?? []);
|
|
336
|
+
const forced = new Set(rspec.require ?? []);
|
|
337
|
+
|
|
338
|
+
const properties: ParsedProperty[] = [];
|
|
339
|
+
for (const [name, prop] of Object.entries(reqProps)) {
|
|
340
|
+
if (name in fixed) continue;
|
|
341
|
+
const override = rspec.overrides?.[name];
|
|
342
|
+
let tsType = override ? ctx.resolveLocator(override) : ctx.resolveType(prop);
|
|
343
|
+
const widen = rspec.widen?.[name];
|
|
344
|
+
if (widen) tsType = `${tsType} | ${widen}`;
|
|
345
|
+
const retype = rspec.retype?.[name];
|
|
346
|
+
if (retype) tsType = retype;
|
|
347
|
+
properties.push({
|
|
348
|
+
name,
|
|
349
|
+
tsType,
|
|
350
|
+
required: (requiredSet.has(name) || forced.has(name)) && !optional.has(name),
|
|
351
|
+
description: prop.description,
|
|
352
|
+
constraints: coreExtractConstraints(prop as JsonSchemaProperty),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
for (const extra of rspec.extraProperties ?? []) properties.push(extra);
|
|
356
|
+
|
|
357
|
+
// Attributes = response props not present in the request schema.
|
|
358
|
+
const attributes: ParsedAttribute[] = [];
|
|
359
|
+
for (const [name, prop] of Object.entries(ctx.flattenProperties(res))) {
|
|
360
|
+
if (name in reqProps) continue;
|
|
361
|
+
attributes.push({ name, tsType: ctx.resolveType(prop) });
|
|
362
|
+
}
|
|
363
|
+
for (const extra of rspec.extraAttributes ?? []) attributes.push(extra);
|
|
364
|
+
|
|
365
|
+
results.push({
|
|
366
|
+
resource: { typeName: rspec.typeName, description: req?.description, properties, attributes },
|
|
367
|
+
propertyTypes: [],
|
|
368
|
+
enums: [],
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Phase 3: property-type classes.
|
|
373
|
+
for (const schemaName of ctx.emitted) {
|
|
374
|
+
const def = schemas[schemaName];
|
|
375
|
+
const requiredSet = new Set(ctx.flattenRequired(def));
|
|
376
|
+
const optional = new Set(PROPERTY_OPTIONAL[schemaName] ?? []);
|
|
377
|
+
const widen = PROPERTY_WIDEN[schemaName] ?? {};
|
|
378
|
+
const properties: ParsedProperty[] = [];
|
|
379
|
+
for (const [name, prop] of Object.entries(ctx.flattenProperties(def))) {
|
|
380
|
+
const base = ctx.resolveType(prop);
|
|
381
|
+
properties.push({
|
|
382
|
+
name,
|
|
383
|
+
tsType: widen[name] ? `${base} | ${widen[name]}` : base,
|
|
384
|
+
required: requiredSet.has(name) && !optional.has(name),
|
|
385
|
+
description: prop.description,
|
|
386
|
+
constraints: coreExtractConstraints(prop as JsonSchemaProperty),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
results.push({
|
|
390
|
+
resource: {
|
|
391
|
+
typeName: `${NAMESPACE}::${SERVICES}::${schemaToClassName(schemaName)}`,
|
|
392
|
+
description: def.description,
|
|
393
|
+
properties,
|
|
394
|
+
attributes: [],
|
|
395
|
+
},
|
|
396
|
+
propertyTypes: [],
|
|
397
|
+
enums: [],
|
|
398
|
+
isProperty: true,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return results;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
class ParseContext {
|
|
406
|
+
/** Emitted property-type schema names, in insertion order. */
|
|
407
|
+
readonly emitted: string[] = [];
|
|
408
|
+
private readonly emittedSet = new Set<string>();
|
|
409
|
+
private readonly classNames = new Map<string, string>();
|
|
410
|
+
|
|
411
|
+
constructor(
|
|
412
|
+
private readonly spec: OpenAPISpec,
|
|
413
|
+
private readonly schemas: Record<string, OpenAPISchema>,
|
|
414
|
+
) {}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Hoist anonymous inline schemas into named components: the node at each
|
|
418
|
+
* pointer moves to `schemas[name]` and a `$ref` to it takes its place. Two
|
|
419
|
+
* pointers may share a name when their shapes are identical (autoscaling's
|
|
420
|
+
* cpu/memory criteria) — the first wins, the second becomes a `$ref` to it.
|
|
421
|
+
*/
|
|
422
|
+
hoist(entries: Array<{ pointer: string; name: string }>): void {
|
|
423
|
+
for (const { pointer, name } of entries) {
|
|
424
|
+
const parts = pointer.slice(2).split("/").map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
425
|
+
const key = parts.pop()!;
|
|
426
|
+
let parent: unknown = this.spec;
|
|
427
|
+
for (const part of parts) {
|
|
428
|
+
if (!parent || typeof parent !== "object") {
|
|
429
|
+
parent = undefined;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
parent = (parent as Record<string, unknown>)[part];
|
|
433
|
+
}
|
|
434
|
+
if (!parent || typeof parent !== "object") continue;
|
|
435
|
+
const node = (parent as Record<string, unknown>)[key];
|
|
436
|
+
if (!node || typeof node !== "object") continue;
|
|
437
|
+
if ((node as OpenAPISchema).$ref) {
|
|
438
|
+
// Already a reference (Render points `memory` at `cpu`): when the
|
|
439
|
+
// hoisted class exists, retarget the reference at it — the original
|
|
440
|
+
// pointer may pass through a node an earlier hoist has since replaced.
|
|
441
|
+
if (this.schemas[name]) (parent as Record<string, unknown>)[key] = { $ref: `${COMPONENT_PREFIX}${name}` };
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (!this.schemas[name]) this.schemas[name] = node as OpenAPISchema;
|
|
445
|
+
(parent as Record<string, unknown>)[key] = { $ref: `${COMPONENT_PREFIX}${name}` };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Resolve a schema locator (bare component name or `#/...` pointer) to its schema node. */
|
|
450
|
+
locate(locator: string): OpenAPISchema | undefined {
|
|
451
|
+
if (locator.startsWith("#/")) return this.pointer(locator);
|
|
452
|
+
return this.schemas[canonicalSchemaName(locator)];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Resolve a JSON pointer inside the spec document (`#/paths/~1disks/post/...`). */
|
|
456
|
+
pointer(ref: string): OpenAPISchema | undefined {
|
|
457
|
+
const parts = ref.slice(2).split("/").map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
458
|
+
let node: unknown = this.spec;
|
|
459
|
+
for (const part of parts) {
|
|
460
|
+
if (!node || typeof node !== "object") return undefined;
|
|
461
|
+
node = (node as Record<string, unknown>)[part];
|
|
462
|
+
}
|
|
463
|
+
return node as OpenAPISchema | undefined;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Follow a `$ref` (component or pointer) to its target node. */
|
|
467
|
+
deref(ref: string): OpenAPISchema | undefined {
|
|
468
|
+
if (isComponentRef(ref)) return this.schemas[canonicalSchemaName(ref.slice(COMPONENT_PREFIX.length))];
|
|
469
|
+
if (ref.startsWith("#/")) return this.pointer(ref);
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Properties of a schema, merging `allOf` members (Render's `envGroup` is `allOf: [envGroupMeta, {...}]`). */
|
|
474
|
+
flattenProperties(def: OpenAPISchema | undefined): Record<string, OpenAPISchema> {
|
|
475
|
+
if (!def) return {};
|
|
476
|
+
const out: Record<string, OpenAPISchema> = {};
|
|
477
|
+
for (const member of def.allOf ?? []) {
|
|
478
|
+
const target = member.$ref ? this.deref(member.$ref) : member;
|
|
479
|
+
Object.assign(out, this.flattenProperties(target));
|
|
480
|
+
}
|
|
481
|
+
Object.assign(out, def.properties ?? {});
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
flattenRequired(def: OpenAPISchema | undefined): string[] {
|
|
486
|
+
if (!def) return [];
|
|
487
|
+
const out: string[] = [];
|
|
488
|
+
for (const member of def.allOf ?? []) {
|
|
489
|
+
const target = member.$ref ? this.deref(member.$ref) : member;
|
|
490
|
+
out.push(...this.flattenRequired(target));
|
|
491
|
+
}
|
|
492
|
+
out.push(...(def.required ?? []));
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Walk the curated request schemas (and their overrides) and collect the
|
|
498
|
+
* named object schemas reachable through their properties — following $ref,
|
|
499
|
+
* allOf, oneOf, arrays, and maps. Only `#/components/schemas/X` targets become
|
|
500
|
+
* classes; anonymous inline objects stay inline.
|
|
501
|
+
*/
|
|
502
|
+
collectPropertyTypes(): void {
|
|
503
|
+
const queue: string[] = [];
|
|
504
|
+
const seed = (node: unknown) => {
|
|
505
|
+
for (const ref of collectRefs(node)) {
|
|
506
|
+
if (!isComponentRef(ref)) {
|
|
507
|
+
// A pointer into paths (or a nested component property): walk its target for further component refs.
|
|
508
|
+
const target = this.deref(ref);
|
|
509
|
+
if (target) seed(target);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const name = canonicalSchemaName(ref.slice(COMPONENT_PREFIX.length));
|
|
513
|
+
const target = this.schemas[name];
|
|
514
|
+
if (!target) continue;
|
|
515
|
+
if (this.isObjectSchema(target) && !this.emittedSet.has(name)) {
|
|
516
|
+
this.emittedSet.add(name);
|
|
517
|
+
this.emitted.push(name);
|
|
518
|
+
queue.push(name);
|
|
519
|
+
} else if (!this.isObjectSchema(target)) {
|
|
520
|
+
// A oneOf-of-refs (envSpecificDetailsPOST) or array alias: walk through it.
|
|
521
|
+
seed(target);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
for (const rspec of RESOURCES) {
|
|
527
|
+
const req = this.locate(rspec.request);
|
|
528
|
+
const props = this.flattenProperties(req);
|
|
529
|
+
for (const [name, prop] of Object.entries(props)) {
|
|
530
|
+
if (rspec.fixed && name in rspec.fixed) continue;
|
|
531
|
+
const override = rspec.overrides?.[name];
|
|
532
|
+
if (override) {
|
|
533
|
+
seed({ $ref: override.startsWith("#/") ? override : `${COMPONENT_PREFIX}${override}` });
|
|
534
|
+
} else {
|
|
535
|
+
seed(prop);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
while (queue.length > 0) {
|
|
540
|
+
const name = queue.shift()!;
|
|
541
|
+
seed(this.flattenProperties(this.schemas[name]));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** An object schema with properties (directly or via allOf), not a pure enum. */
|
|
546
|
+
isObjectSchema(def: OpenAPISchema): boolean {
|
|
547
|
+
if (isEnumDefinition(def)) return false;
|
|
548
|
+
return Object.keys(this.flattenProperties(def)).length > 0;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Resolve a locator (as used by `overrides`) to a TS type. */
|
|
552
|
+
resolveLocator(locator: string): string {
|
|
553
|
+
if (locator.startsWith("#/")) return this.resolveType({ $ref: locator });
|
|
554
|
+
return this.resolveType({ $ref: `${COMPONENT_PREFIX}${locator}` });
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Resolve an OpenAPI schema node to its TypeScript type string. Emitted object
|
|
559
|
+
* schemas resolve to their class name; enums inline as string-literal unions;
|
|
560
|
+
* anonymous objects inline as object literals; everything else falls back to
|
|
561
|
+
* primitives or `Record<string, any>`.
|
|
562
|
+
*/
|
|
563
|
+
resolveType(prop: OpenAPISchema | undefined, depth = 0): string {
|
|
564
|
+
if (!prop) return "any";
|
|
565
|
+
if (depth > 8) return "any";
|
|
566
|
+
|
|
567
|
+
if (prop.$ref) return this.resolveRefType(prop.$ref, depth);
|
|
568
|
+
|
|
569
|
+
// allOf: [{ $ref }] — the OpenAPI idiom for "typed as this schema"; a
|
|
570
|
+
// multi-member allOf (envGroup) is an object merge — inline it.
|
|
571
|
+
if (prop.allOf && prop.allOf.length > 0) {
|
|
572
|
+
if (prop.allOf.length === 1 && prop.allOf[0].$ref) return this.resolveRefType(prop.allOf[0].$ref, depth);
|
|
573
|
+
return this.inlineObject(prop, depth);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const variants = prop.oneOf ?? prop.anyOf;
|
|
577
|
+
if (variants && variants.length > 0) {
|
|
578
|
+
const members = variants.map((v) => this.resolveType(v, depth + 1));
|
|
579
|
+
return dedupe(members).join(" | ");
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (prop.enum && prop.enum.length > 0) {
|
|
583
|
+
return enumUnion(prop.enum);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const pt = primaryType(prop.type);
|
|
587
|
+
switch (pt) {
|
|
588
|
+
case "string":
|
|
589
|
+
return "string";
|
|
590
|
+
case "integer":
|
|
591
|
+
case "number":
|
|
592
|
+
return "number";
|
|
593
|
+
case "boolean":
|
|
594
|
+
return "boolean";
|
|
595
|
+
case "array": {
|
|
596
|
+
if (prop.items) {
|
|
597
|
+
const itemType = this.resolveType(prop.items, depth + 1);
|
|
598
|
+
return itemType.includes(" | ") ? `(${itemType})[]` : `${itemType}[]`;
|
|
599
|
+
}
|
|
600
|
+
return "any[]";
|
|
601
|
+
}
|
|
602
|
+
case "object":
|
|
603
|
+
if (prop.properties && Object.keys(prop.properties).length > 0) {
|
|
604
|
+
return this.inlineObject(prop, depth);
|
|
605
|
+
}
|
|
606
|
+
if (prop.additionalProperties && typeof prop.additionalProperties === "object") {
|
|
607
|
+
return `Record<string, ${this.resolveType(prop.additionalProperties, depth + 1)}>`;
|
|
608
|
+
}
|
|
609
|
+
return "Record<string, any>";
|
|
610
|
+
default:
|
|
611
|
+
// Untyped node with properties (some inline bodies omit `type`).
|
|
612
|
+
if (prop.properties && Object.keys(prop.properties).length > 0) return this.inlineObject(prop, depth);
|
|
613
|
+
return "any";
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Render an anonymous object schema as a TS object literal type. */
|
|
618
|
+
private inlineObject(def: OpenAPISchema, depth: number): string {
|
|
619
|
+
const props = this.flattenProperties(def);
|
|
620
|
+
const required = new Set(this.flattenRequired(def));
|
|
621
|
+
const members = Object.entries(props).map(([name, p]) => {
|
|
622
|
+
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
623
|
+
return `${key}${required.has(name) ? "" : "?"}: ${this.resolveType(p, depth + 1)}`;
|
|
624
|
+
});
|
|
625
|
+
return `{ ${members.join("; ")} }`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** Resolve a `$ref` (component or pointer) to a TypeScript type. */
|
|
629
|
+
private resolveRefType(ref: string, depth: number): string {
|
|
630
|
+
if (!isComponentRef(ref)) {
|
|
631
|
+
// A pointer into paths, or into a component's nested property
|
|
632
|
+
// (`#/components/schemas/redisDetail/properties/maintenance`) — no
|
|
633
|
+
// class, resolve structurally.
|
|
634
|
+
return ref.startsWith("#/") ? this.resolveType(this.pointer(ref), depth + 1) : "any";
|
|
635
|
+
}
|
|
636
|
+
const name = canonicalSchemaName(ref.slice(COMPONENT_PREFIX.length));
|
|
637
|
+
const def = this.schemas[name];
|
|
638
|
+
if (!def) return "any";
|
|
639
|
+
|
|
640
|
+
// Emitted object schemas reference their class by name.
|
|
641
|
+
if (this.emittedSet.has(name)) return schemaToClassName(name);
|
|
642
|
+
|
|
643
|
+
// Enums inline as string-literal unions.
|
|
644
|
+
if (isEnumDefinition(def)) return enumUnion(def.enum ?? []);
|
|
645
|
+
|
|
646
|
+
// Non-object aliases (oneOf-of-refs, arrays, primitives): resolve through.
|
|
647
|
+
if (!this.isObjectSchema(def)) return this.resolveType(def, depth + 1);
|
|
648
|
+
|
|
649
|
+
// Object schemas we did not emit (response-only shapes) loosen to a map.
|
|
650
|
+
return "Record<string, any>";
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
655
|
+
|
|
656
|
+
/** Collect every `$ref` string nested anywhere in a node. */
|
|
657
|
+
function collectRefs(node: unknown, acc: Set<string> = new Set()): Set<string> {
|
|
658
|
+
if (!node || typeof node !== "object") return acc;
|
|
659
|
+
if (Array.isArray(node)) {
|
|
660
|
+
for (const item of node) collectRefs(item, acc);
|
|
661
|
+
return acc;
|
|
662
|
+
}
|
|
663
|
+
const obj = node as Record<string, unknown>;
|
|
664
|
+
const ref = obj.$ref;
|
|
665
|
+
if (typeof ref === "string") acc.add(ref);
|
|
666
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
667
|
+
if (key === "$ref") continue;
|
|
668
|
+
collectRefs(value, acc);
|
|
669
|
+
}
|
|
670
|
+
return acc;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** A `$ref` to a whole component schema (`#/components/schemas/X`), not a pointer into one. */
|
|
674
|
+
function isComponentRef(ref: string): boolean {
|
|
675
|
+
return ref.startsWith(COMPONENT_PREFIX) && !ref.slice(COMPONENT_PREFIX.length).includes("/");
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function enumUnion(values: unknown[]): string {
|
|
679
|
+
return [...values].map((v) => JSON.stringify(v)).sort().join(" | ");
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function dedupe(values: string[]): string[] {
|
|
683
|
+
return [...new Set(values)];
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** Apply the schema aliases (spec quirks) to a component name. */
|
|
687
|
+
export function canonicalSchemaName(name: string): string {
|
|
688
|
+
return SCHEMA_ALIASES[name] ?? name;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ── Naming helpers ─────────────────────────────────────────────────
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Convert a component schema name into a PascalCase class name, dropping the
|
|
695
|
+
* request-shape suffixes Render uses so the authoring surface reads as the
|
|
696
|
+
* concept, not the wire DTO:
|
|
697
|
+
* "webServiceDetailsPOST" → "WebServiceDetails", "readReplicaInput" →
|
|
698
|
+
* "ReadReplica", "routePost" → "Route", "cidrBlockAndDescription" →
|
|
699
|
+
* "CidrBlockAndDescription".
|
|
700
|
+
*/
|
|
701
|
+
export function schemaToClassName(schemaName: string): string {
|
|
702
|
+
const override = CLASS_NAME_OVERRIDES[schemaName];
|
|
703
|
+
if (override) return override;
|
|
704
|
+
let base = schemaName;
|
|
705
|
+
base = base.replace(/Input$/, "");
|
|
706
|
+
base = base.replace(/(POST|PATCH|PUT|Post|Patch|Put)$/, "");
|
|
707
|
+
base = base.replace(/Input$/, "");
|
|
708
|
+
return base.charAt(0).toUpperCase() + base.slice(1);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** Extract short name: "Render::Services::WebService" → "WebService". */
|
|
712
|
+
export function renderShortName(typeName: string): string {
|
|
713
|
+
const parts = typeName.split("::");
|
|
714
|
+
return parts[parts.length - 1];
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Extract service name: "Render::Services::WebService" → "Services". */
|
|
718
|
+
export function renderServiceName(typeName: string): string {
|
|
719
|
+
const parts = typeName.split("::");
|
|
720
|
+
return parts.length >= 2 ? parts[1] : SERVICES;
|
|
721
|
+
}
|