@voxgig/apidef 6.3.7 → 6.3.9
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/dist/apidef.d.ts +3 -2
- package/dist/apidef.js +35 -5
- package/dist/apidef.js.map +1 -1
- package/dist/builder/entity/entity.d.ts +2 -1
- package/dist/builder/entity/entity.js +63 -0
- package/dist/builder/entity/entity.js.map +1 -1
- package/dist/builder/entity/info.js.map +1 -1
- package/dist/builder/entity.js.map +1 -1
- package/dist/builder/flow/flowHeuristic01.js.map +1 -1
- package/dist/builder/flow.js.map +1 -1
- package/dist/guide/guide.js +49 -6
- package/dist/guide/guide.js.map +1 -1
- package/dist/guide/heuristic01.js +99 -89
- package/dist/guide/heuristic01.js.map +1 -1
- package/dist/parse.js +151 -18
- package/dist/parse.js.map +1 -1
- package/dist/resolver.js.map +1 -1
- package/dist/transform/args.js.map +1 -1
- package/dist/transform/clean.js.map +1 -1
- package/dist/transform/entity.js.map +1 -1
- package/dist/transform/field.js +14 -0
- package/dist/transform/field.js.map +1 -1
- package/dist/transform/flow.js.map +1 -1
- package/dist/transform/flowstep.js.map +1 -1
- package/dist/transform/operation.js.map +1 -1
- package/dist/transform/select.js.map +1 -1
- package/dist/transform/top.d.ts +2 -1
- package/dist/transform/top.js +32 -2
- package/dist/transform/top.js.map +1 -1
- package/dist/transform.d.ts +8 -7
- package/dist/transform.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +97 -84
- package/dist/types.js.map +1 -1
- package/dist/utility.d.ts +6 -1
- package/dist/utility.js +205 -16
- package/dist/utility.js.map +1 -1
- package/package.json +8 -8
- package/src/apidef.ts +38 -4
- package/src/builder/entity/entity.ts +70 -1
- package/src/guide/guide.ts +50 -6
- package/src/guide/heuristic01.ts +37 -28
- package/src/parse.ts +161 -20
- package/src/transform/field.ts +16 -1
- package/src/transform/top.ts +37 -2
- package/src/tsconfig.json +1 -0
- package/src/types.ts +5 -0
- package/src/utility.ts +226 -16
package/src/transform/top.ts
CHANGED
|
@@ -44,6 +44,13 @@ const topTransform = async function(
|
|
|
44
44
|
kit.info = stringifyInfoScalars(def.info ?? {})
|
|
45
45
|
kit.info.servers = stringifyInfoScalars(def.servers ?? [])
|
|
46
46
|
|
|
47
|
+
// Guarantee at least one sentence of API description. Many specs (e.g. the
|
|
48
|
+
// readme.io-hosted Bluefin APIs) ship a placeholder `info.description` of
|
|
49
|
+
// "." — letterless, useless prose. When the description is empty or has no
|
|
50
|
+
// letters, synthesise a sentence from the title so the api-info.aontu (and
|
|
51
|
+
// the docs generated from it) never carry an empty/degenerate description.
|
|
52
|
+
kit.info.description = ensureDescription(kit.info)
|
|
53
|
+
|
|
47
54
|
// Public APIs that declare NO authentication (no security schemes, no
|
|
48
55
|
// top-level `security`, and no per-operation `security`) get an explicit
|
|
49
56
|
// no-auth signal in the model. Downstream sdkgen reads it via
|
|
@@ -120,6 +127,31 @@ const topTransform = async function(
|
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
|
|
130
|
+
// True when the text carries at least one alphabetic character — i.e. it is
|
|
131
|
+
// real prose rather than a placeholder like "." / "---" / whitespace.
|
|
132
|
+
function hasLetters(text: string): boolean {
|
|
133
|
+
return /[a-zA-Z]/.test(text)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
// A non-empty, at-least-one-sentence description for the API. Keeps the spec's
|
|
138
|
+
// own `info.description` when it is real prose; otherwise synthesises a sentence
|
|
139
|
+
// from the title (falling back to a generic sentence when even that is
|
|
140
|
+
// missing). Never returns an empty or letterless string.
|
|
141
|
+
function ensureDescription(info: any): string {
|
|
142
|
+
const current = 'string' === typeof info.description ? info.description.trim() : ''
|
|
143
|
+
if ('' !== current && hasLetters(current)) {
|
|
144
|
+
return info.description
|
|
145
|
+
}
|
|
146
|
+
const title = 'string' === typeof info.title ? info.title.trim() : ''
|
|
147
|
+
if ('' === title || !hasLetters(title)) {
|
|
148
|
+
return 'Client SDK for this API.'
|
|
149
|
+
}
|
|
150
|
+
// Avoid a redundant "… Api API." when the title already names itself an API.
|
|
151
|
+
return 'The ' + title + (/\bapi\b/i.test(title) ? '' : ' API') + '.'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
123
155
|
// A short one-line description of the API's purpose: the spec's
|
|
124
156
|
// `info.summary` (OpenAPI 3.1) when present, else the first prose sentence
|
|
125
157
|
// of `info.description` with leading markdown headings/blank lines stripped
|
|
@@ -129,12 +161,14 @@ function resolveSummary(def: any): string | undefined {
|
|
|
129
161
|
const info = def?.info ?? {}
|
|
130
162
|
|
|
131
163
|
const explicit = 'string' === typeof info.summary ? info.summary.trim() : ''
|
|
132
|
-
if ('' !== explicit) {
|
|
164
|
+
if ('' !== explicit && hasLetters(explicit)) {
|
|
133
165
|
return firstSentence(explicit)
|
|
134
166
|
}
|
|
135
167
|
|
|
136
168
|
const desc = 'string' === typeof info.description ? info.description : ''
|
|
137
|
-
|
|
169
|
+
// Treat letterless prose (a bare "." placeholder, "---", …) as no summary
|
|
170
|
+
// rather than surfacing it verbatim.
|
|
171
|
+
if ('' === desc.trim() || !hasLetters(desc)) {
|
|
138
172
|
return undefined
|
|
139
173
|
}
|
|
140
174
|
|
|
@@ -418,6 +452,7 @@ export {
|
|
|
418
452
|
topTransform,
|
|
419
453
|
resolveSecurity,
|
|
420
454
|
resolveSummary,
|
|
455
|
+
ensureDescription,
|
|
421
456
|
resolveWebsite,
|
|
422
457
|
homepageFromServer,
|
|
423
458
|
findAuthPrefix,
|
package/src/tsconfig.json
CHANGED
package/src/types.ts
CHANGED
|
@@ -160,6 +160,11 @@ type Metrics = {
|
|
|
160
160
|
|
|
161
161
|
type ApiDefContext = {
|
|
162
162
|
fs: any,
|
|
163
|
+
// True only when the caller supplied ApiDefOptions.fs (e.g. memfs), as
|
|
164
|
+
// opposed to the node:fs default. aontu/@tabnas/multisource treats the
|
|
165
|
+
// presence of an injected fs as "paths are POSIX", so forwarding the real
|
|
166
|
+
// node:fs breaks include resolution on Windows. See guide/guide.ts.
|
|
167
|
+
fsInjected: boolean,
|
|
163
168
|
log: any,
|
|
164
169
|
spec: any,
|
|
165
170
|
opts: any,
|
package/src/utility.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { snakify, camelify, kebabify, each } from 'jostraca'
|
|
|
6
6
|
import { decircular } from '@voxgig/util'
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
slice, merge, inject, clone, isnode, walk, transform, select,
|
|
9
|
+
slice, merge, inject, clone, isnode, walk, transform, select, keysof,
|
|
10
10
|
Injection,
|
|
11
11
|
M_VAL,
|
|
12
12
|
M_KEYPRE,
|
|
@@ -134,7 +134,12 @@ function formatJsonSrc(jsonsrc: string) {
|
|
|
134
134
|
//
|
|
135
135
|
// Keys are lowercase; depluralize() does a case-insensitive lookup
|
|
136
136
|
// and reapplies the caller's casing on the way out.
|
|
137
|
-
|
|
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
|
+
const IRREGULARS: Record<string, string> = Object.assign(Object.create(null), {
|
|
138
143
|
'analytics': 'analytics',
|
|
139
144
|
'analyses': 'analysis',
|
|
140
145
|
'appendices': 'appendix',
|
|
@@ -198,7 +203,18 @@ const IRREGULARS: Record<string, string> = {
|
|
|
198
203
|
'vertices': 'vertex',
|
|
199
204
|
'women': 'woman',
|
|
200
205
|
'yes': 'yes',
|
|
201
|
-
}
|
|
206
|
+
})
|
|
207
|
+
|
|
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).
|
|
202
218
|
|
|
203
219
|
// Sorted longest-first so the most specific IRREGULARS suffix wins.
|
|
204
220
|
// Without this, 'women' would be shadowed by 'men' (3 < 5) under
|
|
@@ -235,12 +251,13 @@ function matchCase(source: string, target: string): string {
|
|
|
235
251
|
// inherit the override without signature churn. apidef is
|
|
236
252
|
// single-model-per-process; if that ever changes, switch this to a
|
|
237
253
|
// per-context map.
|
|
238
|
-
|
|
254
|
+
// Null-prototype: see NULL_PROTO_NOTE above.
|
|
255
|
+
let CUSTOM_PLURALS: Record<string, string> = Object.create(null)
|
|
239
256
|
let CUSTOM_PLURAL_KEYS: string[] = []
|
|
240
257
|
|
|
241
258
|
|
|
242
259
|
function setCustomPlurals(plurals: Record<string, string> | undefined | null) {
|
|
243
|
-
CUSTOM_PLURALS =
|
|
260
|
+
CUSTOM_PLURALS = Object.create(null)
|
|
244
261
|
if (plurals) {
|
|
245
262
|
for (const k of Object.keys(plurals)) {
|
|
246
263
|
// Skip null/undefined values so a partially-typed model entry
|
|
@@ -752,7 +769,7 @@ function formatJSONIC(
|
|
|
752
769
|
}
|
|
753
770
|
|
|
754
771
|
return renderJSONIC(val, hsepd, showd, useColor, maxlines, exclude, c,
|
|
755
|
-
renderPrimitive, renderComment)
|
|
772
|
+
renderPrimitive, renderComment, false)
|
|
756
773
|
}
|
|
757
774
|
|
|
758
775
|
|
|
@@ -766,6 +783,12 @@ function renderJSONIC(
|
|
|
766
783
|
c: (color: any, text: string) => string,
|
|
767
784
|
renderPrimitive: (v: any) => string,
|
|
768
785
|
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
|
+
decircularized: boolean,
|
|
769
792
|
): string {
|
|
770
793
|
|
|
771
794
|
const space = ' '
|
|
@@ -782,6 +805,10 @@ function renderJSONIC(
|
|
|
782
805
|
kind: 'close'
|
|
783
806
|
token: '}' | ']'
|
|
784
807
|
indentLevel: number
|
|
808
|
+
// The container this frame closes. `seen` tracks the ANCESTOR PATH, so
|
|
809
|
+
// the node is removed again on the way out — otherwise a merely repeated
|
|
810
|
+
// (shared) node is indistinguishable from a cycle.
|
|
811
|
+
node?: any
|
|
785
812
|
}
|
|
786
813
|
|
|
787
814
|
const seen = new WeakSet()
|
|
@@ -807,6 +834,9 @@ function renderJSONIC(
|
|
|
807
834
|
top -= 1
|
|
808
835
|
|
|
809
836
|
if (frame.kind === 'close') {
|
|
837
|
+
if (undefined !== frame.node) {
|
|
838
|
+
seen.delete(frame.node)
|
|
839
|
+
}
|
|
810
840
|
const indent = space.repeat(frame.indentLevel)
|
|
811
841
|
const hsep = 0 < frame.indentLevel && frame.indentLevel <= hsepd
|
|
812
842
|
lines.push(`${indent}${c('bracket', frame.token)}${hsep ? '\n' : ''}`)
|
|
@@ -826,10 +856,29 @@ function renderJSONIC(
|
|
|
826
856
|
continue
|
|
827
857
|
}
|
|
828
858
|
|
|
829
|
-
//
|
|
859
|
+
// Repeated reference — fall back to decircular, but only once.
|
|
830
860
|
if (seen.has(v)) {
|
|
831
|
-
|
|
832
|
-
|
|
861
|
+
if (!decircularized) {
|
|
862
|
+
// decircular recurses per level, so a deep value can overflow the
|
|
863
|
+
// stack inside it. This whole path usually runs while formatting an
|
|
864
|
+
// error, so degrade to the marker instead of throwing over it.
|
|
865
|
+
let flat: any
|
|
866
|
+
try {
|
|
867
|
+
flat = decircular(val)
|
|
868
|
+
}
|
|
869
|
+
catch (err: any) {
|
|
870
|
+
flat = undefined
|
|
871
|
+
}
|
|
872
|
+
if (undefined !== flat) {
|
|
873
|
+
return renderJSONIC(flat, hsepd, showd, useColor, maxlines, exclude, c,
|
|
874
|
+
renderPrimitive, renderComment, true)
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
// decircular did not resolve it, so render a marker rather than
|
|
878
|
+
// recursing again. Better a placeholder than a crash while
|
|
879
|
+
// formatting an error the user actually needs to read.
|
|
880
|
+
lines.push(`${linePrefix}${c('string', '"[Circular]"')}${commentSuffix}`)
|
|
881
|
+
continue
|
|
833
882
|
}
|
|
834
883
|
seen.add(v)
|
|
835
884
|
|
|
@@ -837,13 +886,13 @@ function renderJSONIC(
|
|
|
837
886
|
const arr = v as any[]
|
|
838
887
|
if (arr.length === 0) {
|
|
839
888
|
lines.push(`${linePrefix}${c('bracket', '[')}${commentSuffix}`)
|
|
840
|
-
stack[++top] = { kind: 'close', token: ']', indentLevel }
|
|
889
|
+
stack[++top] = { kind: 'close', token: ']', indentLevel, node: v }
|
|
841
890
|
continue
|
|
842
891
|
}
|
|
843
892
|
|
|
844
893
|
// opening line
|
|
845
894
|
lines.push(`${linePrefix}${c('bracket', '[')}${commentSuffix}`)
|
|
846
|
-
stack[++top] = { kind: 'close', token: ']', indentLevel }
|
|
895
|
+
stack[++top] = { kind: 'close', token: ']', indentLevel, node: v }
|
|
847
896
|
|
|
848
897
|
// children (reverse push)
|
|
849
898
|
const childPrefix = space.repeat(indentLevel + 1)
|
|
@@ -872,13 +921,13 @@ function renderJSONIC(
|
|
|
872
921
|
|
|
873
922
|
if (printableKeys.length === 0) {
|
|
874
923
|
lines.push(`${linePrefix}${c('bracket', '{')}${commentSuffix}`)
|
|
875
|
-
stack[++top] = { kind: 'close', token: '}', indentLevel }
|
|
924
|
+
stack[++top] = { kind: 'close', token: '}', indentLevel, node: v }
|
|
876
925
|
continue
|
|
877
926
|
}
|
|
878
927
|
|
|
879
928
|
// opening line
|
|
880
929
|
lines.push(`${linePrefix}${c('bracket', '{')}${commentSuffix}`)
|
|
881
|
-
stack[++top] = { kind: 'close', token: '}', indentLevel }
|
|
930
|
+
stack[++top] = { kind: 'close', token: '}', indentLevel, node: v }
|
|
882
931
|
|
|
883
932
|
const nextIndentStr = space.repeat(indentLevel + 1)
|
|
884
933
|
for (let i = printableKeys.length - 1; i >= 0; i--) {
|
|
@@ -905,7 +954,17 @@ function renderJSONIC(
|
|
|
905
954
|
}
|
|
906
955
|
|
|
907
956
|
|
|
908
|
-
|
|
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
|
+
const VALID_CANON: Record<string, string> = Object.assign(Object.create(null), {
|
|
909
968
|
'string': '`$STRING`',
|
|
910
969
|
'number': '`$NUMBER`',
|
|
911
970
|
'integer': '`$INTEGER`',
|
|
@@ -914,7 +973,9 @@ const VALID_CANON: Record<string, string> = {
|
|
|
914
973
|
'array': '`$ARRAY`',
|
|
915
974
|
'object': '`$OBJECT`',
|
|
916
975
|
'any': '`$ANY`',
|
|
917
|
-
}
|
|
976
|
+
})
|
|
977
|
+
|
|
978
|
+
const CANON_ONE = '`$ONE`'
|
|
918
979
|
|
|
919
980
|
|
|
920
981
|
function validator(torig: undefined | string | string[]): any {
|
|
@@ -924,7 +985,7 @@ function validator(torig: undefined | string | string[]): any {
|
|
|
924
985
|
return canon
|
|
925
986
|
}
|
|
926
987
|
else if (Array.isArray(torig)) {
|
|
927
|
-
return [
|
|
988
|
+
return [CANON_ONE, torig.map((t: string) => validator(t))]
|
|
928
989
|
}
|
|
929
990
|
else {
|
|
930
991
|
return '`$ANY`'
|
|
@@ -1376,6 +1437,150 @@ export type {
|
|
|
1376
1437
|
PathMatch
|
|
1377
1438
|
}
|
|
1378
1439
|
|
|
1440
|
+
|
|
1441
|
+
// A response property only "wraps" the entity when it is itself a structured
|
|
1442
|
+
// value that could contain the entity: an object, an array, a $ref, or a
|
|
1443
|
+
// composed (allOf/oneOf/anyOf) schema. A scalar property (string, integer,
|
|
1444
|
+
// number, boolean) that merely shares the entity's name is a field of the
|
|
1445
|
+
// entity, not a wrapper, so the response must not be unwrapped down to it.
|
|
1446
|
+
function isEntityWrapperProp(propSchema: any): boolean {
|
|
1447
|
+
if (null == propSchema || 'object' !== typeof propSchema) {
|
|
1448
|
+
return false
|
|
1449
|
+
}
|
|
1450
|
+
if (null != propSchema.$ref) {
|
|
1451
|
+
return true
|
|
1452
|
+
}
|
|
1453
|
+
if (null != propSchema.properties ||
|
|
1454
|
+
null != propSchema.items ||
|
|
1455
|
+
null != propSchema.allOf ||
|
|
1456
|
+
null != propSchema.oneOf ||
|
|
1457
|
+
null != propSchema.anyOf) {
|
|
1458
|
+
return true
|
|
1459
|
+
}
|
|
1460
|
+
const t = propSchema.type
|
|
1461
|
+
return 'object' === t || 'array' === t
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
// A response body that is nothing but a single wrapper property is an
|
|
1466
|
+
// ENVELOPE around the result: `{item: {...}}`, `{data: {...}}`,
|
|
1467
|
+
// `{items: [...]}`, `{results: [...]}`. Return that property's name so the
|
|
1468
|
+
// caller can unwrap to it, or null when the body is the result itself.
|
|
1469
|
+
//
|
|
1470
|
+
// Two conditions keep this from firing on a response that IS the entity:
|
|
1471
|
+
//
|
|
1472
|
+
// 1. EXACTLY ONE property. A body with siblings is a structure in its own
|
|
1473
|
+
// right, not a wrapper — `{ok, id}` from a delete, or any paged
|
|
1474
|
+
// `{results, next}`, must be handed over whole.
|
|
1475
|
+
// 2. The property's SHAPE matches the operation's cardinality. A `list`
|
|
1476
|
+
// unwraps only to an array, every other op only to a non-array. So a
|
|
1477
|
+
// single-entity op facing `{items: [...]}` is left alone rather than
|
|
1478
|
+
// silently yielding a list, and vice versa.
|
|
1479
|
+
//
|
|
1480
|
+
// A one-field entity whose sole field is itself structured can still be
|
|
1481
|
+
// unwrapped wrongly; that is the residual cost of the spec not saying which
|
|
1482
|
+
// it means. Naming the wrapper after the entity remains the unambiguous
|
|
1483
|
+
// signal, and is still checked first.
|
|
1484
|
+
function envelopeProp(resprops: any, opname: string): string | null {
|
|
1485
|
+
const keys = keysof(resprops)
|
|
1486
|
+
if (1 !== keys.length) {
|
|
1487
|
+
return null
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
const key = keys[0]
|
|
1491
|
+
const prop = resprops[key]
|
|
1492
|
+
if (!isEntityWrapperProp(prop)) {
|
|
1493
|
+
return null
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const islist = propIsList(prop)
|
|
1497
|
+
if (null == islist || islist !== ('list' === opname)) {
|
|
1498
|
+
return null
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
return key
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
|
|
1505
|
+
// Is this schema a collection? null when the schema does not say.
|
|
1506
|
+
//
|
|
1507
|
+
// `isEntityWrapperProp` accepts a composed schema (allOf/oneOf/anyOf) as
|
|
1508
|
+
// structured, but a composed schema carries no outer `type` or `items` — so
|
|
1509
|
+
// reading those alone silently called it a non-list. A `list` then kept its
|
|
1510
|
+
// envelope, and worse, a single-entity op unwrapped to an array-valued
|
|
1511
|
+
// property. Composed branches are inspected instead, and unanimity required:
|
|
1512
|
+
// a union that is an array in one branch and an object in another does not
|
|
1513
|
+
// say what the caller will get, and an envelope is not worth guessing at.
|
|
1514
|
+
function propIsList(schema: any): boolean | null {
|
|
1515
|
+
if (null == schema || 'object' !== typeof schema) {
|
|
1516
|
+
return null
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
const branches = schema.allOf ?? schema.oneOf ?? schema.anyOf
|
|
1520
|
+
if (Array.isArray(branches)) {
|
|
1521
|
+
if (0 === branches.length) {
|
|
1522
|
+
return null
|
|
1523
|
+
}
|
|
1524
|
+
const first = propIsList(branches[0])
|
|
1525
|
+
if (null == first) {
|
|
1526
|
+
return null
|
|
1527
|
+
}
|
|
1528
|
+
for (const branch of branches) {
|
|
1529
|
+
if (propIsList(branch) !== first) {
|
|
1530
|
+
return null
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return first
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
return 'array' === schema.type || null != schema.items
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
// The request BODY a closed schema permits, as a transform mapping.
|
|
1541
|
+
//
|
|
1542
|
+
// `additionalProperties: false` is the spec saying the server rejects any
|
|
1543
|
+
// property it did not declare. When a body says that, sending the caller's
|
|
1544
|
+
// whole request payload is wrong: an op's payload also carries its PATH
|
|
1545
|
+
// params (`id` for `PUT /item/{id}`), and a closed shape 400s the entire
|
|
1546
|
+
// request over that one extra key. Restricting the body to the declared
|
|
1547
|
+
// properties is then not a heuristic — it is what the spec asked for.
|
|
1548
|
+
//
|
|
1549
|
+
// Returns null for an open or property-less schema, where `reqdata` (send
|
|
1550
|
+
// everything) remains the right default: an open body accepts extras, and
|
|
1551
|
+
// with no declared properties there is nothing to restrict to.
|
|
1552
|
+
//
|
|
1553
|
+
// The KEY is the property's wire name — that is what goes on the wire and
|
|
1554
|
+
// what the server matches against. The SOURCE is read by the field's
|
|
1555
|
+
// CANONICAL name, because that is the only name the caller ever sees:
|
|
1556
|
+
// findFieldDefs runs every property through `canonize(normalizeFieldName())`,
|
|
1557
|
+
// so a spec property `UserName` reaches the generated request type as
|
|
1558
|
+
// `user_name`. Reading `reqdata.UserName` would find nothing and send
|
|
1559
|
+
// undefined.
|
|
1560
|
+
function closedBodyTransform(schema: any): Record<string, string> | null {
|
|
1561
|
+
if (null == schema || 'object' !== typeof schema) {
|
|
1562
|
+
return null
|
|
1563
|
+
}
|
|
1564
|
+
if (false !== schema.additionalProperties) {
|
|
1565
|
+
return null
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
const names = keysof(schema.properties)
|
|
1569
|
+
if (0 === names.length) {
|
|
1570
|
+
return null
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// Null prototype: a spec is free to declare a property called `__proto__`,
|
|
1574
|
+
// and on an ordinary object that assignment sets the prototype instead of
|
|
1575
|
+
// creating an own property — the mapping would vanish, and with it the
|
|
1576
|
+
// whole body restriction if it were the only one.
|
|
1577
|
+
const out: Record<string, string> = Object.create(null)
|
|
1578
|
+
for (const name of names) {
|
|
1579
|
+
out[name] = '`reqdata.' + canonize(normalizeFieldName(name)) + '`'
|
|
1580
|
+
}
|
|
1581
|
+
return out
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1379
1584
|
export {
|
|
1380
1585
|
nom,
|
|
1381
1586
|
getdlog,
|
|
@@ -1390,6 +1595,8 @@ export {
|
|
|
1390
1595
|
makeWarner,
|
|
1391
1596
|
formatJSONIC,
|
|
1392
1597
|
validator,
|
|
1598
|
+
VALID_CANON,
|
|
1599
|
+
CANON_ONE,
|
|
1393
1600
|
canonize,
|
|
1394
1601
|
canonizeCmpName,
|
|
1395
1602
|
stripSchemaNamespace,
|
|
@@ -1408,5 +1615,8 @@ export {
|
|
|
1408
1615
|
getModelPath,
|
|
1409
1616
|
sortedKeys,
|
|
1410
1617
|
sortedEntries,
|
|
1618
|
+
isEntityWrapperProp,
|
|
1619
|
+
envelopeProp,
|
|
1620
|
+
closedBodyTransform,
|
|
1411
1621
|
|
|
1412
1622
|
}
|