@voxgig/apidef 8.15.2 → 8.16.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/builder/entity/entity.js +8 -5
- package/dist/builder/entity/entity.js.map +1 -1
- package/dist/builder/entity/info.js +1 -1
- package/dist/builder/entity/info.js.map +1 -1
- package/dist/builder/flow.js +3 -3
- package/dist/builder/flow.js.map +1 -1
- package/dist/cli.js +8 -10
- package/dist/cli.js.map +1 -1
- package/dist/guide/guide.d.ts +14 -1
- package/dist/guide/guide.js +104 -62
- package/dist/guide/guide.js.map +1 -1
- package/dist/guide/heuristic01.js +105 -34
- package/dist/guide/heuristic01.js.map +1 -1
- package/dist/parse.d.ts +2 -1
- package/dist/parse.js +42 -43
- package/dist/parse.js.map +1 -1
- package/dist/refcount.d.ts +6 -0
- package/dist/refcount.js +215 -0
- package/dist/refcount.js.map +1 -0
- package/dist/transform/field.js +1 -1
- package/dist/transform/field.js.map +1 -1
- package/dist/transform/operation.js +1 -1
- package/dist/transform/operation.js.map +1 -1
- package/dist/transform/top.js +1 -1
- package/dist/transform/top.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.d.ts +2 -1
- package/dist/utility.js +42 -1
- package/dist/utility.js.map +1 -1
- package/model/{guide.aon → guide.aontu} +1 -1
- package/package.json +3 -3
- package/src/builder/entity/entity.ts +8 -5
- package/src/builder/entity/info.ts +1 -1
- package/src/builder/flow.ts +3 -3
- package/src/cli.ts +8 -10
- package/src/guide/guide.ts +133 -71
- package/src/guide/heuristic01.ts +136 -38
- package/src/parse.ts +64 -38
- package/src/refcount.ts +244 -0
- package/src/transform/field.ts +1 -1
- package/src/transform/operation.ts +1 -1
- package/src/transform/top.ts +1 -1
- package/src/types.ts +2 -2
- package/src/utility.ts +54 -1
- /package/model/{apidef.aon → apidef.aontu} +0 -0
package/src/parse.ts
CHANGED
|
@@ -132,6 +132,16 @@ async function parseOpenAPI(source: any, _meta?: any) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
|
|
135
|
+
// Edges decycle cut, so the guide can still count through them; a clone has none.
|
|
136
|
+
const DECYCLED = new WeakMap<object, Map<string, object>>()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
function decycledChild(holder: any, key: string | number): any {
|
|
140
|
+
const cut = DECYCLED.get(holder)?.get(String(key))
|
|
141
|
+
return undefined === cut ? holder[key] : cut
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
135
145
|
function decycle(root: any) {
|
|
136
146
|
// Entry path of each node on the current ancestor chain; presence in this
|
|
137
147
|
// map is what identifies a back-edge. Nodes are removed on the way out, so
|
|
@@ -161,6 +171,12 @@ function decycle(root: any) {
|
|
|
161
171
|
|
|
162
172
|
const cyclePath = onPath.get(val)
|
|
163
173
|
if (undefined !== cyclePath) {
|
|
174
|
+
let cuts = DECYCLED.get(node)
|
|
175
|
+
if (undefined === cuts) {
|
|
176
|
+
cuts = new Map()
|
|
177
|
+
DECYCLED.set(node, cuts)
|
|
178
|
+
}
|
|
179
|
+
cuts.set(String(key), val)
|
|
164
180
|
node[key] = `[Circular *${cyclePath.join('.')}]`
|
|
165
181
|
continue
|
|
166
182
|
}
|
|
@@ -189,54 +205,63 @@ function refSiblings(node: any): any {
|
|
|
189
205
|
}
|
|
190
206
|
|
|
191
207
|
|
|
192
|
-
function addXRefsAndResolve(
|
|
208
|
+
function addXRefsAndResolve(
|
|
209
|
+
obj: any,
|
|
210
|
+
root: any,
|
|
211
|
+
visited: WeakSet<any> = new WeakSet(),
|
|
212
|
+
expanding: Map<any, any> = new Map(),
|
|
213
|
+
) {
|
|
193
214
|
if (!obj || typeof obj !== 'object') return
|
|
194
|
-
if (!visited) visited = new WeakSet()
|
|
195
215
|
if (visited.has(obj)) return
|
|
196
216
|
visited.add(obj)
|
|
197
217
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
} else {
|
|
209
|
-
item['x-ref'] = xref
|
|
210
|
-
addXRefsAndResolve(item, root, visited)
|
|
211
|
-
}
|
|
212
|
-
} else {
|
|
213
|
-
addXRefsAndResolve(item, root, visited)
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
} else {
|
|
218
|
-
for (const key of Object.keys(obj)) {
|
|
219
|
-
const val = obj[key]
|
|
220
|
-
if (val && typeof val === 'object') {
|
|
221
|
-
if (typeof val.$ref === 'string') {
|
|
222
|
-
const xref = val.$ref
|
|
223
|
-
const resolved = resolvePointer(root, xref)
|
|
224
|
-
if (resolved !== undefined) {
|
|
225
|
-
obj[key] = { ...resolved, ...refSiblings(val), 'x-ref': xref }
|
|
226
|
-
addXRefsAndResolve(obj[key], root, visited)
|
|
227
|
-
} else {
|
|
228
|
-
val['x-ref'] = xref
|
|
229
|
-
addXRefsAndResolve(val, root, visited)
|
|
230
|
-
}
|
|
231
|
-
} else {
|
|
232
|
-
addXRefsAndResolve(val, root, visited)
|
|
233
|
-
}
|
|
218
|
+
const keys: (string | number)[] =
|
|
219
|
+
Array.isArray(obj) ? Array.from(obj.keys()) : Object.keys(obj)
|
|
220
|
+
|
|
221
|
+
for (const key of keys) {
|
|
222
|
+
const val = obj[key]
|
|
223
|
+
if (val && typeof val === 'object') {
|
|
224
|
+
if (typeof val.$ref === 'string') {
|
|
225
|
+
resolveRefSite(obj, key, root, visited, expanding)
|
|
226
|
+
} else {
|
|
227
|
+
addXRefsAndResolve(val, root, visited, expanding)
|
|
234
228
|
}
|
|
235
229
|
}
|
|
236
230
|
}
|
|
237
231
|
}
|
|
238
232
|
|
|
239
233
|
|
|
234
|
+
function resolveRefSite(
|
|
235
|
+
holder: any,
|
|
236
|
+
key: string | number,
|
|
237
|
+
root: any,
|
|
238
|
+
visited: WeakSet<any>,
|
|
239
|
+
expanding: Map<any, any>,
|
|
240
|
+
) {
|
|
241
|
+
const site = holder[key]
|
|
242
|
+
|
|
243
|
+
const inProgress = expanding.get(site)
|
|
244
|
+
if (undefined !== inProgress) {
|
|
245
|
+
holder[key] = inProgress
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const xref = site.$ref
|
|
250
|
+
const resolved = resolvePointer(root, xref)
|
|
251
|
+
if (resolved === undefined) {
|
|
252
|
+
site['x-ref'] = xref
|
|
253
|
+
addXRefsAndResolve(site, root, visited, expanding)
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const copy = { ...resolved, ...refSiblings(site), 'x-ref': xref }
|
|
258
|
+
holder[key] = copy
|
|
259
|
+
expanding.set(site, copy)
|
|
260
|
+
addXRefsAndResolve(copy, root, visited, expanding)
|
|
261
|
+
expanding.delete(site)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
240
265
|
function resolvePointer(root: any, ref: string): any {
|
|
241
266
|
const seen = new Set<string>()
|
|
242
267
|
const siblings: any[] = []
|
|
@@ -316,6 +341,7 @@ function validateSource(kind: string, source: any, meta: { file: string }) {
|
|
|
316
341
|
|
|
317
342
|
export {
|
|
318
343
|
parse,
|
|
344
|
+
decycledChild,
|
|
319
345
|
}
|
|
320
346
|
|
|
321
347
|
|
package/src/refcount.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/* Copyright (c) 2026 Voxgig, MIT License */
|
|
2
|
+
|
|
3
|
+
import { decycledChild } from './parse'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const REFCOUNT_CAP = 1_000_000_000
|
|
7
|
+
|
|
8
|
+
const ROOT = '\x01ROOT'
|
|
9
|
+
|
|
10
|
+
const INDEX_RE = /^(0|[1-9]\d*)$/
|
|
11
|
+
|
|
12
|
+
type Body = Map<string, number>
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
function satAdd(a: number, b: number): number {
|
|
16
|
+
return Math.min(a + b, REFCOUNT_CAP)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
function satMul(a: number, w: number): number {
|
|
21
|
+
if (0 === a || 0 === w) return 0
|
|
22
|
+
if (a > Math.floor(REFCOUNT_CAP / w)) return REFCOUNT_CAP
|
|
23
|
+
return a * w
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
function byCodePoint(a: string, b: string): number {
|
|
28
|
+
const n = Math.min(a.length, b.length)
|
|
29
|
+
for (let i = 0; i < n; i++) {
|
|
30
|
+
const x = a.charCodeAt(i)
|
|
31
|
+
const y = b.charCodeAt(i)
|
|
32
|
+
if (x !== y) return codePointRank(x) - codePointRank(y)
|
|
33
|
+
}
|
|
34
|
+
return a.length - b.length
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
// Surrogate units sort below U+E000-U+FFFF as UTF-16 but above them by code point.
|
|
39
|
+
function codePointRank(unit: number): number {
|
|
40
|
+
return unit < 0xD800 ? unit : unit < 0xE000 ? unit + 0x2000 : unit - 0x800
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
function isNode(v: any): boolean {
|
|
45
|
+
return null != v && 'object' === typeof v
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
function refLabel(v: any): string | undefined {
|
|
50
|
+
if (!isNode(v) || Array.isArray(v)) return undefined
|
|
51
|
+
if ('string' === typeof v['x-ref']) return v['x-ref']
|
|
52
|
+
if (!Object.prototype.hasOwnProperty.call(v, 'x-ref') && 'string' === typeof v.$ref) {
|
|
53
|
+
return v.$ref
|
|
54
|
+
}
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
function hasKid(node: any, key: string): boolean {
|
|
60
|
+
if (!isNode(node)) return false
|
|
61
|
+
return Array.isArray(node) ?
|
|
62
|
+
INDEX_RE.test(key) && Number(key) < node.length :
|
|
63
|
+
Object.prototype.hasOwnProperty.call(node, key)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
function hasKids(node: any): boolean {
|
|
68
|
+
if (!isNode(node)) return false
|
|
69
|
+
return 0 < (Array.isArray(node) ? node.length : Object.keys(node).length)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
function kidKeys(node: any): string[] {
|
|
74
|
+
return Array.isArray(node) ?
|
|
75
|
+
node.map((_: any, i: number) => String(i)) : Object.keys(node)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
// Parts join on '\0'. Escaping '\0' and '\x01' keeps keys injective and in part
|
|
80
|
+
// order, and an escape never puts 'R' after '\x01', so no key equals ROOT.
|
|
81
|
+
function nodeKey(label: string, over: string[]): string {
|
|
82
|
+
return [label, ...over]
|
|
83
|
+
.map(part => part.replace(/[\0\x01]/g, c => '\0' === c ? '\x01\x01' : '\x01\x02'))
|
|
84
|
+
.join('\0')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
// Occurrences of each reference label per use in the resolved spec, where a $ref
|
|
89
|
+
// chain resolves to its first label.
|
|
90
|
+
function countRefs(def: any): Record<string, number> {
|
|
91
|
+
const targets = new Map<string, any>()
|
|
92
|
+
const target = (label: string): any => {
|
|
93
|
+
if (targets.has(label)) return targets.get(label)
|
|
94
|
+
let t: any = undefined
|
|
95
|
+
if (label.startsWith('#/')) {
|
|
96
|
+
t = def
|
|
97
|
+
for (const raw of label.substring(2).split('/')) {
|
|
98
|
+
const part = raw.replace(/~1/g, '/').replace(/~0/g, '~')
|
|
99
|
+
if (!hasKid(t, part)) {
|
|
100
|
+
t = undefined
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
t = decycledChild(t, part)
|
|
104
|
+
}
|
|
105
|
+
if (!isNode(t)) t = undefined
|
|
106
|
+
}
|
|
107
|
+
targets.set(label, t)
|
|
108
|
+
return t
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const same = (a: any, b: any): boolean => {
|
|
112
|
+
if (isNode(a) && a === b) return true
|
|
113
|
+
const label = refLabel(a)
|
|
114
|
+
return undefined !== label && label === refLabel(b)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const labelOf = new Map<string, string>()
|
|
118
|
+
const skipOf = new Map<string, Set<string>>()
|
|
119
|
+
const found: string[] = []
|
|
120
|
+
|
|
121
|
+
const scan = (start: any, skip: Set<string>): Body => {
|
|
122
|
+
const body: Body = new Map()
|
|
123
|
+
const expanded = new Set<any>()
|
|
124
|
+
const stack: any[] = []
|
|
125
|
+
for (const k of kidKeys(start)) {
|
|
126
|
+
if ('x-ref' !== k && '$ref' !== k && !skip.has(k)) {
|
|
127
|
+
stack.push(decycledChild(start, k))
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
while (0 < stack.length) {
|
|
132
|
+
const v = stack.pop()
|
|
133
|
+
if (!isNode(v)) continue
|
|
134
|
+
|
|
135
|
+
const label = refLabel(v)
|
|
136
|
+
if (undefined === label) {
|
|
137
|
+
// Expanding a shared plain node once per scan is what ends a cycle of them.
|
|
138
|
+
if (expanded.has(v)) continue
|
|
139
|
+
expanded.add(v)
|
|
140
|
+
for (const k of kidKeys(v)) stack.push(decycledChild(v, k))
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const t = target(label)
|
|
145
|
+
const over: string[] = []
|
|
146
|
+
for (const k of Object.keys(v)) {
|
|
147
|
+
if ('x-ref' === k || '$ref' === k) continue
|
|
148
|
+
const vk = decycledChild(v, k)
|
|
149
|
+
if (hasKid(t, k)) {
|
|
150
|
+
const tk = decycledChild(t, k)
|
|
151
|
+
if (same(vk, tk)) continue
|
|
152
|
+
if (hasKids(tk)) over.push(k)
|
|
153
|
+
}
|
|
154
|
+
stack.push(vk)
|
|
155
|
+
}
|
|
156
|
+
over.sort(byCodePoint)
|
|
157
|
+
|
|
158
|
+
const node = nodeKey(label, over)
|
|
159
|
+
if (!labelOf.has(node)) {
|
|
160
|
+
labelOf.set(node, label)
|
|
161
|
+
skipOf.set(node, new Set(over))
|
|
162
|
+
found.push(node)
|
|
163
|
+
}
|
|
164
|
+
body.set(node, (body.get(node) ?? 0) + 1)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return body
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const bodies = new Map<string, Body>([[ROOT, scan(def, new Set())]])
|
|
171
|
+
for (let i = 0; i < found.length; i++) {
|
|
172
|
+
const node = found[i]
|
|
173
|
+
const t = target(labelOf.get(node) as string)
|
|
174
|
+
bodies.set(node, undefined === t ? new Map() : scan(t, skipOf.get(node) as Set<string>))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const count = countPaths(bodies)
|
|
178
|
+
|
|
179
|
+
const out: Record<string, number> = {}
|
|
180
|
+
for (const node of found) {
|
|
181
|
+
const label = labelOf.get(node) as string
|
|
182
|
+
out[label] = satAdd(out[label] ?? 0, count.get(node) ?? 0)
|
|
183
|
+
}
|
|
184
|
+
return out
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
function countPaths(bodies: Map<string, Body>): Map<string, number> {
|
|
189
|
+
// Code-point order, so both ports cut the same edge of a reference cycle.
|
|
190
|
+
const successors = (u: string) =>
|
|
191
|
+
[...(bodies.get(u) as Body).keys()].sort(byCodePoint)
|
|
192
|
+
|
|
193
|
+
const onStack = new Set<string>([ROOT])
|
|
194
|
+
const finished = new Set<string>()
|
|
195
|
+
const backEdges = new Map<string, Set<string>>()
|
|
196
|
+
const post: string[] = []
|
|
197
|
+
|
|
198
|
+
const stack = [{ node: ROOT, next: successors(ROOT), i: 0 }]
|
|
199
|
+
while (0 < stack.length) {
|
|
200
|
+
const frame = stack[stack.length - 1]
|
|
201
|
+
if (frame.i < frame.next.length) {
|
|
202
|
+
const v = frame.next[frame.i++]
|
|
203
|
+
if (onStack.has(v)) {
|
|
204
|
+
let cut = backEdges.get(frame.node)
|
|
205
|
+
if (undefined === cut) {
|
|
206
|
+
cut = new Set()
|
|
207
|
+
backEdges.set(frame.node, cut)
|
|
208
|
+
}
|
|
209
|
+
cut.add(v)
|
|
210
|
+
}
|
|
211
|
+
else if (!finished.has(v)) {
|
|
212
|
+
onStack.add(v)
|
|
213
|
+
stack.push({ node: v, next: successors(v), i: 0 })
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
stack.pop()
|
|
218
|
+
onStack.delete(frame.node)
|
|
219
|
+
finished.add(frame.node)
|
|
220
|
+
post.push(frame.node)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const count = new Map<string, number>([[ROOT, 1]])
|
|
225
|
+
for (let i = post.length - 1; 0 <= i; i--) {
|
|
226
|
+
const u = post[i]
|
|
227
|
+
const cu = count.get(u) ?? 0
|
|
228
|
+
const cut = backEdges.get(u)
|
|
229
|
+
for (const [v, w] of bodies.get(u) as Body) {
|
|
230
|
+
if (cut?.has(v)) continue
|
|
231
|
+
count.set(v, satAdd(count.get(v) ?? 0, satMul(cu, w)))
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return count
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
export {
|
|
239
|
+
REFCOUNT_CAP,
|
|
240
|
+
byCodePoint,
|
|
241
|
+
countRefs,
|
|
242
|
+
satAdd,
|
|
243
|
+
satMul,
|
|
244
|
+
}
|
package/src/transform/field.ts
CHANGED
|
@@ -509,7 +509,7 @@ function compositeId(
|
|
|
509
509
|
return { single: singleKeyOf(ment, identityParams(ment)) } as any
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
-
// `from` STATED IN guide.
|
|
512
|
+
// `from` STATED IN guide.aontu WINS PER PART, so a spec can correct one
|
|
513
513
|
// mapping without restating the others — which matters because the
|
|
514
514
|
// heuristic gets most of them right and the odd one wrong.
|
|
515
515
|
const withFrom = (parts: string[], usesep: string) => {
|
|
@@ -27,7 +27,7 @@ import type {
|
|
|
27
27
|
|
|
28
28
|
// The op names the transform resolves. Anything else under a guide path's
|
|
29
29
|
// `op` map is dropped, and an unknown name (a verb such as `merge`, or a
|
|
30
|
-
// typo) is dropped WITH A WARNING: guide.
|
|
30
|
+
// typo) is dropped WITH A WARNING: guide.aontu is the only correction surface
|
|
31
31
|
// (ADR-002), so a correction that vanishes silently defeats it. A non-CRUD
|
|
32
32
|
// verb is declared as `action: <verb>: {}` beside a CRUD op on the same path.
|
|
33
33
|
const RESOLVED_OPS = ['load', 'list', 'create', 'update', 'remove', 'patch']
|
package/src/transform/top.ts
CHANGED
|
@@ -40,7 +40,7 @@ const topTransform = async function(
|
|
|
40
40
|
// Guarantee at least one sentence of API description. Many specs (e.g. the
|
|
41
41
|
// readme.io-hosted Bluefin APIs) ship a placeholder `info.description` of
|
|
42
42
|
// "." — letterless, useless prose. When the description is empty or has no
|
|
43
|
-
// letters, synthesise a sentence from the title so the api-info.
|
|
43
|
+
// letters, synthesise a sentence from the title so the api-info.aontu (and
|
|
44
44
|
// the docs generated from it) never carry an empty/degenerate description.
|
|
45
45
|
kit.info.description = ensureDescription(kit.info)
|
|
46
46
|
|
package/src/types.ts
CHANGED
|
@@ -268,10 +268,10 @@ type GuideEntity = {
|
|
|
268
268
|
name: string
|
|
269
269
|
orig: string
|
|
270
270
|
// `false` drops the entity downstream (transform/entity.ts). Emitted by
|
|
271
|
-
// the heuristic for an access-token exchange, and editable in guide.
|
|
271
|
+
// the heuristic for an access-token exchange, and editable in guide.aontu —
|
|
272
272
|
// which is the ONLY correction surface (ADR-002).
|
|
273
273
|
active?: boolean
|
|
274
|
-
// Why the heuristic deactivated it, so guide.
|
|
274
|
+
// Why the heuristic deactivated it, so guide.aontu reads as a record of a
|
|
275
275
|
// decision rather than an unexplained `active: false`.
|
|
276
276
|
why_inactive?: string
|
|
277
277
|
field?: Record<string, GuidePath>
|
package/src/utility.ts
CHANGED
|
@@ -1141,7 +1141,7 @@ const CMP_RESULT_VERBS = [
|
|
|
1141
1141
|
|
|
1142
1142
|
const CMP_PREFIXES = ['get_', 'post_', 'put_', 'delete_', 'patch_']
|
|
1143
1143
|
|
|
1144
|
-
// A guide node (entity, path or op) is active unless guide.
|
|
1144
|
+
// A guide node (entity, path or op) is active unless guide.aontu explicitly
|
|
1145
1145
|
// says otherwise. `active` has always been declared in the guide model and
|
|
1146
1146
|
// documented as the escape hatch for a misclassified entity; this is the
|
|
1147
1147
|
// single place that decides what it means, so entity/flow/operation transforms
|
|
@@ -1595,6 +1595,58 @@ function envelopeProp(resprops: any, opname: string): string | null {
|
|
|
1595
1595
|
}
|
|
1596
1596
|
|
|
1597
1597
|
|
|
1598
|
+
// What a page may hold beside its records, compared without case, `_` or
|
|
1599
|
+
// `-`. Any other property is data, which makes the component a record.
|
|
1600
|
+
const ENVELOPE_PAGING_PROPS = new Set([
|
|
1601
|
+
'count', 'total', 'totalcount', 'totalhits', 'totalitems', 'totalpages',
|
|
1602
|
+
'totalresults', 'page', 'pages', 'pagecount', 'pagenumber', 'pagesize',
|
|
1603
|
+
'perpage', 'limit', 'offset', 'cursor', 'next', 'nextcursor', 'nextpage',
|
|
1604
|
+
'nextpagetoken', 'nexttoken', 'previous', 'prev', 'previouscursor',
|
|
1605
|
+
'prevcursor', 'previouspage', 'prevpage', 'hasmore', 'hasnext',
|
|
1606
|
+
'hasprevious', 'object', 'url',
|
|
1607
|
+
])
|
|
1608
|
+
|
|
1609
|
+
|
|
1610
|
+
function isEnvelopePagingProp(name: string): boolean {
|
|
1611
|
+
return ENVELOPE_PAGING_PROPS.has(name.toLowerCase().replace(/[_-]/g, ''))
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
|
|
1615
|
+
// The component a response envelope carries: the resolved reference of the
|
|
1616
|
+
// record envelopeProp unwraps to. Narrower than envelopeProp, since a record
|
|
1617
|
+
// with one structured property passes that test too: an envelope has no `id`,
|
|
1618
|
+
// a page holds nothing beside its records but paging, and a single-item
|
|
1619
|
+
// envelope holds nothing beside the item.
|
|
1620
|
+
function envelopeItemRef(schema: any, opname: string): string | null {
|
|
1621
|
+
const props = schema?.properties
|
|
1622
|
+
const key = envelopeProp(props, opname)
|
|
1623
|
+
if (null == key || null != props.id) {
|
|
1624
|
+
return null
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
const prop = props[key]
|
|
1628
|
+
const islist = propIsList(prop)
|
|
1629
|
+
const rest = keysof(props).filter((k: string) => k !== key)
|
|
1630
|
+
if (islist ? !rest.every(isEnvelopePagingProp) : 0 < rest.length) {
|
|
1631
|
+
return null
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
const item = islist ? prop.items : prop
|
|
1635
|
+
if (!isRecordSchema(item)) {
|
|
1636
|
+
return null
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
const xref = item['x-ref']
|
|
1640
|
+
return 'string' === typeof xref && '' !== xref ? xref : null
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
function isRecordSchema(schema: any): boolean {
|
|
1645
|
+
return null != schema && 'object' === typeof schema && (
|
|
1646
|
+
null != schema.properties || null != schema.allOf || 'object' === schema.type)
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
|
|
1598
1650
|
function untaggedUnionBranches(schema: any): number {
|
|
1599
1651
|
if (null == schema || 'object' !== typeof schema) {
|
|
1600
1652
|
return 0
|
|
@@ -1757,6 +1809,7 @@ export {
|
|
|
1757
1809
|
sortedEntries,
|
|
1758
1810
|
isEntityWrapperProp,
|
|
1759
1811
|
envelopeProp,
|
|
1812
|
+
envelopeItemRef,
|
|
1760
1813
|
closedBodyTransform,
|
|
1761
1814
|
untaggedUnionBranches,
|
|
1762
1815
|
scanUntaggedUnion,
|
|
File without changes
|