@voxgig/apidef 8.2.3 → 8.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/desc.d.ts +2 -0
- package/dist/guide/graphql01.d.ts +3 -2
- package/dist/guide/graphql01.js +48 -2
- package/dist/guide/graphql01.js.map +1 -1
- package/dist/guide/guide.js +65 -0
- package/dist/guide/guide.js.map +1 -1
- package/dist/guide/heuristic01.js +119 -5
- package/dist/guide/heuristic01.js.map +1 -1
- package/dist/model.d.ts +6 -0
- package/dist/transform/args.js +21 -2
- package/dist/transform/args.js.map +1 -1
- package/dist/transform/entity.js +1 -0
- package/dist/transform/entity.js.map +1 -1
- package/dist/transform/field.js +332 -4
- package/dist/transform/field.js.map +1 -1
- package/dist/transform/operation.js +43 -3
- package/dist/transform/operation.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.js.map +1 -1
- package/dist/utility.d.ts +2 -1
- package/dist/utility.js +32 -8
- package/dist/utility.js.map +1 -1
- package/model/apidef.aon +40 -0
- package/model/guide.aon +43 -0
- package/package.json +1 -1
- package/src/desc.ts +6 -0
- package/src/guide/graphql01.ts +61 -3
- package/src/guide/guide.ts +74 -0
- package/src/guide/heuristic01.ts +134 -4
- package/src/model.ts +27 -0
- package/src/transform/args.ts +26 -2
- package/src/transform/entity.ts +1 -0
- package/src/transform/field.ts +373 -4
- package/src/transform/operation.ts +49 -3
- package/src/types.ts +15 -0
- package/src/utility.ts +34 -9
package/src/guide/heuristic01.ts
CHANGED
|
@@ -633,6 +633,15 @@ function ResolveEntityName(spec: TaskSpec) {
|
|
|
633
633
|
ment.entname = entname
|
|
634
634
|
ment.pm = pm
|
|
635
635
|
|
|
636
|
+
// Which entity took each path+method, in resolution order. verbOnParent
|
|
637
|
+
// reads the item path's GET owner from here: a t/p/ item path can be
|
|
638
|
+
// split across entities by method (a PUT answering with a one-off
|
|
639
|
+
// acknowledgement is named after it), and the parent of a verb is the
|
|
640
|
+
// entity a read of the item returns.
|
|
641
|
+
work.pathowner = work.pathowner ?? {}
|
|
642
|
+
work.pathowner[pathStr] = work.pathowner[pathStr] ?? {}
|
|
643
|
+
work.pathowner[pathStr][methodName] = entname
|
|
644
|
+
|
|
636
645
|
debugpath(pathStr, methodName, 'RESOLVE-ENTITY-NAME',
|
|
637
646
|
formatJSONIC({ entdesc, ment }, { hsepd: 0, $: true, color: true }))
|
|
638
647
|
}
|
|
@@ -737,6 +746,10 @@ function RenameParams(spec: TaskSpec) {
|
|
|
737
746
|
oldParam.endsWith('id')
|
|
738
747
|
|| oldParam.endsWith('Id')
|
|
739
748
|
|| canonize(oldParam) === parentName
|
|
749
|
+
// GitHub-style `<parent>_number` keys (pull_number, issue_number):
|
|
750
|
+
// the parent's own key under another name. Only when the param sits
|
|
751
|
+
// under its own entity, so a nested collection keeps its parent key.
|
|
752
|
+
|| (oldParam.endsWith('_number') && parentName === entdesc.name)
|
|
740
753
|
|
|
741
754
|
debugpath(pathStr, mdesc.method, 'RENAME-PARAM-PART', parts, partI, partStr, {
|
|
742
755
|
lastPart,
|
|
@@ -960,8 +973,20 @@ function FindActions(spec: TaskSpec) {
|
|
|
960
973
|
|
|
961
974
|
const cmp = ment.cmp
|
|
962
975
|
|
|
976
|
+
// A verb that ResolveEntityName assigned to its parent entity
|
|
977
|
+
// (verbOnParent) is an action whatever the parent literal canonizes to:
|
|
978
|
+
// `/app/installations/{installation_id}/access_tokens` belongs to `app`.
|
|
979
|
+
// Recorded directly rather than through updateAction, whose guard against
|
|
980
|
+
// an entity "already encoding" the verb would drop `archive` on
|
|
981
|
+
// `email_archive` and leave the verb as a plain CRUD point.
|
|
982
|
+
if (null != ment.verb_on_parent) {
|
|
983
|
+
pathdesc.action[lastPartCanon] = pathdesc.action[lastPartCanon] ?? {
|
|
984
|
+
why_action: ['ent', entdesc.name, 'verb-on-parent', lastPart, methodName],
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
963
988
|
// /api/foo/bar where foo is the entity and bar is the action, no id param
|
|
964
|
-
if (
|
|
989
|
+
else if (
|
|
965
990
|
secondLastPartCanon === cmp
|
|
966
991
|
|| secondLastPartCanon === ment.origcmp
|
|
967
992
|
|| secondLastPartCanon === entname
|
|
@@ -1272,9 +1297,17 @@ function entityPathMatch_tpte(
|
|
|
1272
1297
|
let ecm = undefined
|
|
1273
1298
|
|
|
1274
1299
|
if (null != ment.cmp) {
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1300
|
+
const parent = verbOnParent(data, pm, mdesc)
|
|
1301
|
+
if (null != parent) {
|
|
1302
|
+
entname = parent
|
|
1303
|
+
ment.verb_on_parent = getelem(pm, -1)
|
|
1304
|
+
why.push('verb-on-parent=' + parent)
|
|
1305
|
+
}
|
|
1306
|
+
else {
|
|
1307
|
+
ecm = entityCmpMatch(data, entname, mdesc, why)
|
|
1308
|
+
entname = ecm.name
|
|
1309
|
+
why.push('has-cmp=' + ecm.orig)
|
|
1310
|
+
}
|
|
1278
1311
|
}
|
|
1279
1312
|
|
|
1280
1313
|
else if (probableEntityMethod(data, mdesc, pm, why)) {
|
|
@@ -1312,6 +1345,103 @@ function endsWithCmp(data: any, pm: PathMatch) {
|
|
|
1312
1345
|
}
|
|
1313
1346
|
|
|
1314
1347
|
|
|
1348
|
+
// A write on `.../<parent>/{id}/<verb>` is a VERB ON THE PARENT, not an
|
|
1349
|
+
// entity named after its result shape.
|
|
1350
|
+
//
|
|
1351
|
+
// GitHub's `PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge` answers with
|
|
1352
|
+
// a `pull-request-merge-result` component. Naming the method's entity after
|
|
1353
|
+
// that component (the cmp-primary rule) produced a `pull_request_merge_result`
|
|
1354
|
+
// entity with a single `update` op, while the GET on the same path (no
|
|
1355
|
+
// response schema) stayed an action on `pull`: one route split across two
|
|
1356
|
+
// entities by method, and the verb unreachable from the entity it acts on.
|
|
1357
|
+
//
|
|
1358
|
+
// Five signals, together: the method writes (a GET on such a path is a
|
|
1359
|
+
// sub-resource read and keeps the component rule); the response component
|
|
1360
|
+
// occurs nowhere else in the spec (a one-off result, not a resource shape);
|
|
1361
|
+
// that component is not the literal's own collection shape (a create-only
|
|
1362
|
+
// `POST .../{id}/labels` answering with a `label` is a nested collection,
|
|
1363
|
+
// not a verb); the item selector itself (`.../pulls/{pull_number}`) is a
|
|
1364
|
+
// path of the spec, so the trailing literal cannot be a collection of its
|
|
1365
|
+
// own; and nothing extends the path (`.../private-registries/{secret_name}`
|
|
1366
|
+
// makes `private-registries` a collection, whatever its POST answers with).
|
|
1367
|
+
// The verb then joins the parent entity, where FindActions records it as an
|
|
1368
|
+
// action and select stamps `$action` on its points. Returns the parent's
|
|
1369
|
+
// entity name, or null when the rule does not apply.
|
|
1370
|
+
function verbOnParent(
|
|
1371
|
+
data: { def: any, guide: any, work: any },
|
|
1372
|
+
pm: PathMatch,
|
|
1373
|
+
mdesc: any,
|
|
1374
|
+
): null | string {
|
|
1375
|
+
const method = mdesc.method
|
|
1376
|
+
if ('GET' === method || 'QUERY' === method || 'HEAD' === method || 'OPTIONS' === method) {
|
|
1377
|
+
return null
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
const ment = mdesc.MethodEntity
|
|
1381
|
+
if (1 < (ment.cmpoccur ?? 0)) {
|
|
1382
|
+
return null
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// A PLURAL literal names a nested collection, whatever it answers with:
|
|
1386
|
+
// `asset_keys` under `{environment_id}` creates an asset key, and
|
|
1387
|
+
// `approvals` under `{merge_request_iid}` is a collection of approvals.
|
|
1388
|
+
// A verb is singular — `merge`, `revoke`, `resend_confirmation` — so the
|
|
1389
|
+
// component rule below is never reached for a plural, which is what keeps
|
|
1390
|
+
// a create-only collection an entity of its own.
|
|
1391
|
+
const lit = snakify(getelem(pm, -1))
|
|
1392
|
+
if ('' === lit || depluralize(lit) !== lit) {
|
|
1393
|
+
return null
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// A singular literal still names a collection when its response component
|
|
1397
|
+
// is that collection's member shape (`label` answering with `label`, or
|
|
1398
|
+
// with a parent-prefixed `thing_label`); a verb answers with something
|
|
1399
|
+
// else.
|
|
1400
|
+
const verb = canonize(getelem(pm, -1))
|
|
1401
|
+
const cmp = String(ment.cmp ?? '')
|
|
1402
|
+
if ('' === verb || cmp === verb || cmp.endsWith('_' + verb)) {
|
|
1403
|
+
return null
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
const defpaths = data.def?.paths ?? {}
|
|
1407
|
+
|
|
1408
|
+
// Paths compare with parameters normalised: the item path may spell its
|
|
1409
|
+
// key `{id}` where the verb path spells it `{thing_number}`.
|
|
1410
|
+
const normalize = (p: string) => p.replace(/\{[^}]+\}/g, '{}')
|
|
1411
|
+
const itemNorm = normalize(pm.path.replace(/\/[^/]+$/, ''))
|
|
1412
|
+
const prefix = normalize(pm.path) + '/'
|
|
1413
|
+
|
|
1414
|
+
let itemPath: undefined | string = undefined
|
|
1415
|
+
for (const p of Object.keys(defpaths)) {
|
|
1416
|
+
const pn = normalize(p)
|
|
1417
|
+
if (pn === itemNorm) {
|
|
1418
|
+
itemPath = p
|
|
1419
|
+
}
|
|
1420
|
+
// A leaf: no path continues past the verb. Compared on a segment
|
|
1421
|
+
// boundary, so `/merge` is not "extended" by `/merge-async`.
|
|
1422
|
+
else if (pn.startsWith(prefix)) {
|
|
1423
|
+
return null
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (null == itemPath) {
|
|
1427
|
+
return null
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Methods resolve in path order, so the item path's owners are already
|
|
1431
|
+
// known. The parent is the entity a READ of the item returns: a PUT on
|
|
1432
|
+
// the item answering with a one-off acknowledgement is named after that
|
|
1433
|
+
// and must not claim the verb. Fall back to any owner, then the literal.
|
|
1434
|
+
const owners = data.work.pathowner?.[itemPath] ?? {}
|
|
1435
|
+
const parent = owners.GET ?? owners.QUERY ??
|
|
1436
|
+
Object.values(owners).sort()[0] as undefined | string
|
|
1437
|
+
if (null != parent) {
|
|
1438
|
+
return parent
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
return canonize(getelem(pm, -3))
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
|
|
1315
1445
|
function isOrigCmp(data: any, name: string) {
|
|
1316
1446
|
return null != data.guide.metrics.count.origcmprefs[name]
|
|
1317
1447
|
}
|
package/src/model.ts
CHANGED
|
@@ -84,6 +84,23 @@ type ModelField = {
|
|
|
84
84
|
// `description`. Absent when the spec does not describe the property —
|
|
85
85
|
// generators render an empty cell rather than inventing prose.
|
|
86
86
|
short?: string
|
|
87
|
+
|
|
88
|
+
// SPEC FACTS ABOUT THE FIELD ITSELF, carried through verbatim from the
|
|
89
|
+
// OpenAPI property. Facts the spec states, not inferences.
|
|
90
|
+
//
|
|
91
|
+
// `readOnly` is the load-bearing one: it is the difference between a field
|
|
92
|
+
// a client MAY send and one it may not, which nothing else in this record
|
|
93
|
+
// expresses. Without it every generator necessarily puts server-assigned
|
|
94
|
+
// fields into the type a caller fills in.
|
|
95
|
+
//
|
|
96
|
+
// The booleans are present ONLY when the spec declares them true — each
|
|
97
|
+
// defaults to false in OpenAPI, so absent and explicit-false mean the same
|
|
98
|
+
// thing and emitting the false ones would change every model for no
|
|
99
|
+
// information. `format` is present only for a non-empty string.
|
|
100
|
+
readOnly?: boolean
|
|
101
|
+
writeOnly?: boolean
|
|
102
|
+
deprecated?: boolean
|
|
103
|
+
format?: string
|
|
87
104
|
union?: {
|
|
88
105
|
count: number // how many untagged unions lie beneath the field
|
|
89
106
|
branches: number // widest branch count among them
|
|
@@ -215,6 +232,16 @@ type ModelEntity = {
|
|
|
215
232
|
id?: {
|
|
216
233
|
name: string
|
|
217
234
|
field: string
|
|
235
|
+
// COMPOSITE IDENTITY. Present only when the API addresses one record by
|
|
236
|
+
// MORE THAN ONE path parameter, so no single parameter is the id.
|
|
237
|
+
// github's repo is the case: GET /repos/{owner}/{repo} needs both, and
|
|
238
|
+
// neither alone names a repository.
|
|
239
|
+
//
|
|
240
|
+
// `parts` are those parameters in path order; `sep` joins them into the
|
|
241
|
+
// one `id` an SDK entity carries. Absent means the ordinary single-key
|
|
242
|
+
// entity, so downstream can branch on presence alone.
|
|
243
|
+
parts?: string[]
|
|
244
|
+
sep?: string
|
|
218
245
|
}
|
|
219
246
|
relations: ModelEntityRelations
|
|
220
247
|
}
|
package/src/transform/args.ts
CHANGED
|
@@ -71,7 +71,7 @@ const argsTransform: Transform = async function(
|
|
|
71
71
|
argdefs.push(...(opdef?.parameters ?? []))
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
resolveArgs(ment, mop, mpoint, argdefs)
|
|
74
|
+
resolveArgs(ctx, ment, mop, mpoint, argdefs)
|
|
75
75
|
})
|
|
76
76
|
|
|
77
77
|
})
|
|
@@ -116,7 +116,10 @@ const ARG_KIND: Record<string, ModelArg["kind"]> = {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
|
|
119
|
-
function resolveArgs(
|
|
119
|
+
function resolveArgs(
|
|
120
|
+
ctx: any,
|
|
121
|
+
ment: ModelEntity, mop: ModelOp, mpoint: ModelPoint, argdefs: ParameterDef[]
|
|
122
|
+
) {
|
|
120
123
|
const touchedKeys = new Set<string>()
|
|
121
124
|
|
|
122
125
|
each(argdefs, (argdef: ParameterDef) => {
|
|
@@ -124,6 +127,27 @@ function resolveArgs(ment: ModelEntity, mop: ModelOp, mpoint: ModelPoint, argdef
|
|
|
124
127
|
// by; the snakified form is the user-friendly runtime identifier.
|
|
125
128
|
const specName = normalizeFieldName(argdef.name)
|
|
126
129
|
const orig = depluralize(snakify(specName))
|
|
130
|
+
|
|
131
|
+
// A parameter with no name is not a parameter. This is what a DANGLING
|
|
132
|
+
// `$ref` looks like by the time it reaches here: the reference survives
|
|
133
|
+
// unresolved, `name` and `in` are both absent, and the arg would become
|
|
134
|
+
// a nameless `query` entry that every target then has to render. Ruby
|
|
135
|
+
// cannot: `Struct.new(:"")` raises at load and takes the whole SDK with
|
|
136
|
+
// it. Drop it and say which reference is missing.
|
|
137
|
+
if ('' === orig) {
|
|
138
|
+
const ref = (argdef as any)?.$ref
|
|
139
|
+
ctx?.warn?.({
|
|
140
|
+
note: `Parameter with no name on entity=${ment.name} op=${mop.name}` +
|
|
141
|
+
` path=${mpoint.orig} is dropped` +
|
|
142
|
+
(null == ref ? '.' : `: \`$ref\` "${ref}" resolves to nothing.`) +
|
|
143
|
+
' A parameter needs a `name`, or a reference that resolves to one.',
|
|
144
|
+
entity: ment.name,
|
|
145
|
+
path: mpoint.orig,
|
|
146
|
+
op: mop.name,
|
|
147
|
+
})
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
127
151
|
const kind = ARG_KIND[argdef.in] ?? 'query'
|
|
128
152
|
// Rename map can be keyed by either the spec original (camelCase) or by
|
|
129
153
|
// the snakified form depending on which path went through heuristic01.
|
package/src/transform/entity.ts
CHANGED