@voxgig/apidef 8.15.1 → 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/guide/guide.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import Path from 'node:path'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { each } from 'jostraca'
|
|
6
6
|
|
|
7
7
|
import { Aontu } from 'aontu'
|
|
8
8
|
|
|
@@ -46,47 +46,79 @@ const aontu = new Aontu()
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (fs.existsSync(guidepath) || !fs.existsSync(legacyguide)) {
|
|
54
|
-
return false
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let migrated = String(fs.readFileSync(legacyguide, 'utf8'))
|
|
58
|
-
.replace(/@"@voxgig\/apidef\/model\/guide\.aontu"/g,
|
|
59
|
-
'@"@voxgig/apidef/model/guide.aon"')
|
|
49
|
+
function migrateGuideIncludes(src: string, guideprefix: string): string {
|
|
50
|
+
let migrated = src
|
|
51
|
+
.replace(/@"@voxgig\/apidef\/model\/guide\.aon"/g,
|
|
52
|
+
'@"@voxgig/apidef/model/guide.aontu"')
|
|
60
53
|
|
|
61
54
|
// The sibling include is written bare or with `./`; both name this file.
|
|
62
55
|
for (const dir of ['', './']) {
|
|
63
56
|
migrated = migrated
|
|
64
|
-
.split('@"' + dir + guideprefix + 'base-guide.
|
|
65
|
-
.join('@"' + dir + guideprefix + 'base-guide.
|
|
57
|
+
.split('@"' + dir + guideprefix + 'base-guide.aon"')
|
|
58
|
+
.join('@"' + dir + guideprefix + 'base-guide.aontu"')
|
|
66
59
|
}
|
|
67
60
|
|
|
68
|
-
|
|
61
|
+
return migrated
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
// aontu refuses a bare sibling include, so it gains the `./` it needs.
|
|
66
|
+
function prefixGuideInclude(src: string, guideprefix: string): string {
|
|
67
|
+
return src
|
|
68
|
+
.split('@"' + guideprefix + 'base-guide.aontu"')
|
|
69
|
+
.join('@"./' + guideprefix + 'base-guide.aontu"')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
// A `.aon` entry file is unresolvable: aontu reads only `.aontu` as source.
|
|
74
|
+
// So this renames AND rewrites both includes — a repair, not a convenience.
|
|
75
|
+
function migrateLegacyGuide(fs: any, folder: string, guideprefix: string): boolean {
|
|
76
|
+
const guidepath = Path.join(folder, 'guide', guideprefix + 'guide.aontu')
|
|
77
|
+
const legacyguide = Path.join(folder, 'guide', guideprefix + 'guide.aon')
|
|
78
|
+
|
|
79
|
+
if (fs.existsSync(guidepath) || !fs.existsSync(legacyguide)) {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fs.writeFileSync(guidepath,
|
|
84
|
+
migrateGuideIncludes(String(fs.readFileSync(legacyguide, 'utf8')), guideprefix))
|
|
69
85
|
try { fs.unlinkSync(legacyguide) } catch (_err: any) { }
|
|
70
86
|
|
|
71
87
|
return true
|
|
72
88
|
}
|
|
73
89
|
|
|
90
|
+
|
|
91
|
+
// A `.aontu` entry file may still include a `.aon` sibling, so the rename
|
|
92
|
+
// above never fires for it while its include still names an absent file.
|
|
93
|
+
function migrateLegacyGuideInclude(
|
|
94
|
+
fs: any, guidepath: string, guideprefix: string
|
|
95
|
+
): boolean {
|
|
96
|
+
return rewriteGuide(fs, guidepath, (src) => migrateGuideIncludes(src, guideprefix))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
74
100
|
function migrateGuideIncludePrefix(
|
|
75
101
|
fs: any, guidepath: string, guideprefix: string
|
|
102
|
+
): boolean {
|
|
103
|
+
return rewriteGuide(fs, guidepath, (src) => prefixGuideInclude(src, guideprefix))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
function rewriteGuide(
|
|
108
|
+
fs: any, guidepath: string, rewrite: (src: string) => string
|
|
76
109
|
): boolean {
|
|
77
110
|
if (!fs.existsSync(guidepath)) {
|
|
78
111
|
return false
|
|
79
112
|
}
|
|
80
113
|
|
|
81
|
-
const bare = '@"' + guideprefix + 'base-guide.aon"'
|
|
82
114
|
const src = String(fs.readFileSync(guidepath, 'utf8'))
|
|
115
|
+
const migrated = rewrite(src)
|
|
83
116
|
|
|
84
|
-
if (
|
|
117
|
+
if (migrated === src) {
|
|
85
118
|
return false
|
|
86
119
|
}
|
|
87
120
|
|
|
88
|
-
fs.writeFileSync(guidepath,
|
|
89
|
-
src.split(bare).join('@"./' + guideprefix + 'base-guide.aon"'))
|
|
121
|
+
fs.writeFileSync(guidepath, migrated)
|
|
90
122
|
|
|
91
123
|
return true
|
|
92
124
|
}
|
|
@@ -94,6 +126,26 @@ function migrateGuideIncludePrefix(
|
|
|
94
126
|
|
|
95
127
|
|
|
96
128
|
|
|
129
|
+
function guideConflictMessage(
|
|
130
|
+
path: string, conflict: { line: number, text: string }
|
|
131
|
+
): string {
|
|
132
|
+
return `@voxgig/apidef: guide: unresolved merge conflict at ${path}:${conflict.line}\n` +
|
|
133
|
+
` ${conflict.text}\n` +
|
|
134
|
+
`Resolve the marked block in ${path}.`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
function missingGuideMessage(path: string, guideprefix: string): string {
|
|
139
|
+
return `@voxgig/apidef: guide: ${path} defines no guide map; it needs the include ` +
|
|
140
|
+
`@"./${guideprefix}base-guide.aontu".`
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
function isPlainObject(val: any): boolean {
|
|
145
|
+
return null != val && 'object' === typeof val && !Array.isArray(val)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
97
149
|
function findConflict(src: string): { line: number, text: string } | null {
|
|
98
150
|
const lines = String(src || '').split('\n')
|
|
99
151
|
|
|
@@ -115,7 +167,7 @@ async function buildGuide(ctx: ApiDefContext): Promise<any> {
|
|
|
115
167
|
const folder = Path.resolve(ctx.opts.folder)
|
|
116
168
|
|
|
117
169
|
try {
|
|
118
|
-
|
|
170
|
+
await buildBaseGuide(ctx)
|
|
119
171
|
}
|
|
120
172
|
catch (err: any) {
|
|
121
173
|
errs.push(err)
|
|
@@ -125,16 +177,23 @@ async function buildGuide(ctx: ApiDefContext): Promise<any> {
|
|
|
125
177
|
|
|
126
178
|
let src = ''
|
|
127
179
|
const guideprefix = null == ctx.opts.outprefix ? '' : ctx.opts.outprefix
|
|
128
|
-
let guidepath = Path.join(folder, 'guide', guideprefix + 'guide.
|
|
180
|
+
let guidepath = Path.join(folder, 'guide', guideprefix + 'guide.aontu')
|
|
129
181
|
|
|
130
182
|
if (migrateLegacyGuide(ctx.fs, folder, guideprefix)) {
|
|
131
|
-
log.info({ point: 'migrate-guide', note: 'guide.
|
|
183
|
+
log.info({ point: 'migrate-guide', note: 'guide.aon -> guide.aontu' })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (migrateLegacyGuideInclude(ctx.fs, guidepath, guideprefix)) {
|
|
187
|
+
log.info({
|
|
188
|
+
point: 'migrate-guide-include',
|
|
189
|
+
note: 'base-guide.aon -> base-guide.aontu'
|
|
190
|
+
})
|
|
132
191
|
}
|
|
133
192
|
|
|
134
193
|
if (migrateGuideIncludePrefix(ctx.fs, guidepath, guideprefix)) {
|
|
135
194
|
log.info({
|
|
136
195
|
point: 'migrate-guide-prefix',
|
|
137
|
-
note: 'base-guide.
|
|
196
|
+
note: 'base-guide.aontu -> ./base-guide.aontu'
|
|
138
197
|
})
|
|
139
198
|
}
|
|
140
199
|
|
|
@@ -154,35 +213,10 @@ async function buildGuide(ctx: ApiDefContext): Promise<any> {
|
|
|
154
213
|
|
|
155
214
|
handleErrors(ctx, errs)
|
|
156
215
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
checksrc = checkpath === guidepath ? src : String(ctx.fs.readFileSync(checkpath, 'utf8'))
|
|
162
|
-
}
|
|
163
|
-
catch (_err: any) {
|
|
164
|
-
continue
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const conflict = findConflict(checksrc)
|
|
168
|
-
if (null != conflict) {
|
|
169
|
-
errs.push(new Error(
|
|
170
|
-
`@voxgig/apidef: guide: unresolved merge conflict at ${
|
|
171
|
-
relativizePath(checkpath)}:${conflict.line}\n` +
|
|
172
|
-
` ${conflict.text}\n` +
|
|
173
|
-
`A guide is merged, not overwritten, so an edit the regenerated base\n` +
|
|
174
|
-
`guide contradicts is left for a human to settle. Resolve the marked\n` +
|
|
175
|
-
`block` +
|
|
176
|
-
// DELETING ONLY HELPS FOR THE BASE GUIDE. Regeneration rewrites that
|
|
177
|
-
// file, while the top-level entry guide is the user's own and is read
|
|
178
|
-
// back unchanged — so advising its deletion would send a reader in a
|
|
179
|
-
// circle, failing this same check on the next build.
|
|
180
|
-
(checkpath === basepath ?
|
|
181
|
-
`, or delete ${guideprefix}base-guide.aon to regenerate it from the\n` +
|
|
182
|
-
`specification and re-apply the edit afterwards.` :
|
|
183
|
-
` in ${relativizePath(checkpath)}.`)))
|
|
184
|
-
break
|
|
185
|
-
}
|
|
216
|
+
// Only the entry file: the base guide was just rewritten from the spec.
|
|
217
|
+
const conflict = findConflict(src)
|
|
218
|
+
if (null != conflict) {
|
|
219
|
+
errs.push(new Error(guideConflictMessage(relativizePath(guidepath), conflict)))
|
|
186
220
|
}
|
|
187
221
|
|
|
188
222
|
handleErrors(ctx, errs)
|
|
@@ -194,7 +228,7 @@ async function buildGuide(ctx: ApiDefContext): Promise<any> {
|
|
|
194
228
|
|
|
195
229
|
const opts: any = {
|
|
196
230
|
path: guidepath,
|
|
197
|
-
|
|
231
|
+
errfs: ctx.fs,
|
|
198
232
|
}
|
|
199
233
|
|
|
200
234
|
if (ctx.fsInjected) {
|
|
@@ -203,7 +237,19 @@ async function buildGuide(ctx: ApiDefContext): Promise<any> {
|
|
|
203
237
|
|
|
204
238
|
ctx.work.guideAontuFs = undefined !== opts.fs
|
|
205
239
|
|
|
206
|
-
|
|
240
|
+
// One AontuError carries every aontu failure, formatted: collect mode
|
|
241
|
+
// leaves a generation-time failure's message empty.
|
|
242
|
+
let guideModel: any
|
|
243
|
+
try {
|
|
244
|
+
guideModel = aontu.generate(src, opts)
|
|
245
|
+
}
|
|
246
|
+
catch (err: any) {
|
|
247
|
+
errs.push(err)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (0 === errs.length && !isPlainObject(guideModel?.guide)) {
|
|
251
|
+
errs.push(new Error(missingGuideMessage(relativizePath(guidepath), guideprefix)))
|
|
252
|
+
}
|
|
207
253
|
|
|
208
254
|
handleErrors(ctx, errs)
|
|
209
255
|
|
|
@@ -256,8 +302,10 @@ async function buildBaseGuide(ctx: ApiDefContext) {
|
|
|
256
302
|
throw new Error('Unknown guide strategy: ' + ctx.opts.strategy)
|
|
257
303
|
}
|
|
258
304
|
|
|
305
|
+
const guideprefix = null == ctx.opts.outprefix ? '' : ctx.opts.outprefix
|
|
306
|
+
|
|
259
307
|
const guideBlocks = [
|
|
260
|
-
|
|
308
|
+
...baseGuideHeader(guideprefix),
|
|
261
309
|
'',
|
|
262
310
|
'guide: {',
|
|
263
311
|
]
|
|
@@ -371,7 +419,7 @@ async function buildBaseGuide(ctx: ApiDefContext) {
|
|
|
371
419
|
guideBlocks.push(
|
|
372
420
|
` # Deactivated by the heuristic` +
|
|
373
421
|
(null == why ? '' : ` (${why})`) + `. Set` +
|
|
374
|
-
` \`active: true\` here in guide.
|
|
422
|
+
` \`active: true\` here in guide.aontu to generate it as an entity.`)
|
|
375
423
|
guideBlocks.push(` active: *false`)
|
|
376
424
|
}
|
|
377
425
|
|
|
@@ -392,25 +440,32 @@ async function buildBaseGuide(ctx: ApiDefContext) {
|
|
|
392
440
|
|
|
393
441
|
ctx.note.guide = { base: guideSrc }
|
|
394
442
|
|
|
395
|
-
const
|
|
396
|
-
|
|
443
|
+
const guidefolder = Path.join(ctx.opts.folder, 'guide')
|
|
444
|
+
ctx.fs.mkdirSync(guidefolder, { recursive: true })
|
|
445
|
+
ctx.fs.writeFileSync(Path.join(guidefolder, guideprefix + 'base-guide.aontu'), guideSrc)
|
|
446
|
+
}
|
|
397
447
|
|
|
398
|
-
const jostraca = Jostraca({
|
|
399
|
-
folder: ctx.opts.folder + '/guide',
|
|
400
|
-
now: ctx.spec.now,
|
|
401
|
-
fs: () => ctx.fs,
|
|
402
|
-
log: ctx.log,
|
|
403
|
-
})
|
|
404
448
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
449
|
+
// The entry file a project writes once and owns; the CLI prints it.
|
|
450
|
+
function guideEntrySource(guideprefix: string): string[] {
|
|
451
|
+
return [
|
|
452
|
+
'# Guide entry file: put customizations below the includes. The base guide',
|
|
453
|
+
'# it includes is generated and overwritten on every build.',
|
|
454
|
+
'@"@voxgig/apidef/model/guide.aontu"',
|
|
455
|
+
'@"./' + guideprefix + 'base-guide.aontu"',
|
|
456
|
+
]
|
|
457
|
+
}
|
|
408
458
|
|
|
409
|
-
const jres = await jostraca.generate({
|
|
410
|
-
existing: { txt: { merge: true } }
|
|
411
|
-
}, root)
|
|
412
459
|
|
|
413
|
-
|
|
460
|
+
// Written into every base guide, so a reader of the file learns where an
|
|
461
|
+
// edit belongs before making one there.
|
|
462
|
+
function baseGuideHeader(guideprefix: string): string[] {
|
|
463
|
+
return [
|
|
464
|
+
'# Generated from the API definition and overwritten on every build: do not',
|
|
465
|
+
'# edit this file. Put customizations in the guide entry file, which includes',
|
|
466
|
+
'# this one and overrides its defaults:',
|
|
467
|
+
'# ' + guideprefix + 'guide.aontu',
|
|
468
|
+
]
|
|
414
469
|
}
|
|
415
470
|
|
|
416
471
|
|
|
@@ -555,6 +610,13 @@ function validateBaseBuide(ctx: ApiDefContext, baseguide: any) {
|
|
|
555
610
|
|
|
556
611
|
|
|
557
612
|
export {
|
|
613
|
+
migrateGuideIncludes,
|
|
614
|
+
prefixGuideInclude,
|
|
615
|
+
findConflict,
|
|
616
|
+
guideConflictMessage,
|
|
617
|
+
missingGuideMessage,
|
|
618
|
+
baseGuideHeader,
|
|
619
|
+
guideEntrySource,
|
|
558
620
|
migrateLegacyGuide,
|
|
559
621
|
migrateGuideIncludePrefix,
|
|
560
622
|
buildGuide
|
package/src/guide/heuristic01.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { each } from 'jostraca'
|
|
|
8
8
|
import { size, merge, getelem, isempty, items, keysof } from '@voxgig/struct'
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
-
isEntityWrapperProp, envelopeProp, closedBodyTransform,
|
|
11
|
+
isEntityWrapperProp, envelopeProp, envelopeItemRef, closedBodyTransform,
|
|
12
12
|
authExchangeOp, specSecuredByDefault,
|
|
13
13
|
} from '../utility'
|
|
14
14
|
|
|
@@ -66,6 +66,8 @@ import { snakify } from 'jostraca'
|
|
|
66
66
|
|
|
67
67
|
import { mergeCollectionPaths } from '../transform/entity'
|
|
68
68
|
|
|
69
|
+
import { byCodePoint, countRefs, satAdd } from '../refcount'
|
|
70
|
+
|
|
69
71
|
import type {
|
|
70
72
|
PathMatch
|
|
71
73
|
} from '../utility'
|
|
@@ -77,8 +79,8 @@ const KONSOLE_LOG = console['log']
|
|
|
77
79
|
// Log non - fatal wierdness.
|
|
78
80
|
const dlog = getdlog('apidef', __filename)
|
|
79
81
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
+
// A schema whose per-use occurrences, over the method count or over the path
|
|
83
|
+
// count, fall below these rates names an entity rather than a shared shape.
|
|
82
84
|
const IS_ENTCMP_METHOD_RATE = 0.21
|
|
83
85
|
const IS_ENTCMP_PATH_RATE = 0.41
|
|
84
86
|
|
|
@@ -98,6 +100,9 @@ const METHOD_IDOP: Record<string, string> = {
|
|
|
98
100
|
OPTIONS: 'OPTIONS',
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
// Tried in order: the first shape a path matches decides how its entity is named.
|
|
104
|
+
const ENTITY_PATH_SHAPES = ['t/p/t/', 't/p/', 'p/t/', 't/', 't/p/p']
|
|
105
|
+
|
|
101
106
|
const METHOD_CONSIDER_ORDER: Record<string, number> = {
|
|
102
107
|
'GET': 100,
|
|
103
108
|
'QUERY': 150,
|
|
@@ -123,6 +128,8 @@ async function heuristic01(ctx: ApiDefContext): Promise<Guide> {
|
|
|
123
128
|
]
|
|
124
129
|
},
|
|
125
130
|
{ select: selectCmpXrefs, apply: MeasureRef },
|
|
131
|
+
{ select: selectAllMethods, apply: MeasureEnvelope },
|
|
132
|
+
MeasureEnvelopeItems,
|
|
126
133
|
{
|
|
127
134
|
select: selectAllMethods, apply: [
|
|
128
135
|
ResolveEntityComponent,
|
|
@@ -206,6 +213,7 @@ function Prepare(spec: TaskSpec) {
|
|
|
206
213
|
work: {
|
|
207
214
|
pathmap: {},
|
|
208
215
|
entmap: {},
|
|
216
|
+
envelope: {},
|
|
209
217
|
entity: {
|
|
210
218
|
count: {
|
|
211
219
|
seen: 0,
|
|
@@ -279,10 +287,11 @@ function PreparePath(spec: TaskSpec) {
|
|
|
279
287
|
|
|
280
288
|
|
|
281
289
|
function selectCmpXrefs(_source: any, spec: TaskSpec) {
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
290
|
+
const counts = countRefs(spec.ctx.def)
|
|
291
|
+
return Object.keys(counts)
|
|
292
|
+
.sort(byCodePoint)
|
|
293
|
+
.filter(val => val.match(/\/(components\/schemas|definitions)\//))
|
|
294
|
+
.map(val => ({ val, count: counts[val] }))
|
|
286
295
|
}
|
|
287
296
|
|
|
288
297
|
|
|
@@ -290,14 +299,16 @@ function MeasureRef(spec: TaskSpec) {
|
|
|
290
299
|
const guide = spec.data.guide
|
|
291
300
|
const metrics = guide.metrics
|
|
292
301
|
|
|
293
|
-
|
|
302
|
+
const xref = spec.node.val
|
|
303
|
+
let m = xref.val.match(/\/(components\/schemas|definitions)\/(.+)$/)
|
|
294
304
|
if (m) {
|
|
295
305
|
const name = canonizeCmpName(m[2])
|
|
296
306
|
if (null == metrics.count.origcmprefs[name]) {
|
|
297
307
|
metrics.count.cmp++
|
|
298
308
|
metrics.count.origcmprefs[name] = 0
|
|
299
309
|
}
|
|
300
|
-
metrics.count.origcmprefs[name]
|
|
310
|
+
metrics.count.origcmprefs[name] =
|
|
311
|
+
satAdd(metrics.count.origcmprefs[name], xref.count)
|
|
301
312
|
|
|
302
313
|
if (null == metrics.found.cmp[name]) {
|
|
303
314
|
metrics.found.cmp[name] = { orig: m[2] }
|
|
@@ -306,6 +317,44 @@ function MeasureRef(spec: TaskSpec) {
|
|
|
306
317
|
}
|
|
307
318
|
|
|
308
319
|
|
|
320
|
+
// Being an envelope belongs to the component, not to one operation: it names
|
|
321
|
+
// through the record it carries only when every operation answering with it
|
|
322
|
+
// unwraps it, so the operations on one resource are never split between the
|
|
323
|
+
// record's name and the envelope's. An operation unwraps only the response
|
|
324
|
+
// ResolveTransform reads. The entry is the item's reference, or '' for none.
|
|
325
|
+
function MeasureEnvelope(spec: TaskSpec) {
|
|
326
|
+
const work = spec.data.work
|
|
327
|
+
const mdesc = spec.node.val
|
|
328
|
+
const opname = methodOpname(mdesc, matchEntityPath(work.pathmap[mdesc.path].parts), [])
|
|
329
|
+
const unwrapref = getResponseSchema(successResponse(mdesc.responses))?.['x-ref']
|
|
330
|
+
|
|
331
|
+
for (const schema of successSchemas(mdesc.responses)) {
|
|
332
|
+
const xref = schema['x-ref']
|
|
333
|
+
if (null != xref) {
|
|
334
|
+
const itemref = null == opname || xref !== unwrapref ? null :
|
|
335
|
+
envelopeItemRef(schema, opname)
|
|
336
|
+
work.envelope[xref] = '' === work.envelope[xref] || null == itemref ? '' : itemref
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
// An item carried by more than one envelope is named by none of them: the
|
|
343
|
+
// envelopes' own names are then what tell the resources apart.
|
|
344
|
+
function MeasureEnvelopeItems(spec: TaskSpec) {
|
|
345
|
+
const envelope: Record<string, string> = spec.data.work.envelope
|
|
346
|
+
const carriers: Record<string, number> = {}
|
|
347
|
+
for (const itemref of Object.values(envelope)) {
|
|
348
|
+
carriers[itemref] = (carriers[itemref] ?? 0) + 1
|
|
349
|
+
}
|
|
350
|
+
for (const xref of Object.keys(envelope)) {
|
|
351
|
+
if (1 < carriers[envelope[xref]]) {
|
|
352
|
+
envelope[xref] = ''
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
309
358
|
function selectAllMethods(_source: any, spec: TaskSpec): MethodDesc[] {
|
|
310
359
|
const ctx = spec.ctx
|
|
311
360
|
|
|
@@ -370,9 +419,10 @@ function ResolveEntityComponent(spec: TaskSpec) {
|
|
|
370
419
|
|
|
371
420
|
let responses = methodDef.responses
|
|
372
421
|
|
|
373
|
-
let origxrefs: any[] = findPotentialSchemaRefs(
|
|
374
|
-
val
|
|
375
|
-
|
|
422
|
+
let origxrefs: any[] = findPotentialSchemaRefs(
|
|
423
|
+
pathStr, methodName, responses, work.envelope, why_cmp).map(val => ({
|
|
424
|
+
val
|
|
425
|
+
}))
|
|
376
426
|
|
|
377
427
|
let cmpxrefs = origxrefs
|
|
378
428
|
.filter(xref => xref.val.includes('schema') || xref.val.includes('definitions'))
|
|
@@ -544,25 +594,25 @@ function ResolveEntityName(spec: TaskSpec) {
|
|
|
544
594
|
|
|
545
595
|
let entname
|
|
546
596
|
|
|
547
|
-
|
|
597
|
+
const pm = matchEntityPath(parts)
|
|
548
598
|
|
|
549
|
-
if (
|
|
599
|
+
if ('t/p/t/' === pm?.expr) {
|
|
550
600
|
entname = entityPathMatch_tpte(data, pm, mdesc, why_path)
|
|
551
601
|
}
|
|
552
602
|
|
|
553
|
-
else if (
|
|
603
|
+
else if ('t/p/' === pm?.expr) {
|
|
554
604
|
entname = entityPathMatch_tpe(data, pm, mdesc, why_path)
|
|
555
605
|
}
|
|
556
606
|
|
|
557
|
-
else if (
|
|
607
|
+
else if ('p/t/' === pm?.expr) {
|
|
558
608
|
entname = entityPathMatch_pte(data, pm, mdesc, why_path)
|
|
559
609
|
}
|
|
560
610
|
|
|
561
|
-
else if (
|
|
611
|
+
else if ('t/' === pm?.expr) {
|
|
562
612
|
entname = entityPathMatch_te(data, pm, mdesc, why_path)
|
|
563
613
|
}
|
|
564
614
|
|
|
565
|
-
else if (
|
|
615
|
+
else if ('t/p/p' === pm?.expr) {
|
|
566
616
|
entname = entityPathMatch_tpp(data, pm, mdesc, why_path)
|
|
567
617
|
}
|
|
568
618
|
|
|
@@ -1004,7 +1054,7 @@ function ResolveOperation(spec: TaskSpec) {
|
|
|
1004
1054
|
|
|
1005
1055
|
|
|
1006
1056
|
if ('load' === standard_opname) {
|
|
1007
|
-
const islist = isListResponse(mdesc, pathStr, why_op)
|
|
1057
|
+
const islist = isListResponse(mdesc, ment.pm, pathStr, why_op)
|
|
1008
1058
|
opname = islist ? 'list' : opname
|
|
1009
1059
|
}
|
|
1010
1060
|
|
|
@@ -1074,8 +1124,7 @@ function ResolveTransform(spec: TaskSpec) {
|
|
|
1074
1124
|
res: undefined,
|
|
1075
1125
|
}
|
|
1076
1126
|
|
|
1077
|
-
const
|
|
1078
|
-
const resprops = getResponseSchema(resokdef)?.properties
|
|
1127
|
+
const resprops = getResponseSchema(successResponse(mdesc.responses))?.properties
|
|
1079
1128
|
debugpath(pathStr, methodName, 'TRANSFORM-RES', keysof(resprops))
|
|
1080
1129
|
|
|
1081
1130
|
if (resprops) {
|
|
@@ -1418,6 +1467,21 @@ function getRequestBodySchema(requestBody: any) {
|
|
|
1418
1467
|
requestBody?.schema
|
|
1419
1468
|
}
|
|
1420
1469
|
|
|
1470
|
+
// The response an operation's result is read from.
|
|
1471
|
+
function successResponse(responses: any): any {
|
|
1472
|
+
return responses?.[200] ?? responses?.[201]
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
// The response schemas an operation answers with when it succeeds, in the
|
|
1477
|
+
// order they are tried.
|
|
1478
|
+
function successSchemas(responses: any): any[] {
|
|
1479
|
+
return ['200', '201']
|
|
1480
|
+
.map((rescode) => getResponseSchema(responses?.[rescode]))
|
|
1481
|
+
.filter((schema) => null != schema)
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
|
|
1421
1485
|
function getResponseSchema(response: any) {
|
|
1422
1486
|
return response?.content?.['application/json']?.schema ??
|
|
1423
1487
|
response?.schema
|
|
@@ -1634,14 +1698,35 @@ function cmpOccursInPath(data: { def: any, work: any }, cmpname: string): boolea
|
|
|
1634
1698
|
|
|
1635
1699
|
|
|
1636
1700
|
|
|
1701
|
+
function matchEntityPath(parts: string[]): PathMatch | null {
|
|
1702
|
+
for (const shape of ENTITY_PATH_SHAPES) {
|
|
1703
|
+
const pm = pathMatch(parts, shape)
|
|
1704
|
+
if (null != pm) {
|
|
1705
|
+
return pm
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
return null
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
|
|
1712
|
+
// The operation ResolveOperation will assign, needed before the entity is
|
|
1713
|
+
// named: whether a response unwraps as an envelope depends on it.
|
|
1714
|
+
function methodOpname(
|
|
1715
|
+
mdesc: Record<string, any>,
|
|
1716
|
+
pm: PathMatch | null,
|
|
1717
|
+
why: string[]
|
|
1718
|
+
): string | undefined {
|
|
1719
|
+
const opname = METHOD_IDOP[mdesc.method]
|
|
1720
|
+
return 'load' === opname && isListResponse(mdesc, pm, mdesc.path, why) ? 'list' : opname
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
|
|
1637
1724
|
function isListResponse(
|
|
1638
1725
|
mdesc: Record<string, any>,
|
|
1726
|
+
pm: PathMatch | null | undefined,
|
|
1639
1727
|
pathStr: string,
|
|
1640
1728
|
why: string[]
|
|
1641
1729
|
): boolean {
|
|
1642
|
-
const ment = mdesc.MethodEntity
|
|
1643
|
-
const pm = ment.pm
|
|
1644
|
-
|
|
1645
1730
|
let islist = false
|
|
1646
1731
|
let schema
|
|
1647
1732
|
|
|
@@ -1652,8 +1737,7 @@ function isListResponse(
|
|
|
1652
1737
|
why.push('end-param')
|
|
1653
1738
|
}
|
|
1654
1739
|
else {
|
|
1655
|
-
|
|
1656
|
-
schema = getResponseSchema(response)
|
|
1740
|
+
schema = getResponseSchema(successResponse(mdesc.responses))
|
|
1657
1741
|
|
|
1658
1742
|
if (null == schema) {
|
|
1659
1743
|
why.push('no-schema')
|
|
@@ -1875,22 +1959,30 @@ function makeMethodEntityDesc(desc: Record<string, any>): MethodEntityDesc {
|
|
|
1875
1959
|
}
|
|
1876
1960
|
|
|
1877
1961
|
|
|
1878
|
-
function findPotentialSchemaRefs(
|
|
1962
|
+
function findPotentialSchemaRefs(
|
|
1963
|
+
pathStr: string,
|
|
1964
|
+
methodName: string,
|
|
1965
|
+
responses: any,
|
|
1966
|
+
envelope: Record<string, string>,
|
|
1967
|
+
why: string[],
|
|
1968
|
+
) {
|
|
1879
1969
|
const xrefs: string[] = []
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
xrefs.push(schema['x-ref'])
|
|
1970
|
+
for (const schema of successSchemas(responses)) {
|
|
1971
|
+
if (null != schema['x-ref']) {
|
|
1972
|
+
// An envelope component names its wrapping, not the entity: the
|
|
1973
|
+
// component it carries takes its place.
|
|
1974
|
+
const itemref = envelope[schema['x-ref']]
|
|
1975
|
+
if ('' !== itemref) {
|
|
1976
|
+
why.push('envelope=' + cmpRefName(schema['x-ref']))
|
|
1977
|
+
xrefs.push(itemref)
|
|
1889
1978
|
}
|
|
1890
|
-
else
|
|
1891
|
-
xrefs.push(schema
|
|
1979
|
+
else {
|
|
1980
|
+
xrefs.push(schema['x-ref'])
|
|
1892
1981
|
}
|
|
1893
1982
|
}
|
|
1983
|
+
else if ('array' === schema.type && null != schema.items?.['x-ref']) {
|
|
1984
|
+
xrefs.push(schema.items?.['x-ref'])
|
|
1985
|
+
}
|
|
1894
1986
|
}
|
|
1895
1987
|
|
|
1896
1988
|
debugpath(pathStr, methodName, 'POTENTIAL-SCHEMA-REFS', xrefs)
|
|
@@ -1898,6 +1990,12 @@ function findPotentialSchemaRefs(pathStr: string, methodName: string, responses:
|
|
|
1898
1990
|
}
|
|
1899
1991
|
|
|
1900
1992
|
|
|
1993
|
+
function cmpRefName(xref: string): string {
|
|
1994
|
+
const m = xref.match(/\/(components\/schemas|definitions)\/(.+)$/)
|
|
1995
|
+
return null == m ? xref : canonizeCmpName(m[2])
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
|
|
1901
1999
|
function hasMethod(def: any, pathStr: string, methodName: string) {
|
|
1902
2000
|
const pathDef = def?.paths?.[pathStr]
|
|
1903
2001
|
const found = (
|