@voxgig/sdkgen 4.17.6 → 4.18.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/bin/voxgig-sdkgen +1 -1
- package/dist/cmp/AgentGuideContent.js +30 -0
- package/dist/cmp/AgentGuideContent.js.map +1 -1
- package/dist/cmp/FeatureDocs.d.ts +6 -1
- package/dist/cmp/FeatureDocs.js +27 -0
- package/dist/cmp/FeatureDocs.js.map +1 -1
- package/dist/cmp/ReadmeRefFeatures.js +23 -13
- package/dist/cmp/ReadmeRefFeatures.js.map +1 -1
- package/dist/helpers/applicability.js +7 -0
- package/dist/helpers/applicability.js.map +1 -1
- package/dist/helpers/canonSpec.d.ts +12 -0
- package/dist/helpers/canonSpec.js +259 -0
- package/dist/helpers/canonSpec.js.map +1 -0
- package/dist/helpers/optspec.d.ts +4 -0
- package/dist/helpers/optspec.js +152 -0
- package/dist/helpers/optspec.js.map +1 -0
- package/dist/sdkgen.d.ts +3 -1
- package/dist/sdkgen.js +12 -3
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/model/sdkgen.aon +125 -0
- package/package.json +1 -1
- package/project/.sdk/model/feature/audit.aon +15 -0
- package/project/.sdk/model/feature/cache.aon +13 -0
- package/project/.sdk/model/feature/clienttrack.aon +21 -0
- package/project/.sdk/model/feature/cost.aon +15 -0
- package/project/.sdk/model/feature/debug.aon +15 -0
- package/project/.sdk/model/feature/feature-index.aon +1 -0
- package/project/.sdk/model/feature/idempotency.aon +13 -0
- package/project/.sdk/model/feature/log.aon +13 -0
- package/project/.sdk/model/feature/metrics.aon +13 -0
- package/project/.sdk/model/feature/netsim.aon +14 -0
- package/project/.sdk/model/feature/paging.aon +15 -0
- package/project/.sdk/model/feature/proxy.aon +13 -0
- package/project/.sdk/model/feature/ratelimit.aon +15 -0
- package/project/.sdk/model/feature/retry.aon +15 -0
- package/project/.sdk/model/feature/streaming.aon +15 -0
- package/project/.sdk/model/feature/telemetry.aon +19 -0
- package/project/.sdk/model/feature/test.aon +13 -0
- package/project/.sdk/model/feature/timeout.aon +15 -0
- package/project/.sdk/model/feature/validate.aon +73 -0
- package/project/.sdk/model/target/js.aon +1 -1
- package/project/.sdk/model/target/ts.aon +1 -1
- package/project/.sdk/src/cmp/js/Main_js.ts +2 -0
- package/project/.sdk/src/cmp/js/Schema_js.ts +66 -0
- package/project/.sdk/src/cmp/scala/Main_scala.ts +30 -0
- package/project/.sdk/src/cmp/ts/Main_ts.ts +2 -0
- package/project/.sdk/src/cmp/ts/Schema_ts.ts +66 -0
- package/project/.sdk/tm/js/src/feature/validate/ValidateFeature.js +296 -0
- package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +26 -61
- package/project/.sdk/tm/ts/src/feature/validate/ValidateFeature.ts +298 -0
- package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +24 -61
- package/project/sdkgen-package.json +3 -2
- package/src/cmp/AgentGuideContent.ts +30 -0
- package/src/cmp/FeatureDocs.ts +39 -0
- package/src/cmp/ReadmeRefFeatures.ts +24 -13
- package/src/helpers/applicability.ts +8 -0
- package/src/helpers/canonSpec.ts +301 -0
- package/src/helpers/optspec.ts +180 -0
- package/src/sdkgen.ts +9 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// Canonical type-sentinel -> struct.validate SPEC mapper.
|
|
2
|
+
//
|
|
3
|
+
// The sibling of canonType: that table turns a field's canon sentinel into a
|
|
4
|
+
// language TYPE NAME (what the generated source declares); this one turns the
|
|
5
|
+
// same sentinel into a struct.validate SPEC VALUE (what the generated runtime
|
|
6
|
+
// checks a payload against). One vocabulary, two renderings, and — as with
|
|
7
|
+
// canonType — a SINGLE mapping that components must not copy locally.
|
|
8
|
+
//
|
|
9
|
+
// WHY THIS IS ALMOST NOTHING. apidef stores `$STRING`, `$INTEGER`, `$NUMBER`,
|
|
10
|
+
// `$BOOLEAN`, `$NULL`, `$ARRAY`, `$OBJECT`, `$ANY` on `fields[].type`, and
|
|
11
|
+
// struct.validate's own vocabulary is `$STRING $NUMBER $INTEGER $DECIMAL
|
|
12
|
+
// $BOOLEAN $NULL $NIL $MAP $LIST $FUNCTION $INSTANCE $ANY $CHILD $ONE $EXACT`.
|
|
13
|
+
// Six of the eight scalar names are the IDENTICAL string. Only three things
|
|
14
|
+
// have to be adapted, and each is a real difference rather than a translation:
|
|
15
|
+
//
|
|
16
|
+
// 1. CONTAINERS ARE NAMED DIFFERENTLY. apidef speaks OpenAPI ($ARRAY,
|
|
17
|
+
// $OBJECT), struct speaks its own value model ($LIST, $MAP).
|
|
18
|
+
//
|
|
19
|
+
// 2. UNIONS ARE SHAPED DIFFERENTLY. apidef nests the members —
|
|
20
|
+
// ['`$ONE`', [a, b]] — and struct wants them flat: ['`$ONE`', a, b].
|
|
21
|
+
// The nested form does not fail loudly, which is the trap: struct reads
|
|
22
|
+
// the inner LIST as a single alternative, so `['`$ONE`', ['`$STRING`',
|
|
23
|
+
// '`$INTEGER`']]` rejects the string 'x' with "expected [string,integer]".
|
|
24
|
+
//
|
|
25
|
+
// 3. STRUCT'S `$STRING` REJECTS THE EMPTY STRING. That is right for an
|
|
26
|
+
// option (an empty apikey is a missing apikey) and wrong for API data,
|
|
27
|
+
// where '' is an ordinary value a server returns. A field spec therefore
|
|
28
|
+
// widens to ['`$ONE`', '`$STRING`', ['`$EXACT`', '']].
|
|
29
|
+
//
|
|
30
|
+
// OPTIONALITY. struct requires every key its spec names, so an optional field
|
|
31
|
+
// is the union of its type with `$NIL` (which matches an ABSENT value; `$NULL`
|
|
32
|
+
// matches a stored JSON null). `req: false` on a field, and `optional: true`
|
|
33
|
+
// on an opRequestShape item, both land here.
|
|
34
|
+
//
|
|
35
|
+
// WHAT THIS CANNOT EXPRESS, because the model does not carry it (the same
|
|
36
|
+
// KNOWN GAPS canonType records): array ELEMENT types, nested object schemas,
|
|
37
|
+
// enums, string formats, and any numeric or length bound. A spec built here
|
|
38
|
+
// checks the shape the model knows about and nothing more — which is why
|
|
39
|
+
// record specs are `$OPEN` (below) rather than closed.
|
|
40
|
+
|
|
41
|
+
import { each } from 'jostraca'
|
|
42
|
+
|
|
43
|
+
import { canonKey } from './canonType'
|
|
44
|
+
import { opRequestShape } from './opShape'
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
// Backtick-wrapped, the way the model and struct both write an injection.
|
|
48
|
+
// Built rather than written literally so the backticks cannot be lost to an
|
|
49
|
+
// editor, a copy-paste, or a template literal.
|
|
50
|
+
const BT = String.fromCharCode(96)
|
|
51
|
+
|
|
52
|
+
function sentinel(name: string): string {
|
|
53
|
+
return BT + '$' + name + BT
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
const S_ONE = sentinel('ONE')
|
|
58
|
+
const S_EXACT = sentinel('EXACT')
|
|
59
|
+
const S_NIL = sentinel('NIL')
|
|
60
|
+
const S_ANY = sentinel('ANY')
|
|
61
|
+
const S_STRING = sentinel('STRING')
|
|
62
|
+
const S_NULL = sentinel('NULL')
|
|
63
|
+
const S_OPEN = sentinel('OPEN')
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
// Canon sentinel key -> the struct.validate sentinel that checks it.
|
|
67
|
+
// STRING is deliberately absent: it needs the empty-string widening above and
|
|
68
|
+
// is handled in canonToSpec, so a caller cannot get the bare form by accident.
|
|
69
|
+
const CANON_STRUCT: Record<string, string> = {
|
|
70
|
+
INTEGER: sentinel('INTEGER'),
|
|
71
|
+
NUMBER: sentinel('NUMBER'),
|
|
72
|
+
BOOLEAN: sentinel('BOOLEAN'),
|
|
73
|
+
NULL: sentinel('NULL'),
|
|
74
|
+
|
|
75
|
+
// The container renaming (difference 1 above).
|
|
76
|
+
ARRAY: sentinel('LIST'),
|
|
77
|
+
OBJECT: sentinel('MAP'),
|
|
78
|
+
|
|
79
|
+
ANY: S_ANY,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
// A string that may be empty (difference 3 above).
|
|
84
|
+
const SPEC_STRING: any[] = [S_ONE, S_STRING, [S_EXACT, '']]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
// Flatten a spec into the alternatives of a union, so that widening a value
|
|
88
|
+
// that is ALREADY a union does not nest one `$ONE` inside another.
|
|
89
|
+
function alternatives(spec: any): any[] {
|
|
90
|
+
if (Array.isArray(spec) && S_ONE === spec[0]) {
|
|
91
|
+
return spec.slice(1)
|
|
92
|
+
}
|
|
93
|
+
return [spec]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
// A spec that also admits an ABSENT value.
|
|
98
|
+
//
|
|
99
|
+
// struct requires every key its spec names, so this is how "optional" is
|
|
100
|
+
// spelled. Flattened through `alternatives`, so widening something that is
|
|
101
|
+
// already a union (an empty-string-tolerant `$STRING`, an apidef `$ONE`)
|
|
102
|
+
// adds one member rather than nesting a union inside a union — which struct
|
|
103
|
+
// reads as a single alternative and rejects.
|
|
104
|
+
function optionalSpec(spec: any): any {
|
|
105
|
+
// `$ANY` already admits an absent value, so widening it would only make
|
|
106
|
+
// the spec longer and its error messages worse.
|
|
107
|
+
if (S_ANY === spec) {
|
|
108
|
+
return spec
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return [S_ONE, ...alternatives(spec), S_NIL]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
// Map one field/param type sentinel to a struct.validate spec value.
|
|
116
|
+
//
|
|
117
|
+
// Unknown or missing sentinel -> `$ANY`, never a throw: this mirrors
|
|
118
|
+
// canonToType's contract, and for the same reason. A sentinel this table has
|
|
119
|
+
// not met is a model the generator should still emit for, and an over-strict
|
|
120
|
+
// spec would reject live traffic at runtime rather than fail a build.
|
|
121
|
+
//
|
|
122
|
+
// `optional` widens the result with `$NIL` so an absent key passes.
|
|
123
|
+
function canonToSpec(type: unknown, optional?: boolean): any {
|
|
124
|
+
let spec: any
|
|
125
|
+
|
|
126
|
+
if (Array.isArray(type)) {
|
|
127
|
+
// The union sentinel. Members are mapped individually and SPLICED IN
|
|
128
|
+
// flat (difference 2 above); a member may itself be a union.
|
|
129
|
+
if ('ONE' === canonKey(type[0]) && Array.isArray(type[1])) {
|
|
130
|
+
const members: any[] = []
|
|
131
|
+
for (const member of type[1]) {
|
|
132
|
+
for (const alt of alternatives(canonToSpec(member))) {
|
|
133
|
+
if (members.indexOf(alt) < 0 || 'string' !== typeof alt) {
|
|
134
|
+
members.push(alt)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A NULLABLE FIELD NEEDS `$NIL`, NOT JUST `$NULL`.
|
|
140
|
+
//
|
|
141
|
+
// apidef writes a nullable field as ['`$ONE`', [<type>, '`$NULL`']],
|
|
142
|
+
// and `$NULL` on its own does match a stored null. Inside a union it
|
|
143
|
+
// does not: struct resolves each alternative through a lookup that
|
|
144
|
+
// reads a stored null as "no value", so the value that reaches the
|
|
145
|
+
// `$NULL` validator is undefined and every nullable field rejected the
|
|
146
|
+
// one value it exists to allow. `$NIL` matches that no-value, so the
|
|
147
|
+
// pair covers both readings.
|
|
148
|
+
//
|
|
149
|
+
// THE COST, stated because it is real and not an oversight: `$NIL` is
|
|
150
|
+
// also how an OPTIONAL field is spelled, and struct cannot tell a
|
|
151
|
+
// stored null from an absent key inside a union — the lookup collapses
|
|
152
|
+
// them before any alternative sees the value. So a REQUIRED nullable
|
|
153
|
+
// field (`string | null`, `req: true`) also passes when the key is
|
|
154
|
+
// missing. The alternative is worse: drop `$NIL` and a nullable field
|
|
155
|
+
// rejects null, which is the one value it is declared to hold, for
|
|
156
|
+
// EVERY such field rather than weakening presence on a subset. Closing
|
|
157
|
+
// the gap needs a presence check outside struct, i.e. a second
|
|
158
|
+
// validation mechanism beside the one this whole module exists to
|
|
159
|
+
// reuse. Revisit if struct gains a spelling that separates them.
|
|
160
|
+
if (0 <= members.indexOf(S_NULL) && members.indexOf(S_NIL) < 0) {
|
|
161
|
+
members.push(S_NIL)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
spec = 0 === members.length ? S_ANY : [S_ONE, ...members]
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// An array-shaped sentinel that is not a union is a shape this mapper
|
|
168
|
+
// has not met. `$ANY` over a guess.
|
|
169
|
+
spec = S_ANY
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
const key = canonKey(type)
|
|
174
|
+
spec = 'STRING' === key ? SPEC_STRING : (CANON_STRUCT[key] ?? S_ANY)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return optional ? optionalSpec(spec) : spec
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
// The spec for one entity RECORD: every active field, keyed by name.
|
|
182
|
+
//
|
|
183
|
+
// OPEN, ALWAYS. apidef records the properties a spec declared, which is not a
|
|
184
|
+
// promise that a response carries nothing else — a server that adds a field is
|
|
185
|
+
// not breaking its clients, and a closed spec would turn that into a client
|
|
186
|
+
// error for every caller at once. The declared fields are type-checked; the
|
|
187
|
+
// rest pass. A caller who wants the closed reading drops `$OPEN` at runtime
|
|
188
|
+
// (the validate feature's `strict` option does exactly that).
|
|
189
|
+
function entityDataSpec(ent: any): Record<string, any> {
|
|
190
|
+
const spec: Record<string, any> = { [S_OPEN]: true }
|
|
191
|
+
|
|
192
|
+
const fields = (ent && ent.fields) ? each(ent.fields) : []
|
|
193
|
+
for (const f of fields) {
|
|
194
|
+
if (null == f || null == f.name || false === f.active) {
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
spec[f.name] = canonToSpec(f.type, false === f.req)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return spec
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
// The spec for one operation's REQUEST payload.
|
|
205
|
+
//
|
|
206
|
+
// The members and their optionality are opRequestShape's answer, not a second
|
|
207
|
+
// reading of the model: the partiality policy that decides what a generated
|
|
208
|
+
// `<Name>CreateData` requires is the same policy that decides what a create
|
|
209
|
+
// call must carry, and two copies of it would drift the moment one is edited.
|
|
210
|
+
function entityOpSpec(ent: any, opname: string): Record<string, any> | null {
|
|
211
|
+
const { items } = opRequestShape(ent, opname)
|
|
212
|
+
if (0 === items.length) {
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const spec: Record<string, any> = { [S_OPEN]: true }
|
|
217
|
+
for (const it of items) {
|
|
218
|
+
spec[it.name] = canonToSpec(it.type, it.optional)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return spec
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
// Every spec one entity needs at runtime: its record shape, and one per op it
|
|
226
|
+
// declares. An entity with no fields and no ops yields `{ data: {$OPEN} }`,
|
|
227
|
+
// which validates everything — the honest answer for a model that says
|
|
228
|
+
// nothing, and cheaper for the runtime than a missing-key branch.
|
|
229
|
+
function entitySpecs(ent: any): { data: Record<string, any>, op: Record<string, any> } {
|
|
230
|
+
const op: Record<string, any> = {}
|
|
231
|
+
|
|
232
|
+
const ops = (ent && ent.op) || {}
|
|
233
|
+
for (const opname of Object.keys(ops).sort()) {
|
|
234
|
+
const opspec = entityOpSpec(ent, opname)
|
|
235
|
+
if (null != opspec) {
|
|
236
|
+
op[opname] = opspec
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { data: entityDataSpec(ent), op }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
// A DEFAULT VALUE, read as a type.
|
|
245
|
+
//
|
|
246
|
+
// A feature declares its options as defaults (`ttl: 5000`, `currency: 'USD'`,
|
|
247
|
+
// `methods: ['GET']`) — a map written to document what happens when you say
|
|
248
|
+
// nothing, not to state a type. struct.validate reads it by example, and by
|
|
249
|
+
// example is SHARPER than the author meant: `unit: 0` says "an integer", so
|
|
250
|
+
// cost's own `unit: 0.002` failed its own feature's spec; `header: ''` says
|
|
251
|
+
// "a non-empty string", which the default itself is not.
|
|
252
|
+
//
|
|
253
|
+
// So the default is kept for its KIND and widened to that kind's sentinel.
|
|
254
|
+
// Nothing is lost by dropping the value: a feature's defaults are applied by
|
|
255
|
+
// the feature's own code (`this._options.currency || 'USD'`), never by this
|
|
256
|
+
// spec — each feature entry is optional, and struct fills in nothing through
|
|
257
|
+
// an optional union.
|
|
258
|
+
//
|
|
259
|
+
// This applies to a DEFAULTS map only. `main.kit.optspec` is written as a
|
|
260
|
+
// spec, so its concrete values are the author's own choice of constraint and
|
|
261
|
+
// are passed through untouched.
|
|
262
|
+
function byExampleSpec(val: any): any {
|
|
263
|
+
if (null == val) {
|
|
264
|
+
return S_ANY
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (Array.isArray(val)) {
|
|
268
|
+
return sentinel('LIST')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const t = typeof val
|
|
272
|
+
|
|
273
|
+
if ('string' === t) {
|
|
274
|
+
return SPEC_STRING
|
|
275
|
+
}
|
|
276
|
+
if ('number' === t) {
|
|
277
|
+
// `$NUMBER` admits integer and decimal alike; `$INTEGER` would re-impose
|
|
278
|
+
// exactly the trap this function exists to remove.
|
|
279
|
+
return sentinel('NUMBER')
|
|
280
|
+
}
|
|
281
|
+
if ('boolean' === t) {
|
|
282
|
+
return sentinel('BOOLEAN')
|
|
283
|
+
}
|
|
284
|
+
if ('function' === t) {
|
|
285
|
+
return S_ANY
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return sentinel('MAP')
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
export {
|
|
293
|
+
byExampleSpec,
|
|
294
|
+
canonToSpec,
|
|
295
|
+
optionalSpec,
|
|
296
|
+
entityDataSpec,
|
|
297
|
+
entityOpSpec,
|
|
298
|
+
entitySpecs,
|
|
299
|
+
sentinel,
|
|
300
|
+
SPEC_STRING,
|
|
301
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// THE CLIENT OPTION SPEC, assembled from the model.
|
|
2
|
+
//
|
|
3
|
+
// One schema, not twenty. `main.kit.optspec` (model/sdkgen.aon) declares the
|
|
4
|
+
// standard SDK options; each active feature's own `config.options` declares
|
|
5
|
+
// its own. Both are already struct.validate specs in the by-example sense — a
|
|
6
|
+
// concrete value is the type AND the default — so assembling them is a merge,
|
|
7
|
+
// not a translation. The `Spec` component writes the result into the SDK and
|
|
8
|
+
// makeOptions validates the caller's options against it.
|
|
9
|
+
//
|
|
10
|
+
// WHAT THIS REPLACES. Every language's make_options template carried its own
|
|
11
|
+
// literal copy of the standard options (cpp and scala as JSON string
|
|
12
|
+
// constants), and the feature half did not exist at all: the shipped spec was
|
|
13
|
+
//
|
|
14
|
+
// feature: { `$CHILD`: { `$OPEN`: true, active: false } }
|
|
15
|
+
//
|
|
16
|
+
// so a feature's options were never checked and a typo in one was silently
|
|
17
|
+
// ignored — `{ feature: { cache: { ttl: '5s' } } }` configured nothing and
|
|
18
|
+
// said nothing. Meanwhile the README's option tables came from a THIRD place
|
|
19
|
+
// (`target.options`, `feature.config.options`), which is how a documented
|
|
20
|
+
// option and a validated option could be different sets.
|
|
21
|
+
|
|
22
|
+
import { each } from 'jostraca'
|
|
23
|
+
import { KIT, getModelPath } from '@voxgig/apidef'
|
|
24
|
+
|
|
25
|
+
import { targetFeatures } from './applicability'
|
|
26
|
+
import { byExampleSpec, entitySpecs, optionalSpec, sentinel } from './canonSpec'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
const S_CHILD = sentinel('CHILD')
|
|
30
|
+
const S_OPEN = sentinel('OPEN')
|
|
31
|
+
const S_ONE = sentinel('ONE')
|
|
32
|
+
const S_NIL = sentinel('NIL')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
// A feature entry the caller may omit.
|
|
36
|
+
//
|
|
37
|
+
// WHY NOT A BARE MAP. A spec value is also the value validate INSERTS when
|
|
38
|
+
// the key is absent, so naming every feature with a bare map would put an
|
|
39
|
+
// entry for every feature the model declares into `options.feature` — and
|
|
40
|
+
// `makeOptions` derives the feature ADD ORDER from that map's keys. The
|
|
41
|
+
// `$ONE`/`$NIL` union keeps an absent feature absent and validates a supplied
|
|
42
|
+
// one, which is the behaviour `$CHILD` had and the behaviour callers have.
|
|
43
|
+
function optional(spec: any): any[] {
|
|
44
|
+
return [S_ONE, spec, S_NIL]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
// The spec for one feature's options.
|
|
49
|
+
//
|
|
50
|
+
// TWO DECLARATIONS, ONE SPEC:
|
|
51
|
+
// `config.options` — options WITH a default. Also the docs table.
|
|
52
|
+
// `config.optspec` — options with NO sensible default: callbacks (`sink`,
|
|
53
|
+
// `now`, `idgen`), injected objects (`logger`), values
|
|
54
|
+
// that only make sense when supplied. Declared as bare
|
|
55
|
+
// sentinels, so they type-check without inventing a
|
|
56
|
+
// default the feature would then have to ignore.
|
|
57
|
+
//
|
|
58
|
+
// OPEN BY DEFAULT. A feature may read an option neither list names — the
|
|
59
|
+
// shipped set still does, which is why this is open rather than closed — and
|
|
60
|
+
// rejecting those would break working clients on upgrade. `config.strict:
|
|
61
|
+
// true` closes one feature's spec once its options are fully declared.
|
|
62
|
+
function featureOptionSpec(feat: any): Record<string, any> {
|
|
63
|
+
const config = (feat && feat.config) || {}
|
|
64
|
+
const spec: Record<string, any> = {}
|
|
65
|
+
|
|
66
|
+
if (true !== config.strict) {
|
|
67
|
+
spec[S_OPEN] = true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Every feature takes `active`, whatever else it declares. Stated rather
|
|
71
|
+
// than assumed: a feature whose model omits `config.options` entirely
|
|
72
|
+
// (nothing stops one) must still validate the one option it certainly has.
|
|
73
|
+
spec.active = optionalSpec(byExampleSpec(false))
|
|
74
|
+
|
|
75
|
+
// EVERY FEATURE OPTION IS OPTIONAL, and that is not a policy choice here —
|
|
76
|
+
// it is what a caller does. `{ feature: { cost: { active: true, unit:
|
|
77
|
+
// 0.002 } } }` names two of cost's nine options and means the defaults for
|
|
78
|
+
// the other seven, which the feature's own code applies. A spec naming a
|
|
79
|
+
// key requires it, so without this widening cost's own corpus case failed
|
|
80
|
+
// its own feature's spec for the seven it did not mention.
|
|
81
|
+
//
|
|
82
|
+
// Widened to their KIND as well — see byExampleSpec.
|
|
83
|
+
for (const [name, val] of Object.entries(config.options || {})) {
|
|
84
|
+
spec[name] = optionalSpec(byExampleSpec(val))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Type-only declarations win: a name in both is one the author gave a
|
|
88
|
+
// default AND a type, and the type is the more specific statement. Written
|
|
89
|
+
// as sentinels already, so they are widened for absence but not for kind.
|
|
90
|
+
for (const [name, val] of Object.entries(config.optspec || {})) {
|
|
91
|
+
spec[name] = optionalSpec(val)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return spec
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
// The assembled option spec for a target.
|
|
99
|
+
//
|
|
100
|
+
// `targetname` gates the feature half exactly as `configDefinition` does: a
|
|
101
|
+
// target must not validate against — or document — a feature it has no
|
|
102
|
+
// implementation for. Without a name every active feature is included.
|
|
103
|
+
function optionSpec(model: any, targetname?: string): Record<string, any> {
|
|
104
|
+
// `only_active: false` IS THE WHOLE SPEC. getModelPath filters out any
|
|
105
|
+
// child whose `active` is false, and a spec is full of them: `test: {
|
|
106
|
+
// active: false }`, the `$CHILD` entity template, every feature default.
|
|
107
|
+
// Filtered, the spec silently lost its `test` entry and validate then
|
|
108
|
+
// rejected a perfectly ordinary `{ test: { active: true } }` with
|
|
109
|
+
// "Unexpected keys at field <root>: test". This is a SCHEMA, not a set of
|
|
110
|
+
// things to emit — `active: false` here is a default value, not a switch.
|
|
111
|
+
const base = getModelPath(model, `main.${KIT}.optspec`,
|
|
112
|
+
{ required: false, only_active: false }) || {}
|
|
113
|
+
|
|
114
|
+
// Cloned, because the model is shared across every target in one build and
|
|
115
|
+
// a spec assembled for `ts` must not be the object `js` then extends.
|
|
116
|
+
const spec: Record<string, any> = JSON.parse(JSON.stringify(base))
|
|
117
|
+
|
|
118
|
+
// The generic entry, kept from the shipped spec: a feature supplied at
|
|
119
|
+
// construction through `extend` (the station adopt path) is not in the
|
|
120
|
+
// model, so its options must still pass. Named entries below are checked
|
|
121
|
+
// against their own spec; anything else gets `active` checked and the rest
|
|
122
|
+
// waved through.
|
|
123
|
+
const feature: Record<string, any> = {
|
|
124
|
+
[S_CHILD]: {
|
|
125
|
+
[S_OPEN]: true,
|
|
126
|
+
active: false,
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const feats = targetFeatures(model, null == targetname ? undefined : targetname)
|
|
131
|
+
each(feats).forEach((f: any) => {
|
|
132
|
+
if (null == f || null == f.name || false === f.active) {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
feature[f.name] = optional(featureOptionSpec(f))
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
spec.feature = feature
|
|
139
|
+
|
|
140
|
+
return spec
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
// The per-entity data/request specs a target needs, or null when it needs
|
|
145
|
+
// none.
|
|
146
|
+
//
|
|
147
|
+
// GATED ON THE FEATURE, and that gate is the whole reason this returns null
|
|
148
|
+
// rather than an empty map. The specs are derived data: every one of them can
|
|
149
|
+
// be rebuilt from `fields[].type`, which the generated config already
|
|
150
|
+
// carries. Emitting them unconditionally would grow every existing project's
|
|
151
|
+
// generated source — and, above the config size threshold, flip targets from
|
|
152
|
+
// the literal representation to the parsed-JSON one — to carry a spec nothing
|
|
153
|
+
// reads. A project that turns `validate` on pays for it; nobody else does.
|
|
154
|
+
function entitySpecMap(model: any, targetname?: string): Record<string, any> | null {
|
|
155
|
+
const feats = targetFeatures(model, null == targetname ? undefined : targetname)
|
|
156
|
+
const validate = (feats as any).validate
|
|
157
|
+
|
|
158
|
+
if (null == validate || false === validate.active) {
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const entity = getModelPath(model, `main.${KIT}.entity`, { required: false }) || {}
|
|
163
|
+
|
|
164
|
+
const specs: Record<string, any> = {}
|
|
165
|
+
each(entity).forEach((ent: any) => {
|
|
166
|
+
if (null == ent || null == ent.name) {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
specs[ent.name] = entitySpecs(ent)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
return specs
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
export {
|
|
177
|
+
optionSpec,
|
|
178
|
+
featureOptionSpec,
|
|
179
|
+
entitySpecMap,
|
|
180
|
+
}
|
package/src/sdkgen.ts
CHANGED
|
@@ -61,6 +61,8 @@ import { collectDeps } from './helpers/collectDeps'
|
|
|
61
61
|
import { guardModelNames } from './helpers/modelNames'
|
|
62
62
|
import type { DepEntry } from './helpers/collectDeps'
|
|
63
63
|
import { canonToType, canonToDtype, canonKey, canonScalarKey } from './helpers/canonType'
|
|
64
|
+
import { canonToSpec, entityDataSpec, entityOpSpec, entitySpecs } from './helpers/canonSpec'
|
|
65
|
+
import { optionSpec, featureOptionSpec, entitySpecMap } from './helpers/optspec'
|
|
64
66
|
import { OP_SUFFIX, opTypeName, opParams, ownPoint, opActions, entityActions, entityPath, opRequestShape, entityIdField, entityDataIdField, entityOps, entityPrimaryOp, pickExampleEntity, entityClassName, entityTypeCollisions, warnEntityTypeCollisions, deriveEntityNames, entityCollection } from './helpers/opShape'
|
|
65
67
|
import { isReservedName, safeVarName, exampleVarName, phpEntityAccessor, entityCacheField, isRbCoreConstant, isRbSdkConstant, rbSafeTypeName, isSwiftSdkType, swiftSafeTypeName, isPhpReservedType, isPhpSdkClass, phpSafeTypeName, isTsReservedType, tsSafeTypeName, jsProp, jsOptProp, jsKey, luaKey, prefixLeadingDigit } from './helpers/naming'
|
|
66
68
|
import { serverVariables, hasServerVariables, serverVarEnv } from './helpers/serverVars'
|
|
@@ -1237,6 +1239,13 @@ export {
|
|
|
1237
1239
|
pointTerminalParam,
|
|
1238
1240
|
pointPathKey,
|
|
1239
1241
|
canonToType,
|
|
1242
|
+
canonToSpec,
|
|
1243
|
+
entityDataSpec,
|
|
1244
|
+
entityOpSpec,
|
|
1245
|
+
entitySpecs,
|
|
1246
|
+
optionSpec,
|
|
1247
|
+
featureOptionSpec,
|
|
1248
|
+
entitySpecMap,
|
|
1240
1249
|
canonToDtype,
|
|
1241
1250
|
canonKey,
|
|
1242
1251
|
canonScalarKey,
|