@voxgig/apidef 6.3.7 → 6.3.8
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 +2 -2
- package/dist/apidef.js +21 -5
- package/dist/apidef.js.map +1 -1
- 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 +169 -8
- package/dist/utility.js.map +1 -1
- package/package.json +8 -8
- package/src/apidef.ts +22 -4
- 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 +186 -8
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
|
|
@@ -905,7 +922,17 @@ function renderJSONIC(
|
|
|
905
922
|
}
|
|
906
923
|
|
|
907
924
|
|
|
908
|
-
|
|
925
|
+
// Canonical type-sentinel vocabulary. VALID_CANON maps an OpenAPI type NAME
|
|
926
|
+
// to its `$SENTINEL` form; CANON_ONE is the union sentinel produced by
|
|
927
|
+
// `validator` for a multi-type (`['`$ONE`', [member, ...]]`). Both are part
|
|
928
|
+
// of the public API so downstream consumers (e.g. @voxgig/sdkgen's
|
|
929
|
+
// sentinel -> language-type table) can verify they cover the full set
|
|
930
|
+
// instead of hand-syncing against this file.
|
|
931
|
+
//
|
|
932
|
+
// Null-prototype (see NULL_PROTO_NOTE): `type` values come from the spec, so
|
|
933
|
+
// a schema declaring `type: constructor` would otherwise return the Object
|
|
934
|
+
// constructor here rather than falling through to the 'Any' default.
|
|
935
|
+
const VALID_CANON: Record<string, string> = Object.assign(Object.create(null), {
|
|
909
936
|
'string': '`$STRING`',
|
|
910
937
|
'number': '`$NUMBER`',
|
|
911
938
|
'integer': '`$INTEGER`',
|
|
@@ -914,7 +941,9 @@ const VALID_CANON: Record<string, string> = {
|
|
|
914
941
|
'array': '`$ARRAY`',
|
|
915
942
|
'object': '`$OBJECT`',
|
|
916
943
|
'any': '`$ANY`',
|
|
917
|
-
}
|
|
944
|
+
})
|
|
945
|
+
|
|
946
|
+
const CANON_ONE = '`$ONE`'
|
|
918
947
|
|
|
919
948
|
|
|
920
949
|
function validator(torig: undefined | string | string[]): any {
|
|
@@ -924,7 +953,7 @@ function validator(torig: undefined | string | string[]): any {
|
|
|
924
953
|
return canon
|
|
925
954
|
}
|
|
926
955
|
else if (Array.isArray(torig)) {
|
|
927
|
-
return [
|
|
956
|
+
return [CANON_ONE, torig.map((t: string) => validator(t))]
|
|
928
957
|
}
|
|
929
958
|
else {
|
|
930
959
|
return '`$ANY`'
|
|
@@ -1376,6 +1405,150 @@ export type {
|
|
|
1376
1405
|
PathMatch
|
|
1377
1406
|
}
|
|
1378
1407
|
|
|
1408
|
+
|
|
1409
|
+
// A response property only "wraps" the entity when it is itself a structured
|
|
1410
|
+
// value that could contain the entity: an object, an array, a $ref, or a
|
|
1411
|
+
// composed (allOf/oneOf/anyOf) schema. A scalar property (string, integer,
|
|
1412
|
+
// number, boolean) that merely shares the entity's name is a field of the
|
|
1413
|
+
// entity, not a wrapper, so the response must not be unwrapped down to it.
|
|
1414
|
+
function isEntityWrapperProp(propSchema: any): boolean {
|
|
1415
|
+
if (null == propSchema || 'object' !== typeof propSchema) {
|
|
1416
|
+
return false
|
|
1417
|
+
}
|
|
1418
|
+
if (null != propSchema.$ref) {
|
|
1419
|
+
return true
|
|
1420
|
+
}
|
|
1421
|
+
if (null != propSchema.properties ||
|
|
1422
|
+
null != propSchema.items ||
|
|
1423
|
+
null != propSchema.allOf ||
|
|
1424
|
+
null != propSchema.oneOf ||
|
|
1425
|
+
null != propSchema.anyOf) {
|
|
1426
|
+
return true
|
|
1427
|
+
}
|
|
1428
|
+
const t = propSchema.type
|
|
1429
|
+
return 'object' === t || 'array' === t
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
// A response body that is nothing but a single wrapper property is an
|
|
1434
|
+
// ENVELOPE around the result: `{item: {...}}`, `{data: {...}}`,
|
|
1435
|
+
// `{items: [...]}`, `{results: [...]}`. Return that property's name so the
|
|
1436
|
+
// caller can unwrap to it, or null when the body is the result itself.
|
|
1437
|
+
//
|
|
1438
|
+
// Two conditions keep this from firing on a response that IS the entity:
|
|
1439
|
+
//
|
|
1440
|
+
// 1. EXACTLY ONE property. A body with siblings is a structure in its own
|
|
1441
|
+
// right, not a wrapper — `{ok, id}` from a delete, or any paged
|
|
1442
|
+
// `{results, next}`, must be handed over whole.
|
|
1443
|
+
// 2. The property's SHAPE matches the operation's cardinality. A `list`
|
|
1444
|
+
// unwraps only to an array, every other op only to a non-array. So a
|
|
1445
|
+
// single-entity op facing `{items: [...]}` is left alone rather than
|
|
1446
|
+
// silently yielding a list, and vice versa.
|
|
1447
|
+
//
|
|
1448
|
+
// A one-field entity whose sole field is itself structured can still be
|
|
1449
|
+
// unwrapped wrongly; that is the residual cost of the spec not saying which
|
|
1450
|
+
// it means. Naming the wrapper after the entity remains the unambiguous
|
|
1451
|
+
// signal, and is still checked first.
|
|
1452
|
+
function envelopeProp(resprops: any, opname: string): string | null {
|
|
1453
|
+
const keys = keysof(resprops)
|
|
1454
|
+
if (1 !== keys.length) {
|
|
1455
|
+
return null
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const key = keys[0]
|
|
1459
|
+
const prop = resprops[key]
|
|
1460
|
+
if (!isEntityWrapperProp(prop)) {
|
|
1461
|
+
return null
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const islist = propIsList(prop)
|
|
1465
|
+
if (null == islist || islist !== ('list' === opname)) {
|
|
1466
|
+
return null
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
return key
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
// Is this schema a collection? null when the schema does not say.
|
|
1474
|
+
//
|
|
1475
|
+
// `isEntityWrapperProp` accepts a composed schema (allOf/oneOf/anyOf) as
|
|
1476
|
+
// structured, but a composed schema carries no outer `type` or `items` — so
|
|
1477
|
+
// reading those alone silently called it a non-list. A `list` then kept its
|
|
1478
|
+
// envelope, and worse, a single-entity op unwrapped to an array-valued
|
|
1479
|
+
// property. Composed branches are inspected instead, and unanimity required:
|
|
1480
|
+
// a union that is an array in one branch and an object in another does not
|
|
1481
|
+
// say what the caller will get, and an envelope is not worth guessing at.
|
|
1482
|
+
function propIsList(schema: any): boolean | null {
|
|
1483
|
+
if (null == schema || 'object' !== typeof schema) {
|
|
1484
|
+
return null
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
const branches = schema.allOf ?? schema.oneOf ?? schema.anyOf
|
|
1488
|
+
if (Array.isArray(branches)) {
|
|
1489
|
+
if (0 === branches.length) {
|
|
1490
|
+
return null
|
|
1491
|
+
}
|
|
1492
|
+
const first = propIsList(branches[0])
|
|
1493
|
+
if (null == first) {
|
|
1494
|
+
return null
|
|
1495
|
+
}
|
|
1496
|
+
for (const branch of branches) {
|
|
1497
|
+
if (propIsList(branch) !== first) {
|
|
1498
|
+
return null
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return first
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
return 'array' === schema.type || null != schema.items
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
// The request BODY a closed schema permits, as a transform mapping.
|
|
1509
|
+
//
|
|
1510
|
+
// `additionalProperties: false` is the spec saying the server rejects any
|
|
1511
|
+
// property it did not declare. When a body says that, sending the caller's
|
|
1512
|
+
// whole request payload is wrong: an op's payload also carries its PATH
|
|
1513
|
+
// params (`id` for `PUT /item/{id}`), and a closed shape 400s the entire
|
|
1514
|
+
// request over that one extra key. Restricting the body to the declared
|
|
1515
|
+
// properties is then not a heuristic — it is what the spec asked for.
|
|
1516
|
+
//
|
|
1517
|
+
// Returns null for an open or property-less schema, where `reqdata` (send
|
|
1518
|
+
// everything) remains the right default: an open body accepts extras, and
|
|
1519
|
+
// with no declared properties there is nothing to restrict to.
|
|
1520
|
+
//
|
|
1521
|
+
// The KEY is the property's wire name — that is what goes on the wire and
|
|
1522
|
+
// what the server matches against. The SOURCE is read by the field's
|
|
1523
|
+
// CANONICAL name, because that is the only name the caller ever sees:
|
|
1524
|
+
// findFieldDefs runs every property through `canonize(normalizeFieldName())`,
|
|
1525
|
+
// so a spec property `UserName` reaches the generated request type as
|
|
1526
|
+
// `user_name`. Reading `reqdata.UserName` would find nothing and send
|
|
1527
|
+
// undefined.
|
|
1528
|
+
function closedBodyTransform(schema: any): Record<string, string> | null {
|
|
1529
|
+
if (null == schema || 'object' !== typeof schema) {
|
|
1530
|
+
return null
|
|
1531
|
+
}
|
|
1532
|
+
if (false !== schema.additionalProperties) {
|
|
1533
|
+
return null
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const names = keysof(schema.properties)
|
|
1537
|
+
if (0 === names.length) {
|
|
1538
|
+
return null
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// Null prototype: a spec is free to declare a property called `__proto__`,
|
|
1542
|
+
// and on an ordinary object that assignment sets the prototype instead of
|
|
1543
|
+
// creating an own property — the mapping would vanish, and with it the
|
|
1544
|
+
// whole body restriction if it were the only one.
|
|
1545
|
+
const out: Record<string, string> = Object.create(null)
|
|
1546
|
+
for (const name of names) {
|
|
1547
|
+
out[name] = '`reqdata.' + canonize(normalizeFieldName(name)) + '`'
|
|
1548
|
+
}
|
|
1549
|
+
return out
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1379
1552
|
export {
|
|
1380
1553
|
nom,
|
|
1381
1554
|
getdlog,
|
|
@@ -1390,6 +1563,8 @@ export {
|
|
|
1390
1563
|
makeWarner,
|
|
1391
1564
|
formatJSONIC,
|
|
1392
1565
|
validator,
|
|
1566
|
+
VALID_CANON,
|
|
1567
|
+
CANON_ONE,
|
|
1393
1568
|
canonize,
|
|
1394
1569
|
canonizeCmpName,
|
|
1395
1570
|
stripSchemaNamespace,
|
|
@@ -1408,5 +1583,8 @@ export {
|
|
|
1408
1583
|
getModelPath,
|
|
1409
1584
|
sortedKeys,
|
|
1410
1585
|
sortedEntries,
|
|
1586
|
+
isEntityWrapperProp,
|
|
1587
|
+
envelopeProp,
|
|
1588
|
+
closedBodyTransform,
|
|
1411
1589
|
|
|
1412
1590
|
}
|