@voxgig/apidef 8.11.0 → 8.12.1

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 (79) hide show
  1. package/dist/apidef.d.ts +4 -1
  2. package/dist/apidef.js +12 -34
  3. package/dist/apidef.js.map +1 -1
  4. package/dist/builder/entity/entity.js +0 -33
  5. package/dist/builder/entity/entity.js.map +1 -1
  6. package/dist/builder/entity/info.js +0 -4
  7. package/dist/builder/entity/info.js.map +1 -1
  8. package/dist/builder/flow.d.ts +4 -1
  9. package/dist/builder/flow.js +36 -27
  10. package/dist/builder/flow.js.map +1 -1
  11. package/dist/guide/graphql01.js +0 -55
  12. package/dist/guide/graphql01.js.map +1 -1
  13. package/dist/guide/guide.js +3 -126
  14. package/dist/guide/guide.js.map +1 -1
  15. package/dist/guide/heuristic01.js +40 -181
  16. package/dist/guide/heuristic01.js.map +1 -1
  17. package/dist/parse/graphql.js +0 -25
  18. package/dist/parse/graphql.js.map +1 -1
  19. package/dist/parse.js +51 -56
  20. package/dist/parse.js.map +1 -1
  21. package/dist/resolved.d.ts +23 -0
  22. package/dist/resolved.js +92 -0
  23. package/dist/resolved.js.map +1 -0
  24. package/dist/resolver.js +0 -2
  25. package/dist/resolver.js.map +1 -1
  26. package/dist/transform/args.js +2 -25
  27. package/dist/transform/args.js.map +1 -1
  28. package/dist/transform/casecollide.d.ts +3 -0
  29. package/dist/transform/casecollide.js +67 -0
  30. package/dist/transform/casecollide.js.map +1 -0
  31. package/dist/transform/contract.js +43 -38
  32. package/dist/transform/contract.js.map +1 -1
  33. package/dist/transform/entity.js +0 -56
  34. package/dist/transform/entity.js.map +1 -1
  35. package/dist/transform/field.js +1 -368
  36. package/dist/transform/field.js.map +1 -1
  37. package/dist/transform/flowstep.js +0 -49
  38. package/dist/transform/flowstep.js.map +1 -1
  39. package/dist/transform/graphql.js +0 -36
  40. package/dist/transform/graphql.js.map +1 -1
  41. package/dist/transform/operation.js +0 -23
  42. package/dist/transform/operation.js.map +1 -1
  43. package/dist/transform/top.js +1 -128
  44. package/dist/transform/top.js.map +1 -1
  45. package/dist/transform.js.map +1 -1
  46. package/dist/tsconfig.tsbuildinfo +1 -1
  47. package/dist/types.d.ts +2 -0
  48. package/dist/types.js.map +1 -1
  49. package/dist/utility.d.ts +2 -1
  50. package/dist/utility.js +17 -316
  51. package/dist/utility.js.map +1 -1
  52. package/model/apidef.aon +20 -113
  53. package/model/guide.aon +1 -32
  54. package/package.json +5 -5
  55. package/src/apidef.ts +22 -33
  56. package/src/builder/entity/entity.ts +0 -33
  57. package/src/builder/entity/info.ts +0 -4
  58. package/src/builder/flow.ts +39 -24
  59. package/src/desc.ts +0 -22
  60. package/src/guide/graphql01.ts +0 -75
  61. package/src/guide/guide.ts +5 -127
  62. package/src/guide/heuristic01.ts +47 -182
  63. package/src/model.ts +6 -85
  64. package/src/parse/graphql.ts +0 -25
  65. package/src/parse.ts +61 -56
  66. package/src/resolved.ts +136 -0
  67. package/src/resolver.ts +0 -2
  68. package/src/transform/args.ts +2 -25
  69. package/src/transform/casecollide.ts +80 -0
  70. package/src/transform/contract.ts +44 -34
  71. package/src/transform/entity.ts +0 -56
  72. package/src/transform/field.ts +2 -368
  73. package/src/transform/flowstep.ts +0 -49
  74. package/src/transform/graphql.ts +0 -36
  75. package/src/transform/operation.ts +0 -23
  76. package/src/transform/top.ts +1 -131
  77. package/src/transform.ts +0 -1
  78. package/src/types.ts +4 -13
  79. package/src/utility.ts +18 -314
package/src/utility.ts CHANGED
@@ -110,35 +110,6 @@ function formatJsonSrc(jsonsrc: string) {
110
110
  }
111
111
 
112
112
 
113
- // Common irregular plurals, in the form plural → singular. Used at the
114
- // head of depluralize() to short-circuit cases the suffix rules below
115
- // would otherwise mishandle.
116
- //
117
- // Three over-strip classes are worked around here because the surface
118
- // form gives no clean discriminator:
119
- //
120
- // * `-Vse+s` plurals (houses, phases, noses, …) would hit the
121
- // generic `-ses → ∅` rule and become hous/phas/nos. Every such
122
- // -se+s plural needs an explicit entry.
123
- //
124
- // * `-che+s` plurals (caches, niches, headaches, …) would hit the
125
- // generic `-ches → ∅` rule and become cach/nich/headach. Same
126
- // pattern: no letter-doubling tell exists (cache vs church both
127
- // have a single 'ch'), so each -che singular is enumerated.
128
- //
129
- // * `-oe+s` plurals (shoes, canoes, oboes) would hit the generic
130
- // `-oes → -o` rule (for potatoes/heroes) and become sho/cano/obo.
131
- // Only collision-safe entries are listed: a key must not also be a
132
- // suffix of a real `-o`+es plural (e.g. `toes` is excluded because
133
- // it would turn tomatoes → tomatoe).
134
- //
135
- // Keys are lowercase; depluralize() does a case-insensitive lookup
136
- // and reapplies the caller's casing on the way out.
137
- //
138
- // Null-prototype: these tables are indexed by spec-derived names, so a
139
- // schema or path segment called `constructor` / `__proto__` / `toString`
140
- // would otherwise resolve to the inherited Object member and be returned
141
- // as a "match" — crashing matchCase() on a function. See NULL_PROTO_NOTE.
142
113
  const IRREGULARS: Record<string, string> = Object.assign(Object.create(null), {
143
114
  'analytics': 'analytics',
144
115
  'analyses': 'analysis',
@@ -205,16 +176,6 @@ const IRREGULARS: Record<string, string> = Object.assign(Object.create(null), {
205
176
  'yes': 'yes',
206
177
  })
207
178
 
208
- // NULL_PROTO_NOTE: every plain-object lookup table in this module whose keys
209
- // come from an API spec is built with a null prototype
210
- // (`Object.assign(Object.create(null), {...})`). Without it, `TABLE[name]`
211
- // inherits from Object.prototype, so `TABLE['constructor']` yields the Object
212
- // constructor (truthy, a function) and `TABLE['__proto__']` yields
213
- // Object.prototype (truthy, an object). Both then flow into code expecting a
214
- // string. A spec with `components.schemas.Constructor` is enough to reach
215
- // this: canonize -> depluralize -> matchCase -> `.toLowerCase is not a
216
- // function`, failing the whole build at the guide stage. Keep new tables
217
- // null-prototype, or use a Map (CANONIZE_CACHE already does).
218
179
 
219
180
  // Sorted longest-first so the most specific IRREGULARS suffix wins.
220
181
  // Without this, 'women' would be shadowed by 'men' (3 < 5) under
@@ -223,11 +184,6 @@ const IRREGULARS: Record<string, string> = Object.assign(Object.create(null), {
223
184
  const IRREGULAR_KEYS = Object.keys(IRREGULARS).sort((a, b) => b.length - a.length)
224
185
 
225
186
 
226
- // Reapply the case pattern of `source` to `target`. Used so the
227
- // case-insensitive lookups in depluralize() preserve the caller's
228
- // casing on the way out (HOUSES → HOUSE, Houses → House, houses →
229
- // house). Falls through to `target` unchanged for mixed-case sources
230
- // that don't fit one of the three canonical patterns.
231
187
  function matchCase(source: string, target: string): string {
232
188
  if (source === source.toLowerCase()) return target.toLowerCase()
233
189
  if (source === source.toUpperCase()) return target.toUpperCase()
@@ -238,20 +194,6 @@ function matchCase(source: string, target: string): string {
238
194
  }
239
195
 
240
196
 
241
- // Per-model plural overrides, populated from the model's
242
- // `main.custom.plurals` section at apidef pipeline entry and cleared
243
- // between runs. Checked by depluralize() before the built-in
244
- // IRREGULARS table and rule chain — so a model can override any
245
- // default depluralization, including correct-by-default cases, when
246
- // its domain demands a different singular (e.g. fitness API with
247
- // {axes: axe}, photography app with {lenses: lens}).
248
- //
249
- // Module-level rather than a parameter so the many existing
250
- // depluralize/canonize call sites across transforms and guide
251
- // inherit the override without signature churn. apidef is
252
- // single-model-per-process; if that ever changes, switch this to a
253
- // per-context map.
254
- // Null-prototype: see NULL_PROTO_NOTE above.
255
197
  let CUSTOM_PLURALS: Record<string, string> = Object.create(null)
256
198
  let CUSTOM_PLURAL_KEYS: string[] = []
257
199
 
@@ -323,7 +265,6 @@ function depluralize(word: string): string {
323
265
  // rules add a letter, so they need to match the case of the dropped
324
266
  // suffix; all other rules just slice and inherit the caller's case.
325
267
 
326
- // -ies -> -y (cities -> city), but only if result is > 2 chars
327
268
  if (lower.endsWith('ies') && word.length > 3) {
328
269
  const dropped = word.slice(-3)
329
270
  const y = dropped === dropped.toUpperCase() ? 'Y' : 'y'
@@ -356,12 +297,6 @@ function depluralize(word: string): string {
356
297
  return word.slice(0, -1)
357
298
  }
358
299
 
359
- // -zes plurals come from -ze singulars (prize, size, freeze, maze,
360
- // breeze, …) far more often than from a bare -z taking -es. The only
361
- // -zes plurals that strip the full -es have a doubled-z stem
362
- // (buzz/buzzes, fez/fezzes). Discriminate on -zzes so prizes → prize
363
- // instead of priz. Mirrors the -ses/-Vse+s problem the IRREGULARS
364
- // table works around for the -se case.
365
300
  if (lower.endsWith('zzes')) {
366
301
  return word.slice(0, -2)
367
302
  }
@@ -375,7 +310,6 @@ function depluralize(word: string): string {
375
310
  return word.slice(0, -2)
376
311
  }
377
312
 
378
- // -s -> remove -s (cats -> cat), but only if result is > 2 chars
379
313
  if (lower.endsWith('s') &&
380
314
  !lower.endsWith('ss') &&
381
315
  !lower.endsWith('us') &&
@@ -391,8 +325,11 @@ function depluralize(word: string): string {
391
325
 
392
326
  function find(obj: any, qkey: string): any[] {
393
327
  const vals: any[] = []
328
+ const seen = new WeakSet<object>()
394
329
  const collect = (o: any): void => {
395
330
  if (!o || 'object' !== typeof o) return
331
+ if (seen.has(o)) return
332
+ seen.add(o)
396
333
  if (Array.isArray(o)) {
397
334
  for (let i = 0; i < o.length; i++) collect(o[i])
398
335
  } else {
@@ -486,7 +423,6 @@ function $APPEND(inj: Injection, val: any, ref: any, store: any) {
486
423
  const vstore = { ...store }
487
424
  vstore.$TOP = { [key]: dval }
488
425
 
489
- // const ptval = transform({ [key]: dval }, { [key]: xform }, {
490
426
  const ptval = inject({ [key]: xform }, vstore, {
491
427
  meta: { ...inj.meta },
492
428
  errs: inj.errs,
@@ -542,7 +478,6 @@ function $SELECT(inj: Injection, _val: any, _ref: any, store: any) {
542
478
  }
543
479
  }
544
480
 
545
- // TODO: select should be safe for scalars
546
481
  const children = select(dparents, selector)
547
482
 
548
483
  if (0 < children.length) {
@@ -554,8 +489,6 @@ function $SELECT(inj: Injection, _val: any, _ref: any, store: any) {
554
489
  meta: merge([
555
490
  inj.meta,
556
491
 
557
- // TODO: need this hack as struct does not provide a way to get grandparent keys
558
- // also, these capture actions are not preserving the path!
559
492
  { select: { key: { [slice(inj.path, 1, -1).join('+')]: child.$KEY } } }
560
493
  ]),
561
494
  errs: inj.errs,
@@ -574,8 +507,6 @@ function $RECASE(inj: Injection, val: any, ref: any, store: any) {
574
507
  ) {
575
508
  const dval = inj.parent[inj.key]
576
509
 
577
- // TODO: handle paths more generally! use inj.prior?
578
- // TODO: mkae this into a utility method on inj?
579
510
  const dkey = inj.prior.key
580
511
  const gkey = inj.prior.prior.key
581
512
 
@@ -603,13 +534,6 @@ function $RECASE(inj: Injection, val: any, ref: any, store: any) {
603
534
 
604
535
  type PathMatch = (string[] & { index: number, expr: string, path: string })
605
536
 
606
- // A special-purpose regex-style matcher for url paths.
607
- // t - text part
608
- // p - param part
609
- // / - part separator
610
- // / at start - must match from start
611
- // / at end - must match to end
612
- // See utility.test.ts for examples
613
537
  function pathMatch(path: string | string[], expr: string):
614
538
  null | PathMatch {
615
539
 
@@ -744,9 +668,6 @@ function formatJSONIC(
744
668
  '`' + JSON.stringify(v)
745
669
  .substring(1)
746
670
  .replace(/\\n/g, '\n')
747
- // Inside a JSONIC backtick literal a double quote is a literal
748
- // character, so unescape JSON's \" back to " (was previously
749
- // replaced with ':' which silently corrupted quoted text).
750
671
  .replace(/\\"/g, '"')
751
672
  .replace(/`/g, '\\`')
752
673
  .replace(/"$/, '`'))
@@ -783,11 +704,6 @@ function renderJSONIC(
783
704
  c: (color: any, text: string) => string,
784
705
  renderPrimitive: (v: any) => string,
785
706
  renderComment: (c: any) => string | null,
786
- // True once the value has already been through decircular. `seen` never
787
- // forgets a node, so a merely REPEATED reference (a shared node in a DAG,
788
- // not a cycle) also lands in the fallback below. If decircular leaves such
789
- // a repeat in place, retrying forever overflows the stack — which is what
790
- // formatting a deep validation error used to do. Retry at most once.
791
707
  decircularized: boolean,
792
708
  ): string {
793
709
 
@@ -954,16 +870,6 @@ function renderJSONIC(
954
870
  }
955
871
 
956
872
 
957
- // Canonical type-sentinel vocabulary. VALID_CANON maps an OpenAPI type NAME
958
- // to its `$SENTINEL` form; CANON_ONE is the union sentinel produced by
959
- // `validator` for a multi-type (`['`$ONE`', [member, ...]]`). Both are part
960
- // of the public API so downstream consumers (e.g. @voxgig/sdkgen's
961
- // sentinel -> language-type table) can verify they cover the full set
962
- // instead of hand-syncing against this file.
963
- //
964
- // Null-prototype (see NULL_PROTO_NOTE): `type` values come from the spec, so
965
- // a schema declaring `type: constructor` would otherwise return the Object
966
- // constructor here rather than falling through to the 'Any' default.
967
873
  const VALID_CANON: Record<string, string> = Object.assign(Object.create(null), {
968
874
  'string': '`$STRING`',
969
875
  'number': '`$NUMBER`',
@@ -1001,62 +907,26 @@ function transliterate(s: string): string {
1001
907
 
1002
908
  const CANONIZE_CACHE = new Map<string, string>()
1003
909
 
910
+ // A trailing plural after an acronym is part of the word, and snakify would
911
+ // split it letter by letter. See docs/design/derived-names.md
912
+ const ACRONYM_PLURAL_RE = /([A-Z]{2,})s(?![a-zA-Z])/g
913
+
1004
914
  function canonize(s: string) {
1005
915
  if (null == s || '' === s) return ''
1006
916
  const cached = CANONIZE_CACHE.get(s)
1007
917
  if (undefined !== cached) return cached
1008
- const out = depluralize(snakify(transliterate(s).replace(FILE_EXT_RE, '')))
918
+ const deacronymed = transliterate(s)
919
+ .replace(FILE_EXT_RE, '')
920
+ .replace(ACRONYM_PLURAL_RE, (_m, run) => run[0] + run.slice(1).toLowerCase() + 's')
921
+ const out = depluralize(snakify(deacronymed))
1009
922
  .replace(/[^a-zA-Z_0-9]/g, '')
1010
923
  CANONIZE_CACHE.set(s, out)
1011
924
  return out
1012
925
  }
1013
926
 
1014
927
 
1015
- // Canonicalise a FIELD name — which is a WIRE identifier, not a type name.
1016
- //
1017
- // `canonize` is right for entity/type names: it snakifies and depluralizes so
1018
- // `Users` and `user-items` converge on `user` / `user_item`. Applied to a
1019
- // field it is actively WRONG, because the name has to match the JSON the
1020
- // server actually sends:
1021
- //
1022
- // modelType -> canonize -> model_type (server sends modelType)
1023
- // items -> canonize -> item (server sends items)
1024
- //
1025
- // Nothing maps back: the model's `alias.field` map is emitted empty and no
1026
- // generator consumes it, so the wire name is simply lost. Across the fleet's
1027
- // specs that renamed 23% of all fields (146 repos) and depluralized another
1028
- // 13% — every one of those SDKs reading a key the server never sends.
1029
- //
1030
- // So: keep the transliteration and identifier sanitisation that stop a name
1031
- // being unusable in a target language, and drop the snakify/depluralize that
1032
- // change what the name MEANS. Case and plurality are preserved verbatim.
1033
928
  const CANONIZE_FIELD_CACHE = new Map<string, string>()
1034
929
 
1035
- // Re-split a boundary-less entity name using the component name's boundaries.
1036
- //
1037
- // ONLY when the name has no boundaries of its own. A name that already
1038
- // contains `_` was split by something that knew where the words were, and is
1039
- // left exactly as it is.
1040
- //
1041
- // ONLY on an EXACT concatenation match, which is what stops this inventing
1042
- // anything. The component's segments are accumulated left to right and the
1043
- // result is used only if it equals the path-derived name with its separators
1044
- // removed:
1045
- //
1046
- // payeeverification + payee_verification_result
1047
- // payee -> "payee" no
1048
- // payee_verification -> "payeeverification" YES -> payee_verification
1049
- //
1050
- // virtual + payment_account
1051
- // payment -> "payment" no
1052
- // payment_account -> "paymentaccount" no -> unchanged
1053
- //
1054
- // So a component that has nothing to do with the path token cannot rename it,
1055
- // and a component that merely EXTENDS the token (`payment_account_entry` for
1056
- // `payment`) cannot either -- only the prefix that reconstructs the token
1057
- // exactly is taken. A different entity name is never reachable from here; the
1058
- // same letters in the same order, with separators restored, is the whole of
1059
- // what this can produce.
1060
930
  function resplitFromCmp(entname: string, cmp: string, why: string[]): string {
1061
931
  if (null == entname || '' === entname || entname.includes('_')) {
1062
932
  return entname
@@ -1097,12 +967,6 @@ function canonizeField(s: string) {
1097
967
  }
1098
968
 
1099
969
 
1100
- // Namespace-qualified schema names (ASP.NET / Java style:
1101
- // "NoFrixion.MoneyMoov.Models.PaymentRequests.MerchantPayment",
1102
- // "com.example.api.Payment") describe the type by their LAST dotted
1103
- // segment; the namespace prefix is packaging noise. Reduce to the last
1104
- // meaningful segment — skipping version-ish ("v2", "10") or too-short
1105
- // tails — so entity names derive from the type, not the namespace.
1106
970
  function stripSchemaNamespace(name: string): string {
1107
971
  if (null == name || !name.includes('.')) return name
1108
972
  const segs = name.split('.')
@@ -1128,24 +992,6 @@ function canonizeCmpName(orig: string): string {
1128
992
  const FIRST_LETTER_RE = /[a-zA-Z]/
1129
993
 
1130
994
 
1131
- // No target language permits an identifier that starts with a digit, so a
1132
- // name derived from one — a `3dsSession` schema, a `/2fa` path segment, a
1133
- // `_3DSecure` GraphQL type — is prefixed with an `n`.
1134
- //
1135
- // The prefix takes the case of the name it guards: lower for `3ds_session`,
1136
- // upper for `3DSecure`. That keeps the result inside whatever casing
1137
- // convention the caller was already working in, so a later PascalCase or
1138
- // camelCase conversion has nothing to undo. A name with no letter in it at
1139
- // all (`404`) takes the lower-case prefix.
1140
- //
1141
- // This is the ONE place the rule lives. Entity names reach it through
1142
- // `ensureMinEntityName` and the GraphQL guide's `entityName`; project slugs
1143
- // through `sanitizeSlug`.
1144
- //
1145
- // FIELD names deliberately do NOT come here. A field name is a WIRE
1146
- // identifier and renaming it makes the SDK read a key the server never sends
1147
- // — the mistake `canonizeField` exists to document. Targets escape those at
1148
- // the point of emission instead.
1149
995
  function prefixLeadingDigit(s: string): string {
1150
996
  if (null == s || '' === s) return s
1151
997
  const first = s.charCodeAt(0)
@@ -1260,13 +1106,6 @@ function ensureMinEntityName(
1260
1106
  }
1261
1107
 
1262
1108
  if (padded !== name && null != existing[padded]) {
1263
- // The name was modified (truncated/sanitized) and collides with an
1264
- // existing entity. Only a collision between DIFFERENT origins needs a
1265
- // numeric suffix — the same original name re-encountered (e.g. the same
1266
- // long schema referenced by several methods on one path) must reuse the
1267
- // existing entity so its ops merge instead of minting phantom
1268
- // "<entity>2/3/4" entities. Entities record their pre-truncation name
1269
- // as `longname`; entries without one keep the old always-suffix rule.
1270
1109
  if (existing[padded].longname === name) {
1271
1110
  return padded
1272
1111
  }
@@ -1287,24 +1126,8 @@ function ensureMinEntityName(
1287
1126
  // Unconditional suffixes: framework noise, always stripped.
1288
1127
  const CMP_SUFFIXES = ['_rest_controller', '_controller', '_response', '_request']
1289
1128
 
1290
- // Guarded suffixes: pagination wrappers ('_page_response', '_page') and
1291
- // op-reply wrappers ('_create_response', '_update_response') fold wrapper
1292
- // schemas (BeneficiaryPageResponse, MerchantTokenPage,
1293
- // BeneficiariesCreateResponse, ...) into their base entity — but ONLY when
1294
- // the remainder is itself a known component schema (the wrapper
1295
- // convention). Without that guard a real noun gets mangled: an API whose
1296
- // resource IS a page (LandingPage entity at /landing-pages) must keep
1297
- // 'landing_page', not become 'landing'. Order matters: longer first, since
1298
- // '_page_response'/'_create_response' also end with '_response'. Bare
1299
- // '_create'/'_update' are never stripped: too likely part of a real noun.
1300
1129
  const CMP_GUARDED_SUFFIXES = ['_create_response', '_update_response', '_page_response', '_page']
1301
1130
 
1302
- // Op-reply wrappers of the form <namespace><Verb>Result (incident.io:
1303
- // AlertsResolveResultV2, SeveritiesListResultV1). Same idea as
1304
- // '_create_response' above, but the verb varies and sits before the wrapper
1305
- // word rather than being part of a fixed pair, so the shape is matched rather
1306
- // than enumerated. Guarded exactly like the others: nothing is stripped
1307
- // unless the remainder is a real schema.
1308
1131
  const CMP_RESULT_VERBS = [
1309
1132
  'list', 'create', 'show', 'update', 'delete', 'remove',
1310
1133
  'get', 'edit', 'resolve', 'rotate', 'search',
@@ -1322,26 +1145,6 @@ function guideActive(node: any): boolean {
1322
1145
  }
1323
1146
 
1324
1147
 
1325
- // An API's ACCESS-TOKEN EXCHANGE is not a resource, and must not become an
1326
- // entity. The shape apidef looks for is the one every such endpoint has:
1327
- //
1328
- // 1. The spec as a whole is SECURED (a top-level `security` requirement).
1329
- // Without that, a per-operation `security: []` clears nothing and
1330
- // carries no signal at all.
1331
- // 2. The operation clears that requirement with its own `security: []` —
1332
- // it is the one call a client can make before it holds a credential,
1333
- // because it is what issues them.
1334
- // 3. It is a POST. A credential exchange writes; a GET that happens to
1335
- // return a field called `token` is far likelier to be a resource.
1336
- // 4. Its success response carries a TOKEN-shaped field.
1337
- //
1338
- // All four together, or it is a resource. There is deliberately no vendor
1339
- // extension and no overlay to say otherwise (ADR-002): a spec apidef does
1340
- // not control cannot be annotated anyway, and a heuristic that can be
1341
- // corrected in guide.aon needs no second correction surface.
1342
- //
1343
- // Returns the field names the exchange uses, which is what sdkgen's
1344
- // `secrets` feature needs to drive it, or null when this is a resource.
1345
1148
  const AUTH_TOKEN_FIELDS = [
1346
1149
  'access_token', 'accessToken', 'access-token',
1347
1150
  'id_token', 'idToken',
@@ -1367,8 +1170,6 @@ function authExchangeOp(
1367
1170
  return null
1368
1171
  }
1369
1172
 
1370
- // An empty ARRAY, specifically. `security` absent means "inherit the
1371
- // global requirement"; `security: []` means "no credential needed here".
1372
1173
  if (!Array.isArray(op?.security) || 0 !== op.security.length) {
1373
1174
  return null
1374
1175
  }
@@ -1426,9 +1227,6 @@ function schemaProps(schema: any): string[] {
1426
1227
  }
1427
1228
 
1428
1229
 
1429
- // First name in `names` that the schema declares, compared case-insensitively
1430
- // so `Access_Token` matches `access_token`. Ordered by the CANDIDATE list, not
1431
- // by declaration order, so `access_token` wins over a sibling `token`.
1432
1230
  function firstFieldMatch(props: string[], names: string[]): string | null {
1433
1231
  const lower = new Map<string, string>()
1434
1232
  for (const p of props) {
@@ -1556,6 +1354,12 @@ function warnOnError(where: string, warn: Warner, fn: Function, result?: any) {
1556
1354
 
1557
1355
 
1558
1356
 
1357
+ function debugpathOn(): boolean {
1358
+ const apipath = process.env.APIDEF_DEBUG_PATH
1359
+ return null != apipath && '' !== apipath
1360
+ }
1361
+
1362
+
1559
1363
  function debugpath(pathStr: string, methodName: string | null | undefined, ...args: any[]): void {
1560
1364
  const apipath = process.env.APIDEF_DEBUG_PATH
1561
1365
 
@@ -1567,7 +1371,6 @@ function debugpath(pathStr: string, methodName: string | null | undefined, ...ar
1567
1371
  // Check if path matches
1568
1372
  if (pathStr !== targetPath) return
1569
1373
 
1570
- // If a method is specified in apipath and we have a method name, check if it matches
1571
1374
  if (targetMethod && methodName) {
1572
1375
  if (methodName.toLowerCase() !== targetMethod.toLowerCase()) return
1573
1376
  }
@@ -1608,7 +1411,6 @@ function findPathsWithPrefix(
1608
1411
  }
1609
1412
 
1610
1413
 
1611
- // TODO: move to jostraca?
1612
1414
  function allcapify(s?: string) {
1613
1415
  return 'string' === typeof s ? snakify(s).toUpperCase() : ''
1614
1416
  }
@@ -1764,48 +1566,12 @@ function isEntityWrapperProp(propSchema: any): boolean {
1764
1566
  }
1765
1567
 
1766
1568
 
1767
- // A response body that is nothing but a single wrapper property is an
1768
- // ENVELOPE around the result: `{item: {...}}`, `{data: {...}}`,
1769
- // `{items: [...]}`, `{results: [...]}`. Return that property's name so the
1770
- // caller can unwrap to it, or null when the body is the result itself.
1771
- //
1772
- // Two conditions keep this from firing on a response that IS the entity:
1773
- //
1774
- // 1. EXACTLY ONE property. A body with siblings is a structure in its own
1775
- // right, not a wrapper — `{ok, id}` from a delete, or any paged
1776
- // `{results, next}`, must be handed over whole.
1777
- // 2. The property's SHAPE matches the operation's cardinality. A `list`
1778
- // unwraps only to an array, every other op only to a non-array. So a
1779
- // single-entity op facing `{items: [...]}` is left alone rather than
1780
- // silently yielding a list, and vice versa.
1781
- //
1782
- // A one-field entity whose sole field is itself structured can still be
1783
- // unwrapped wrongly; that is the residual cost of the spec not saying which
1784
- // it means. Naming the wrapper after the entity remains the unambiguous
1785
- // signal, and is still checked first.
1786
1569
  function envelopeProp(resprops: any, opname: string): string | null {
1787
1570
  const keys = keysof(resprops)
1788
1571
  if (0 === keys.length) {
1789
1572
  return null
1790
1573
  }
1791
1574
 
1792
- // Exactly one STRUCTURED property, with any siblings being scalars.
1793
- //
1794
- // The original rule demanded exactly one property full stop, which missed
1795
- // the single most common envelope shape in the wild:
1796
- //
1797
- // { "success": true, "data": [ ... ] }
1798
- // { "status": "ok", "result": { ... } }
1799
- //
1800
- // A boolean/string status flag beside the payload is metadata, not a
1801
- // sibling of equal standing, so the body is still an envelope. UniVec's
1802
- // /v1/models returns exactly this and every generated SDK — TypeScript, Go,
1803
- // Python alike — returned an empty list against an API plainly serving
1804
- // data.
1805
- //
1806
- // Scalar-only siblings keep the guard meaningful: `{ok, id}` from a delete
1807
- // has no structured member and is still handed over whole, and a body with
1808
- // TWO structured members is a composite we must not guess at.
1809
1575
  const structured = keys.filter((k: string) => isEntityWrapperProp(resprops[k]))
1810
1576
  if (1 !== structured.length) {
1811
1577
  return null
@@ -1823,15 +1589,6 @@ function envelopeProp(resprops: any, opname: string): string | null {
1823
1589
  }
1824
1590
 
1825
1591
 
1826
- // An UNTAGGED union: `oneOf`/`anyOf` with two or more real branches and no
1827
- // `discriminator`. Nothing in the schema says which branch a given value is,
1828
- // so no generator can choose a variant, and the field can only be modelled as
1829
- // an open type.
1830
- //
1831
- // Two shapes are deliberately NOT unions to resolve:
1832
- // - a discriminated union — the discriminator names the deciding property;
1833
- // - the nullable idiom `anyOf: [X, {type: 'null'}]`, which is one type that
1834
- // may be absent, not a choice between variants.
1835
1592
  function untaggedUnionBranches(schema: any): number {
1836
1593
  if (null == schema || 'object' !== typeof schema) {
1837
1594
  return 0
@@ -1848,17 +1605,6 @@ function untaggedUnionBranches(schema: any): number {
1848
1605
  }
1849
1606
 
1850
1607
 
1851
- // Deepest/widest untagged union reachable from a field schema, or null when
1852
- // the field is resolvable.
1853
- //
1854
- // The search is RECURSIVE because the union is rarely at the top: in the
1855
- // Typebot Builder spec the `groups` field is an array whose item schema
1856
- // carries 18 untagged unions, the widest 19 branches, 12 levels down. Only a
1857
- // field that bottoms out in such a union has to degrade to an open type, so
1858
- // only a recursive scan can report which fields those are.
1859
- //
1860
- // `seen` guards the self-referential schemas these specs use freely; `depth`
1861
- // is bounded so a pathological document cannot spin.
1862
1608
  function scanUntaggedUnion(
1863
1609
  schema: any,
1864
1610
  depth: number = 0,
@@ -1904,15 +1650,6 @@ function scanUntaggedUnion(
1904
1650
  const MAX_UNION_SCAN_DEPTH = 64
1905
1651
 
1906
1652
 
1907
- // Is this schema a collection? null when the schema does not say.
1908
- //
1909
- // `isEntityWrapperProp` accepts a composed schema (allOf/oneOf/anyOf) as
1910
- // structured, but a composed schema carries no outer `type` or `items` — so
1911
- // reading those alone silently called it a non-list. A `list` then kept its
1912
- // envelope, and worse, a single-entity op unwrapped to an array-valued
1913
- // property. Composed branches are inspected instead, and unanimity required:
1914
- // a union that is an array in one branch and an object in another does not
1915
- // say what the caller will get, and an envelope is not worth guessing at.
1916
1653
  function propIsList(schema: any): boolean | null {
1917
1654
  if (null == schema || 'object' !== typeof schema) {
1918
1655
  return null
@@ -1939,26 +1676,6 @@ function propIsList(schema: any): boolean | null {
1939
1676
  }
1940
1677
 
1941
1678
 
1942
- // The request BODY a closed schema permits, as a transform mapping.
1943
- //
1944
- // `additionalProperties: false` is the spec saying the server rejects any
1945
- // property it did not declare. When a body says that, sending the caller's
1946
- // whole request payload is wrong: an op's payload also carries its PATH
1947
- // params (`id` for `PUT /item/{id}`), and a closed shape 400s the entire
1948
- // request over that one extra key. Restricting the body to the declared
1949
- // properties is then not a heuristic — it is what the spec asked for.
1950
- //
1951
- // Returns null for an open or property-less schema, where `reqdata` (send
1952
- // everything) remains the right default: an open body accepts extras, and
1953
- // with no declared properties there is nothing to restrict to.
1954
- //
1955
- // The KEY is the property's wire name — that is what goes on the wire and
1956
- // what the server matches against. The SOURCE is read by the field's
1957
- // CANONICAL name, because that is the only name the caller ever sees:
1958
- // findFieldDefs runs every property through `canonize(normalizeFieldName())`,
1959
- // so a spec property `UserName` reaches the generated request type as
1960
- // `user_name`. Reading `reqdata.UserName` would find nothing and send
1961
- // undefined.
1962
1679
  function closedBodyTransform(schema: any): Record<string, string> | null {
1963
1680
  if (null == schema || 'object' !== typeof schema) {
1964
1681
  return null
@@ -1972,10 +1689,6 @@ function closedBodyTransform(schema: any): Record<string, string> | null {
1972
1689
  return null
1973
1690
  }
1974
1691
 
1975
- // Null prototype: a spec is free to declare a property called `__proto__`,
1976
- // and on an ordinary object that assignment sets the prototype instead of
1977
- // creating an own property — the mapping would vanish, and with it the
1978
- // whole body restriction if it were the only one.
1979
1692
  const out: Record<string, string> = Object.create(null)
1980
1693
  for (const name of names) {
1981
1694
  out[name] = '`reqdata.' + canonize(normalizeFieldName(name)) + '`'
@@ -1983,16 +1696,6 @@ function closedBodyTransform(schema: any): Record<string, string> | null {
1983
1696
  return out
1984
1697
  }
1985
1698
 
1986
- // The first sentence of `text` (up to a `.`/`!`/`?` followed by whitespace
1987
- // or end), whitespace-collapsed and length-capped with an ellipsis.
1988
- //
1989
- // Shared by the API summary and by a field's `short`, because both answer
1990
- // the same question — give me one line of prose for a place that has room
1991
- // for one line. A field's `short` is interpolated straight into a markdown
1992
- // table cell by every generated Readme, where a raw newline ends the row and
1993
- // takes the rest of the table with it. Specs supply plenty: bullet lists,
1994
- // fenced examples and multi-paragraph notes all appear as a property's
1995
- // `description`, up to 1725 characters of it in the validation corpus.
1996
1699
  function firstSentence(text: string): string {
1997
1700
  const collapsed = text.replace(/\s+/g, ' ').trim()
1998
1701
  const m = collapsed.match(/^(.+?[.!?])(\s|$)/)
@@ -2037,6 +1740,7 @@ export {
2037
1740
  normalizeFieldName,
2038
1741
  prefixLeadingDigit,
2039
1742
  debugpath,
1743
+ debugpathOn,
2040
1744
  findPathsWithPrefix,
2041
1745
  writeFileSyncWarn,
2042
1746
  warnOnError,