@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.
Files changed (60) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/AgentGuideContent.js +30 -0
  3. package/dist/cmp/AgentGuideContent.js.map +1 -1
  4. package/dist/cmp/FeatureDocs.d.ts +6 -1
  5. package/dist/cmp/FeatureDocs.js +27 -0
  6. package/dist/cmp/FeatureDocs.js.map +1 -1
  7. package/dist/cmp/ReadmeRefFeatures.js +23 -13
  8. package/dist/cmp/ReadmeRefFeatures.js.map +1 -1
  9. package/dist/helpers/applicability.js +7 -0
  10. package/dist/helpers/applicability.js.map +1 -1
  11. package/dist/helpers/canonSpec.d.ts +12 -0
  12. package/dist/helpers/canonSpec.js +259 -0
  13. package/dist/helpers/canonSpec.js.map +1 -0
  14. package/dist/helpers/optspec.d.ts +4 -0
  15. package/dist/helpers/optspec.js +152 -0
  16. package/dist/helpers/optspec.js.map +1 -0
  17. package/dist/sdkgen.d.ts +3 -1
  18. package/dist/sdkgen.js +12 -3
  19. package/dist/sdkgen.js.map +1 -1
  20. package/dist/tsconfig.tsbuildinfo +1 -1
  21. package/model/sdkgen.aon +125 -0
  22. package/package.json +1 -1
  23. package/project/.sdk/model/feature/audit.aon +15 -0
  24. package/project/.sdk/model/feature/cache.aon +13 -0
  25. package/project/.sdk/model/feature/clienttrack.aon +21 -0
  26. package/project/.sdk/model/feature/cost.aon +15 -0
  27. package/project/.sdk/model/feature/debug.aon +15 -0
  28. package/project/.sdk/model/feature/feature-index.aon +1 -0
  29. package/project/.sdk/model/feature/idempotency.aon +13 -0
  30. package/project/.sdk/model/feature/log.aon +13 -0
  31. package/project/.sdk/model/feature/metrics.aon +13 -0
  32. package/project/.sdk/model/feature/netsim.aon +14 -0
  33. package/project/.sdk/model/feature/paging.aon +15 -0
  34. package/project/.sdk/model/feature/proxy.aon +13 -0
  35. package/project/.sdk/model/feature/ratelimit.aon +15 -0
  36. package/project/.sdk/model/feature/retry.aon +15 -0
  37. package/project/.sdk/model/feature/streaming.aon +15 -0
  38. package/project/.sdk/model/feature/telemetry.aon +19 -0
  39. package/project/.sdk/model/feature/test.aon +13 -0
  40. package/project/.sdk/model/feature/timeout.aon +15 -0
  41. package/project/.sdk/model/feature/validate.aon +73 -0
  42. package/project/.sdk/model/target/js.aon +1 -1
  43. package/project/.sdk/model/target/ts.aon +1 -1
  44. package/project/.sdk/src/cmp/js/Main_js.ts +2 -0
  45. package/project/.sdk/src/cmp/js/Schema_js.ts +66 -0
  46. package/project/.sdk/src/cmp/scala/Main_scala.ts +30 -0
  47. package/project/.sdk/src/cmp/ts/Main_ts.ts +2 -0
  48. package/project/.sdk/src/cmp/ts/Schema_ts.ts +66 -0
  49. package/project/.sdk/tm/js/src/feature/validate/ValidateFeature.js +296 -0
  50. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +26 -61
  51. package/project/.sdk/tm/ts/src/feature/validate/ValidateFeature.ts +298 -0
  52. package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +24 -61
  53. package/project/sdkgen-package.json +3 -2
  54. package/src/cmp/AgentGuideContent.ts +30 -0
  55. package/src/cmp/FeatureDocs.ts +39 -0
  56. package/src/cmp/ReadmeRefFeatures.ts +24 -13
  57. package/src/helpers/applicability.ts +8 -0
  58. package/src/helpers/canonSpec.ts +301 -0
  59. package/src/helpers/optspec.ts +180 -0
  60. 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,