@voxgig/apidef 2.4.0 → 3.0.2

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.
Files changed (96) hide show
  1. package/dist/apidef.d.ts +5 -1
  2. package/dist/apidef.js +197 -112
  3. package/dist/apidef.js.map +1 -1
  4. package/dist/builder/entity/entity.d.ts +3 -0
  5. package/dist/builder/entity/{apiEntity.js → entity.js} +12 -9
  6. package/dist/builder/entity/entity.js.map +1 -0
  7. package/dist/builder/entity/info.d.ts +3 -0
  8. package/dist/builder/entity/info.js +22 -0
  9. package/dist/builder/entity/info.js.map +1 -0
  10. package/dist/builder/entity.js +7 -21
  11. package/dist/builder/entity.js.map +1 -1
  12. package/dist/builder/flow/flowHeuristic01.js +21 -11
  13. package/dist/builder/flow/flowHeuristic01.js.map +1 -1
  14. package/dist/builder/flow.d.ts +2 -1
  15. package/dist/builder/flow.js +29 -4
  16. package/dist/builder/flow.js.map +1 -1
  17. package/dist/def.d.ts +62 -0
  18. package/dist/def.js +4 -0
  19. package/dist/def.js.map +1 -0
  20. package/dist/desc.d.ts +89 -0
  21. package/dist/desc.js +4 -0
  22. package/dist/desc.js.map +1 -0
  23. package/dist/guide/guide.d.ts +2 -1
  24. package/dist/guide/guide.js +161 -30
  25. package/dist/guide/guide.js.map +1 -1
  26. package/dist/guide/heuristic01.d.ts +2 -1
  27. package/dist/guide/heuristic01.js +1120 -234
  28. package/dist/guide/heuristic01.js.map +1 -1
  29. package/dist/model.d.ts +55 -0
  30. package/dist/model.js +4 -0
  31. package/dist/model.js.map +1 -0
  32. package/dist/parse.d.ts +1 -2
  33. package/dist/parse.js +8 -47
  34. package/dist/parse.js.map +1 -1
  35. package/dist/transform/args.d.ts +3 -0
  36. package/dist/transform/args.js +58 -0
  37. package/dist/transform/args.js.map +1 -0
  38. package/dist/transform/clean.js +27 -3
  39. package/dist/transform/clean.js.map +1 -1
  40. package/dist/transform/entity.d.ts +11 -3
  41. package/dist/transform/entity.js +57 -41
  42. package/dist/transform/entity.js.map +1 -1
  43. package/dist/transform/field.d.ts +3 -3
  44. package/dist/transform/field.js +90 -65
  45. package/dist/transform/field.js.map +1 -1
  46. package/dist/transform/operation.d.ts +1 -1
  47. package/dist/transform/operation.js +94 -296
  48. package/dist/transform/operation.js.map +1 -1
  49. package/dist/transform/select.d.ts +3 -0
  50. package/dist/transform/select.js +44 -0
  51. package/dist/transform/select.js.map +1 -0
  52. package/dist/transform/top.d.ts +9 -0
  53. package/dist/transform/top.js +11 -2
  54. package/dist/transform/top.js.map +1 -1
  55. package/dist/transform.js +4 -0
  56. package/dist/transform.js.map +1 -1
  57. package/dist/tsconfig.tsbuildinfo +1 -1
  58. package/dist/types.d.ts +112 -19
  59. package/dist/types.js +4 -2
  60. package/dist/types.js.map +1 -1
  61. package/dist/utility.d.ts +30 -2
  62. package/dist/utility.js +381 -6
  63. package/dist/utility.js.map +1 -1
  64. package/model/apidef.jsonic +75 -1
  65. package/model/guide.jsonic +14 -44
  66. package/package.json +19 -14
  67. package/src/apidef.ts +264 -121
  68. package/src/builder/entity/{apiEntity.ts → entity.ts} +18 -11
  69. package/src/builder/entity/info.ts +53 -0
  70. package/src/builder/entity.ts +9 -35
  71. package/src/builder/flow/flowHeuristic01.ts +46 -12
  72. package/src/builder/flow.ts +39 -5
  73. package/src/def.ts +91 -0
  74. package/src/desc.ts +143 -0
  75. package/src/guide/guide.ts +207 -134
  76. package/src/guide/heuristic01.ts +1651 -272
  77. package/src/model.ts +98 -0
  78. package/src/parse.ts +5 -61
  79. package/src/schematron.ts.off +317 -0
  80. package/src/transform/args.ts +102 -0
  81. package/src/transform/clean.ts +43 -8
  82. package/src/transform/entity.ts +100 -51
  83. package/src/transform/field.ts +150 -71
  84. package/src/transform/operation.ts +118 -414
  85. package/src/transform/select.ts +90 -0
  86. package/src/transform/top.ts +76 -3
  87. package/src/transform.ts +4 -0
  88. package/src/types.ts +185 -5
  89. package/src/utility.ts +481 -9
  90. package/dist/builder/entity/apiEntity.d.ts +0 -3
  91. package/dist/builder/entity/apiEntity.js.map +0 -1
  92. package/dist/builder/entity/def.d.ts +0 -3
  93. package/dist/builder/entity/def.js +0 -19
  94. package/dist/builder/entity/def.js.map +0 -1
  95. package/src/builder/entity/def.ts +0 -44
  96. package/src/guide.ts.off +0 -136
@@ -1,103 +1,285 @@
1
1
 
2
+ import { Ordu } from 'ordu'
3
+ import type { TaskSpec } from 'ordu'
2
4
 
3
- import { each, snakify, names } from 'jostraca'
4
5
 
5
- import { size } from '@voxgig/struct'
6
+ import { each } from 'jostraca'
7
+
8
+ import { size, merge, getelem, isempty, items, keysof } from '@voxgig/struct'
6
9
 
7
10
 
8
11
  import {
9
- depluralize,
10
- getdlog,
12
+ ApiDefContext,
13
+
14
+ Guide,
15
+ GuideMetrics,
16
+ GuideEntity,
17
+ GuidePath,
18
+ GuidePathAction,
19
+ GuideRenameParam,
20
+ GuidePathOp,
21
+ } from '../types'
22
+
23
+ import type {
24
+ CmpDesc,
25
+ MethodDesc,
26
+ MethodEntityDesc,
27
+ EntityDesc,
28
+ EntityPathDesc,
29
+ } from '../desc'
30
+
31
+
32
+
33
+ import type {
34
+ PathDef,
35
+ MethodDef,
36
+ } from '../def'
37
+
38
+
39
+ import {
40
+ canonize,
11
41
  capture,
42
+ debugpath,
12
43
  find,
44
+ findPathsWithPrefix,
45
+ formatJSONIC,
46
+ getdlog,
47
+ pathMatch,
48
+ warnOnError,
49
+ } from '../utility'
50
+
51
+ import type {
52
+ PathMatch
13
53
  } from '../utility'
14
54
 
15
55
 
16
- type EntityDesc = {
17
- name: string
18
- origname: string
19
- why_name?: string[]
20
- plural: string
21
- path: Record<string, EntityPathDesc>
22
- alias: Record<string, string>
23
- }
24
56
 
57
+ // Log non - fatal wierdness.
58
+ const dlog = getdlog('apidef', __filename)
59
+
60
+ // Schema components that occur less than this rate(over total method count) qualify
61
+ // as unique entities, not shared schemas
62
+ const IS_ENTCMP_METHOD_RATE = 0.21
63
+ const IS_ENTCMP_PATH_RATE = 0.41
25
64
 
26
- type EntityPathDesc = {
27
- op: Record<string, any>
28
- why_ent: string[]
65
+
66
+
67
+ const METHOD_IDOP: Record<string, string> = {
68
+ GET: 'load',
69
+ POST: 'create',
70
+ PUT: 'update',
71
+ DELETE: 'remove',
72
+ PATCH: 'patch',
73
+ HEAD: 'head',
74
+ OPTIONS: 'OPTIONS',
75
+ }
76
+
77
+ const METHOD_CONSIDER_ORDER: Record<string, number> = {
78
+ 'GET': 100,
79
+ 'POST': 200,
80
+ 'PUT': 300,
81
+ 'PATCH': 400,
82
+ 'DELETE': 500,
83
+ 'HEAD': 600,
84
+ 'OPTIONS': 700,
29
85
  }
30
86
 
31
- // Log non-fatal wierdness.
32
- const dlog = getdlog('apidef', __filename)
33
87
 
34
88
 
35
- async function heuristic01(ctx: any): Promise<Record<string, any>> {
36
- let guide = ctx.model.main.api.guide
89
+ async function heuristic01(ctx: ApiDefContext): Promise<Guide> {
37
90
 
38
- const metrics = measure(ctx)
39
- // console.dir(metrics, { depth: null })
91
+ const analysis = new Ordu({ select: { sort: true } }).add([
92
+ Prepare,
93
+ {
94
+ select: 'def.paths', apply: [
95
+ MeasurePath,
96
+ { select: '', apply: MeasureMethod },
97
+ PreparePath
98
+ ]
99
+ },
100
+ { select: selectCmpXrefs, apply: MeasureRef },
101
+ {
102
+ select: selectAllMethods, apply: [
103
+ ResolveEntityComponent,
104
+ ResolveEntityName,
105
+ RenameParams,
106
+ FindActions,
107
+ ResolveOperation,
108
+ ResolveTransform,
109
+ // ShowNode,
110
+ ]
111
+ },
112
+ { select: 'work.entmap', apply: BuildEntity }
113
+ ])
40
114
 
41
- const entityDescs = resolveEntityDescs(ctx)
115
+ const result = analysis.execSync(ctx, {})
42
116
 
43
- guide = {
44
- control: guide.control,
45
- entity: entityDescs,
117
+ if (result.err) {
118
+ throw result.err
46
119
  }
47
120
 
121
+ const guide = result.data.guide
122
+
123
+ // console.log('WORK', result.data.work)
124
+
125
+ // console.log('GUIDE')
126
+ // console.dir(guide, { depth: null })
127
+
128
+
129
+ // TODO: move to Ordu
130
+ // warnOnError('reviewEntityDescs', ctx.warn, () => reviewEntityDescs(ctx, result))
131
+
48
132
  return guide
133
+
49
134
  }
50
135
 
51
136
 
52
- function measure(ctx: any) {
53
- const metrics = {
54
- count: {
55
- schema: ({} as Record<string, number>)
56
- }
57
- }
137
+ function ShowNode(spec: TaskSpec) {
138
+ console.log('NODE', spec.node.key, spec.node.val)
139
+ }
58
140
 
59
- let xrefs = find(ctx.def, 'x-ref')
60
- // console.log('XREFS', xrefs)
61
141
 
62
- let schemas = xrefs.filter(xref => xref.val.includes('schema'))
142
+ function Prepare(spec: TaskSpec) {
143
+ const guide: Guide = {
144
+ control: {},
145
+ entity: {},
146
+ metrics: {
147
+ count: {
148
+ path: 0,
149
+ method: 0,
150
+ tag: 0,
151
+ cmp: 0,
152
+ entity: 0,
153
+ origcmprefs: {},
154
+ },
155
+ found: {
156
+ tag: {},
157
+ cmp: {},
158
+ }
159
+ },
160
+ }
63
161
 
64
- schemas.map(schema => {
65
- let m = schema.val.match(/\/components\/schemas\/(.+)$/)
66
- if (m) {
67
- const name = m[1]
68
- metrics.count.schema[name] = 1 + (metrics.count.schema[name] || 0)
162
+ Object.assign(spec.data, {
163
+ def: spec.ctx.def,
164
+ guide,
165
+ work: {
166
+ pathmap: {},
167
+ entmap: {},
168
+ entity: {
169
+ count: {
170
+ seen: 0,
171
+ unresolved: 0,
172
+ }
173
+ },
69
174
  }
70
175
  })
176
+ }
177
+
178
+
179
+ // Expects to run over paths
180
+ function MeasurePath(spec: TaskSpec) {
181
+ const guide = spec.data.guide
182
+ const metrics = guide.metrics
183
+ // const pathstr = spec.node.key
184
+ const pathdef = spec.node.val
71
185
 
186
+ metrics.count.path++
72
187
 
73
- return metrics
188
+ metrics.count.method += (
189
+ (pathdef.get ? 1 : 0) +
190
+ (pathdef.post ? 1 : 0) +
191
+ (pathdef.put ? 1 : 0) +
192
+ (pathdef.patch ? 1 : 0) +
193
+ (pathdef.delete ? 1 : 0) +
194
+ (pathdef.head ? 1 : 0) +
195
+ (pathdef.options ? 1 : 0)
196
+ )
197
+
198
+ }
199
+
200
+
201
+ // Expects to run over paths.<method>
202
+ function MeasureMethod(spec: TaskSpec) {
203
+ const guide = spec.data.guide
204
+ const metrics = guide.metrics
205
+ // const methodstr = spec.node.key
206
+ const methoddef = spec.node.val
207
+
208
+ const pathtags = methoddef.tags
209
+ if (Array.isArray(pathtags)) {
210
+ for (let tag of pathtags) {
211
+ if ('string' === typeof tag && 0 < tag.length) {
212
+ if (!metrics.found.tag[tag]) {
213
+ metrics.count.tag++
214
+ metrics.found.tag[tag] = {
215
+ name: tag,
216
+ canon: canonize(tag),
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
74
222
  }
75
223
 
76
224
 
225
+ function PreparePath(spec: TaskSpec) {
226
+ const work = spec.data.work
227
+ const pathstr = spec.node.key
228
+ const pathdef = spec.node.val
77
229
 
230
+ const pathdesc = {
231
+ path: pathstr,
232
+ def: pathdef,
233
+ parts: pathstr.split('/').filter((p: string) => '' != p),
234
+ op: {}
235
+ }
78
236
 
79
- const METHOD_IDOP: Record<string, string> = {
80
- get: 'load',
81
- post: 'create',
82
- put: 'update',
83
- patch: 'update',
84
- delete: 'remove',
237
+ work.pathmap[pathstr] = pathdesc
238
+ }
239
+
240
+
241
+ function selectCmpXrefs(_source: any, spec: TaskSpec) {
242
+ const out = find(spec.ctx.def, 'x-ref')
243
+ .filter(xref => xref.val.match(/\/(components\/schemas|definitions)\//))
244
+
245
+ // console.log('selectCmpXrefs', out)
246
+ return out
85
247
  }
86
248
 
87
249
 
88
- function resolveEntityDescs(ctx: any) {
89
- const entityDescs: Record<string, any> = {}
90
- const paths = ctx.def.paths
250
+ function MeasureRef(spec: TaskSpec) {
251
+ const guide = spec.data.guide
252
+ const metrics = guide.metrics
253
+
254
+ let m = spec.node.val.val.match(/\/(components\/schemas|definitions)\/(.+)$/)
255
+ if (m) {
256
+ const name = canonize(m[2])
257
+ if (null == metrics.count.origcmprefs[name]) {
258
+ metrics.count.cmp++
259
+ metrics.count.origcmprefs[name] = 0
260
+ }
261
+ metrics.count.origcmprefs[name]++
262
+
263
+ if (null == metrics.found.cmp[name]) {
264
+ metrics.found.cmp[name] = { orig: m[2] }
265
+ }
266
+ }
267
+ }
268
+
91
269
 
270
+ function selectAllMethods(_source: any, spec: TaskSpec): MethodDesc[] {
271
+ const ctx = spec.ctx
272
+ // const paths = ctx.def.paths
92
273
 
93
- const caught = capture(ctx.def, {
274
+ let caught = capture(ctx.def, {
94
275
  paths:
95
- ['`$SELECT`', /\/([a-zA-Z0-1_-]+)(\/\{([a-zA-Z0-1_-]+)\})?$/,
96
- ['`$SELECT`', /get|post|put|patch|delete/i,
276
+ ['`$SELECT`', /.*/,
277
+ ['`$SELECT`', /^get|post|put|patch|delete$/i,
97
278
  ['`$APPEND`', 'methods', {
98
279
  path: '`select$=key.paths`',
99
- method: { '`$LOWER`': '`$KEY`' },
280
+ method: { '`$UPPER`': '`$KEY`' },
100
281
  summary: '`.summary`',
282
+ tags: '`.tags`',
101
283
  parameters: '`.parameters`',
102
284
  responses: '`.responses`',
103
285
  requestBody: '`.requestBody`'
@@ -106,347 +288,1544 @@ function resolveEntityDescs(ctx: any) {
106
288
  ]
107
289
  })
108
290
 
291
+ // TODO: capture should return these empty objects
292
+ caught = caught ?? {}
293
+ caught.methods = caught.methods ?? []
109
294
 
110
- each(caught.methods, (pmdef) => {
111
- let methodDef = pmdef
112
- let pathStr = pmdef.path
113
- let methodStr = pmdef.method
295
+ caught.methods.sort((a: any, b: any) => {
296
+ if (a.path < b.path) {
297
+ return -1
298
+ }
299
+ else if (a.path > b.path) {
300
+ return 1
301
+ }
302
+ else if (METHOD_CONSIDER_ORDER[a.method] < METHOD_CONSIDER_ORDER[b.method]) {
303
+ return -1
304
+ }
305
+ else if (METHOD_CONSIDER_ORDER[a.method] > METHOD_CONSIDER_ORDER[b.method]) {
306
+ return 1
307
+ }
308
+ else {
309
+ return 0
310
+ }
311
+ })
114
312
 
115
- // methodStr = methodStr.toLowerCase()
116
- let why_op: string[] = []
313
+ // console.log(caught.methods.map((n: any) => n.path + ' ' + n.method))
117
314
 
118
- if (!METHOD_IDOP[methodStr]) {
119
- return
120
- }
315
+ return caught.methods || []
316
+ }
121
317
 
122
- const why_ent: string[] = []
123
- const entdesc =
124
- resolveEntity(entityDescs, pathStr, methodDef, methodStr, why_ent)
125
318
 
319
+ function ResolveEntityComponent(spec: TaskSpec) {
320
+ const guide = spec.data.guide
321
+ const metrics = guide.metrics
126
322
 
127
- if (null == entdesc) {
128
- console.log(
129
- 'WARNING: unable to resolve entity for method ' + methodStr +
130
- ' path ' + pathStr)
131
- return
132
- }
323
+ const work = spec.data.work
133
324
 
134
- entdesc.path[pathStr].why_ent = why_ent
325
+ const methodDef = spec.node.val
326
+ const methodName = methodDef.method
327
+ const pathStr = methodDef.path
135
328
 
329
+ const parts = work.pathmap[pathStr].parts
136
330
 
137
- // if (pathStr.includes('courses')) {
138
- // console.log('ENTRES', pathStr, methodStr)
139
- // console.dir(ent2, { depth: null })
140
- // }
331
+ let why_cmp: string[] = []
141
332
 
142
- let opname = resolveOpName(methodStr, methodDef, pathStr, entdesc, why_op)
333
+ let responses = methodDef.responses
143
334
 
144
- if (null == opname) {
145
- console.log(
146
- 'WARNING: unable to resolve operation for method ' + methodStr +
147
- ' path ' + pathStr)
148
- return
149
- }
335
+ let origxrefs: any[] = findPotentialSchemaRefs(pathStr, methodName, responses).map(val => ({
336
+ val
337
+ }))
150
338
 
339
+ let cmpxrefs = origxrefs
340
+ .filter(xref => xref.val.includes('schema') || xref.val.includes('definitions'))
341
+ .map(xref => {
342
+ let m = xref.val.match(/\/components\/schemas\/(.+)$/)
343
+ if (!m) {
344
+ m = xref.val.match(/\/definitions\/(.+)$/)
345
+ }
346
+ if (m) {
347
+ const cmp = canonize(m[1])
151
348
 
152
- const transform: Record<string, any> = {
153
- // reqform: '`reqdata`',
154
- // resform: '`body`',
155
- }
349
+ xref.cmp = cmp
350
+ xref.origcmp = m[1]
351
+ xref.origcmpref = cmp
352
+ }
353
+ return xref
354
+ })
355
+ .filter(xref => null != xref.cmp)
356
+
357
+ // TODO: identify non - ent schemas
358
+ .filter(xref => !xref.val.includes('Meta'))
359
+
360
+ let cleanxrefs = cmpxrefs
361
+ .map(xref => {
362
+
363
+ // Redundancy in cmp name, remove request,response suffix
364
+ // const lastPart = getelem(pathStr.split('/'), -1)
365
+ const lastPart = getelem(parts, -1)
366
+ const lastPartLower = lastPart?.toLowerCase()
367
+ const lastPartCanon = canonize(lastPart)
368
+ const origcmpLower = xref.origcmp?.toLowerCase()
369
+
370
+ if (
371
+ '' !== lastPartCanon
372
+ && (
373
+ xref.cmp === lastPartCanon + '_response'
374
+ || xref.cmp === lastPartCanon + '_request'
375
+ || origcmpLower === lastPartLower + 'response'
376
+ || origcmpLower === lastPartLower + 'request'
377
+ )
378
+ ) {
379
+ let cparts = xref.cmp.split('_')
380
+
381
+ // rec-canonize to deal with plural before removed suffix
382
+ xref.cmp = canonize(cparts.slice(0, cparts.length - 1).join('_'))
383
+ }
384
+
385
+ return xref
386
+ })
156
387
 
157
- const resokdef = methodDef.responses?.[200] || methodDef.responses?.[201]
158
- const resbody = resokdef?.content?.['application/json']?.schema
159
- if (resbody) {
160
- if (resbody[entdesc.origname]) {
161
- transform.resform = '`body.' + entdesc.origname + '`'
388
+ let goodxrefs = cleanxrefs
389
+ .filter(xref => {
390
+ if (
391
+ cleanxrefs.length <= 1
392
+ || pathStr.toLowerCase().includes('/' + xref.cmp + '/')
393
+ // || entityOccursInPath(pathStr.toLowerCase(), xref.cmp)
394
+ || entityOccursInPath(parts, xref.cmp)
395
+ ) {
396
+ return true
162
397
  }
163
- else if (resbody[entdesc.name]) {
164
- transform.resform = '`body.' + entdesc.name + '`'
398
+
399
+ // Exclude high frequency suspicious cmps as probably meta data
400
+ const cmprefs = metrics.count.origcmprefs[xref.origcmpref] ?? 0
401
+ const mcount = metrics.count.method
402
+ const pcount = metrics.count.path
403
+ const method_rate = (0 < mcount ? (cmprefs / mcount) : -1)
404
+ const path_rate = (0 < pcount ? (cmprefs / pcount) : -1)
405
+ // console.log('RCN', xref.cmp, cmprefs, mcount, method_rate, IS_ENTCMP_METHOD_RATE, method_rate < IS_ENTCMP_METHOD_RATE)
406
+ const infrequent =
407
+ method_rate < IS_ENTCMP_METHOD_RATE
408
+ || path_rate < IS_ENTCMP_PATH_RATE
409
+
410
+ if (!infrequent) {
411
+ debugpath(pathStr, methodName, 'CMP-INFREQ',
412
+ xref.val,
413
+ 'method:', method_rate, IS_ENTCMP_METHOD_RATE,
414
+ 'path:', path_rate, IS_ENTCMP_PATH_RATE
415
+ )
416
+ }
417
+
418
+ return infrequent
419
+ })
420
+
421
+ // .sort((a, b) => a.path.length - b.path.length)
422
+
423
+
424
+ const fcmp: any = goodxrefs[0]
425
+
426
+ let out: MethodEntityDesc | undefined = undefined
427
+
428
+ if (null != fcmp) {
429
+ out = makeMethodEntityDesc({
430
+ ref: fcmp.val,
431
+ cmp: fcmp.cmp,
432
+ origcmp: fcmp.origcmp,
433
+ origcmpref: fcmp.origcmpref,
434
+ entname: fcmp.cmp,
435
+ })
436
+ }
437
+
438
+ const tags = methodDef.tags ?? []
439
+ const goodtags = tags.filter((tag: any) => {
440
+ const tagdesc = metrics.found.tag[tag]
441
+ const ctag = tagdesc?.canon
442
+ return (
443
+ !!metrics.found.cmp[ctag] // tag matches a cmp
444
+ || null == fcmp // there's no cmp, so use tag
445
+ )
446
+ })
447
+
448
+ debugpath(pathStr, methodName, 'TAGS', tags, goodtags, fcmp, methodDef, metrics.found)
449
+
450
+ const ftag = goodtags[0]
451
+
452
+ if (null != ftag) {
453
+ const tagdesc = metrics.found.tag[ftag]
454
+ const tagcmp = metrics.found.cmp[tagdesc.canon]
455
+
456
+ if (tagdesc && (tagcmp || null == fcmp)) {
457
+ if (null == out) {
458
+ out = makeMethodEntityDesc({
459
+ ref: 'tag',
460
+ cmp: tagdesc.canon,
461
+ origcmp: ftag,
462
+ why_cmp,
463
+ entname: tagdesc.canon,
464
+ })
465
+ why_cmp.push('tag=' + out.cmp)
466
+ }
467
+ else if (
468
+ (pathStr.includes('/' + ftag + '/') || pathStr.includes('/' + tagdesc.canon + '/'))
469
+ && out.cmp !== tagdesc.canon
470
+ ) {
471
+ out = makeMethodEntityDesc({
472
+ ref: 'tag',
473
+ cmp: tagdesc.canon,
474
+ origcmp: ftag,
475
+ why_cmp,
476
+ entname: tagdesc.canon,
477
+ })
478
+ why_cmp.push('tag/path=' + out.cmp)
165
479
  }
166
480
  }
481
+ }
482
+
483
+ if (null != out) {
484
+ why_cmp.push('cmp/resolve=' + out.cmp)
485
+ out.why_cmp = why_cmp
486
+ out.cmpoccur = metrics.count.origcmprefs[out.origcmpref ?? ''] ?? 0
487
+ out.path_rate = 0 == metrics.count.path ? -1 : (out.cmpoccur / metrics.count.path)
488
+ out.method_rate = 0 == metrics.count.method ? -1 : (out.cmpoccur / metrics.count.method)
489
+
490
+ methodDef.MethodEntity = out
491
+ }
492
+
493
+ debugpath(pathStr, methodName, 'CMP-NAME', out, origxrefs, cleanxrefs, goodxrefs, goodtags)
494
+ }
495
+
496
+
497
+
498
+ function ResolveEntityName(spec: TaskSpec) {
499
+ const ctx = spec.ctx
500
+ const data = spec.data
501
+
502
+ const mdesc: MethodDesc = spec.node.val
503
+ const methodName = mdesc.method
504
+ const pathStr = mdesc.path
505
+
506
+ const work = spec.data.work
507
+
508
+ const pathDesc = work.pathmap[pathStr]
509
+ const parts = pathDesc.parts
510
+
511
+ work.entity.count.seen++
512
+
513
+ let ment: Partial<MethodEntityDesc>
514
+ ment = mdesc.MethodEntity
515
+
516
+ const why_path: string[] = []
517
+
518
+ if (null == ment) {
519
+ why_path.push('no-desc')
520
+ mdesc.MethodEntity = makeMethodEntityDesc({})
521
+ ment = mdesc.MethodEntity
522
+ }
523
+
524
+ why_path.push(...(ment.why_cmp ?? []))
525
+
526
+ let entname
527
+
528
+ let pm = undefined
167
529
 
168
- const reqdef = methodDef.requestBody?.content?.['application/json']?.schema?.properties
169
- if (reqdef) {
170
- if (reqdef[entdesc.origname]) {
171
- transform.reqform = { [entdesc.origname]: '`reqdata`' }
530
+ if (pm = pathMatch(parts, 't/p/t/')) {
531
+ entname = entityPathMatch_tpte(data, pm, mdesc, why_path)
532
+ }
533
+
534
+ else if (pm = pathMatch(parts, 't/p/')) {
535
+ entname = entityPathMatch_tpe(data, pm, mdesc, why_path)
536
+ }
537
+
538
+ else if (pm = pathMatch(parts, 'p/t/')) {
539
+ entname = entityPathMatch_pte(data, pm, mdesc, why_path)
540
+ }
541
+
542
+ else if (pm = pathMatch(parts, 't/')) {
543
+ entname = entityPathMatch_te(data, pm, mdesc, why_path)
544
+ }
545
+
546
+ else if (pm = pathMatch(parts, 't/p/p')) {
547
+ entname = entityPathMatch_tpp(data, pm, mdesc, why_path)
548
+ }
549
+
550
+ else {
551
+ work.entity.count.unresolved++
552
+ entname = 'entity' + work.entity.count.unresolved
553
+ }
554
+
555
+ const entdesc = work.entmap[entname] = work.entmap[entname] ?? {
556
+ name: entname,
557
+ id: 'N' + ('' + Math.random()).substring(2, 10),
558
+ op: {},
559
+ why_path,
560
+ ...ment
561
+ }
562
+
563
+ entdesc.path = (entdesc.path || {})
564
+ entdesc.path[pathStr] = entdesc.path[pathStr] || {
565
+ rename: { param: {} },
566
+ why_rename: { why_param: {} },
567
+ pm,
568
+ }
569
+ entdesc.path[pathStr].op = entdesc.path[pathStr].op || {}
570
+ entdesc.path[pathStr].why_path = why_path
571
+
572
+ ment.entname = entname
573
+ ment.pm = pm
574
+
575
+ debugpath(pathStr, methodName, 'RESOLVE-ENTITY-NAME',
576
+ formatJSONIC({ entdesc, ment }, { hsepd: 0, $: true, color: true }))
577
+ }
578
+
579
+
580
+
581
+ function RenameParams(spec: TaskSpec) {
582
+ const ctx = spec.ctx
583
+ const data = spec.data
584
+ const guide = data.guide
585
+ const metrics = guide.metrics
586
+
587
+ const mdesc = spec.node.val
588
+ const ment = mdesc.MethodEntity
589
+
590
+ const pathStr = mdesc.path
591
+ const work = spec.data.work
592
+
593
+ const entname = mdesc.MethodEntity.entname
594
+ const entdesc = work.entmap[entname]
595
+
596
+ const pathdesc = spec.data.work.pathmap[pathStr]
597
+
598
+ const methodName = mdesc.method
599
+
600
+ // Rewrite path parameters that are identifiers to follow the rules:
601
+ // 0. Parameters named [a-z]?id are considered identifiers
602
+ // 1. last identifier is always {id} as this is the primary entity
603
+ // 2. internal identifiers are formatted as {name_id} where name is the parent entity name
604
+ // Example: /api/bar/{id}/zed/{zid}/foo/{fid} ->
605
+ // /api/bar/{bar_id}/zed/{zed_id}/foo/{id}
606
+
607
+ // id needs to be t/p/
608
+ const multParamEndMatch = pathMatch(mdesc.path, 'p/p/')
609
+ if (multParamEndMatch) {
610
+ return
611
+ }
612
+
613
+
614
+ const pathDesc = entdesc.path[pathStr]
615
+ pathDesc.rename = (pathDesc.rename ?? { param: {} })
616
+ pathDesc.why_rename = (pathDesc.why_rename ?? { why_param: {} })
617
+
618
+ pathDesc.action = (pathDesc.action ?? {})
619
+ pathDesc.why_action = (pathDesc.why_action ?? {})
620
+
621
+ const paramRenameCapture = {
622
+ rename: pathDesc.rename.param = (pathDesc.rename.param ?? {}),
623
+ why: pathDesc.why_rename.why_param = (pathDesc.why_rename.why_param ?? {}),
624
+ }
625
+ const parts = pathdesc.parts
626
+
627
+ const cmpname = mdesc.cmp
628
+ const considerCmp =
629
+ null != cmpname &&
630
+ 0 < metrics.count.uniqschema &&
631
+ mdesc.method_rate < IS_ENTCMP_METHOD_RATE
632
+
633
+ const origParams = []
634
+
635
+ for (let partI = 0; partI < parts.length; partI++) {
636
+ let partStr = parts[partI]
637
+
638
+ if (isParam(partStr)) {
639
+ origParams.push(partStr.replace(/[\}\{\*]/g, ''))
640
+
641
+ const why = []
642
+
643
+ const oldParam = partStr.substring(1, partStr.length - 1)
644
+ paramRenameCapture.why[oldParam] = (paramRenameCapture.why[oldParam] ?? [])
645
+
646
+ const lastPart = partI === parts.length - 1
647
+ const secondLastPart = partI === parts.length - 2
648
+ const notLastPart = partI < parts.length - 1
649
+ const hasParent = 0 < partI && !isParam(parts[partI - 1])
650
+ const parentName = hasParent ? canonize(parts[partI - 1]) : null
651
+ const not_exact_id = 'id' !== oldParam
652
+ const probably_an_id =
653
+ oldParam.endsWith('id')
654
+ || oldParam.endsWith('Id')
655
+ || canonize(oldParam) === parentName
656
+
657
+ debugpath(pathStr, mdesc.method, 'RENAME-PARAM-PART', parts, partI, partStr, {
658
+ lastPart,
659
+ secondLastPart,
660
+ notLastPart,
661
+ hasParent,
662
+ parentName,
663
+ not_exact_id,
664
+ probably_an_id,
665
+ })
666
+
667
+ // Id-like not at end, and after a possible entname.
668
+ // .../parentent/{id}/...
669
+ if (
670
+ probably_an_id
671
+ && hasParent
672
+ && notLastPart
673
+ ) {
674
+ why.push('maybe-parent')
675
+
676
+ // actually an action
677
+ if (
678
+ secondLastPart
679
+ && (
680
+ (
681
+ parentName !== entdesc.name
682
+ && entdesc.name.startsWith(parentName + '_')
683
+ )
684
+ // || parentName === cmp.name
685
+ || parentName === cmpname
686
+ )
687
+ ) {
688
+ // let newParamName = 'id'
689
+ updateParamRename(
690
+ ctx, data, pathStr, methodName, paramRenameCapture, oldParam,
691
+ 'id', 'action-parent:' + entdesc.name)
692
+ why.push('action')
693
+
694
+ updateAction(methodName, oldParam,
695
+ parts[partI + 1], entdesc, pathDesc, 'action-not-parent')
696
+ }
697
+
698
+ else if (hasParent && parentName === cmpname) {
699
+ updateParamRename(
700
+ ctx, data, pathStr, methodName, paramRenameCapture, oldParam,
701
+ 'id', 'id-parent-cmp')
702
+ why.push('id-parent-cmp')
703
+ }
704
+
705
+ else if (hasParent && parentName === entdesc.name) {
706
+ updateParamRename(
707
+ ctx, data, pathStr, methodName, paramRenameCapture, oldParam,
708
+ 'id', 'id-parent-ent')
709
+ why.push('id-parent-ent')
710
+ }
711
+
712
+ else {
713
+ updateParamRename(
714
+ ctx, data, pathStr, methodName, paramRenameCapture, oldParam,
715
+ parentName + '_id', 'parent:' + parentName)
716
+ why.push('parent')
717
+ }
172
718
  }
173
- else if (reqdef[entdesc.origname]) {
174
- transform.reqform = { [entdesc.origname]: '`reqdata`' }
719
+
720
+ // /api/foo/{foo}/bar/...
721
+ // param matches parent entname, but is not _id format
722
+
723
+
724
+ // At end, but not called id.
725
+ // .../ent/{not-id}
726
+ else if (
727
+ lastPart
728
+ && not_exact_id
729
+ && (!hasParent
730
+ || (
731
+ parentName === entdesc.name
732
+ || entdesc.name.endsWith('_' + parentName)
733
+ )
734
+ )
735
+ && (!considerCmp || cmpname === entdesc.name)
736
+ ) {
737
+ updateParamRename(
738
+ ctx, data, pathStr, methodName, paramRenameCapture, oldParam,
739
+ 'id', 'end-id;' + methodName + ';parent=' + hasParent + '/' + parentName +
740
+ ';cmp=' + considerCmp + (null == cmpname ? '' : '/' + cmpname))
741
+ why.push('end-id')
175
742
  }
176
743
 
744
+ // Mot at end, has preceding non-param part.
745
+ // .../parentent/{paramname}/...
746
+ else if (
747
+ notLastPart
748
+ && 1 < partI
749
+ && hasParent
750
+ ) {
751
+ why.push('has-parent')
752
+
753
+ // Actually primary ent with an action$ suffix
754
+ if (
755
+ secondLastPart
756
+ ) {
757
+ why.push('second-last')
758
+
759
+ if (
760
+ 'id' !== oldParam
761
+ // && fixEntName(partStr) === entdesc.name
762
+ && canonize(partStr) === entdesc.name
763
+ ) {
764
+ updateParamRename(
765
+ ctx, data, pathStr, methodName, paramRenameCapture, oldParam,
766
+ 'id', 'end-action')
767
+ why.push('end-action')
768
+
769
+ updateAction(methodName, oldParam,
770
+ parts[partI + 1], entdesc, pathDesc, 'end-action')
771
+ }
772
+ else {
773
+ why.push('not-end-action')
774
+ }
775
+ }
776
+
777
+ // Primary ent id not at end!
778
+ else if (
779
+ hasParent
780
+ && parentName === cmpname
781
+ ) {
782
+ updateParamRename(
783
+ ctx, data, pathStr, methodName, paramRenameCapture, oldParam,
784
+ 'id', 'id-not-last')
785
+
786
+ why.push('id-not-last')
787
+ // paramRenames[oldParam] = 'id'
788
+ // paramRenamesWhy[oldParam].push('id-not-last')
789
+ }
790
+
791
+ // Not primary ent.
792
+ else {
793
+ why.push('default')
794
+
795
+ let newParamName = parentName + '_id'
796
+ if (newParamName != oldParam) {
797
+ updateParamRename(
798
+ ctx, data, pathStr, methodName, paramRenameCapture, oldParam,
799
+ newParamName, 'not-primary')
800
+ why.push('not-primary')
801
+
802
+ // paramRenames[oldParam] = newParamName
803
+ // paramRenamesWhy[oldParam].push('not-primary')
804
+ }
805
+ }
806
+ }
807
+
808
+ why.push('done')
809
+
810
+ if (paramRenameCapture.rename[oldParam] === oldParam) {
811
+ why.push('delete-dup')
812
+ delete paramRenameCapture.rename[oldParam]
813
+ delete paramRenameCapture.why[oldParam]
814
+ }
815
+
816
+ // TODO: these need to done via an API
817
+ debugpath(pathStr, methodName, 'RENAME-PARAM',
818
+ {
819
+ pathStr,
820
+ methodName,
821
+ partStr,
822
+ why,
823
+ oldParam,
824
+ lastPart,
825
+ secondLastPart,
826
+ notLastPart,
827
+ hasParent,
828
+ parentName,
829
+ not_exact_id,
830
+ probably_an_id,
831
+ considerCmp,
832
+ cmp: mdesc.cmp,
833
+ cmpname,
834
+ paramRenameCapture,
835
+ entdesc
836
+ }
837
+ )
177
838
  }
839
+ }
840
+
841
+ ment.rename = paramRenameCapture.rename
842
+ ment.why_rename = paramRenameCapture.why
843
+ ment.rename_orig = origParams
844
+ }
845
+
846
+
847
+ function FindActions(spec: TaskSpec) {
848
+ const mdesc = spec.node.val
849
+ const pathStr = mdesc.path
850
+ const work = spec.data.work
851
+
852
+ const ment = mdesc.MethodEntity
853
+ const entname = ment.entname
854
+ const entdesc = work.entmap[entname]
855
+
856
+ // const pathdesc = spec.data.work.pathmap[pathStr]
857
+ const pathdesc = entdesc.path[pathStr]
858
+
859
+ const methodName = mdesc.method
860
+
861
+ pathdesc.action = (pathdesc.action ?? {})
862
+ pathdesc.why_action = (pathdesc.why_action ?? {})
863
+
864
+ const parts = spec.data.work.pathmap[pathStr].parts
178
865
 
179
- const op = entdesc.path[pathStr].op
866
+ const fourthLastPart = parts[parts.length - 4]
867
+ const fourthLastPartCanon = canonize(fourthLastPart)
868
+ const thirdLastPart = parts[parts.length - 3]
869
+ const thirdLastPartCanon = canonize(thirdLastPart)
870
+ const secondLastPart = parts[parts.length - 2]
871
+ const secondLastPartCanon = canonize(secondLastPart)
872
+ const lastPart = parts[parts.length - 1]
873
+ const lastPartCanon = canonize(lastPart)
180
874
 
181
- op[opname] = {
182
- // TODO: in actual guide, remove "standard" method ops since redundant
183
- method: methodStr,
184
- why_op: why_op.join(';')
875
+ const cmp = ment.cmp
876
+
877
+ // /api/foo/bar where foo is the entity and bar is the action, no id param
878
+ if (
879
+ secondLastPartCanon === cmp
880
+ || secondLastPartCanon === ment.origcmp
881
+ || secondLastPartCanon === entname
882
+ ) {
883
+ if (!isParam(lastPart)) {
884
+ updateAction(methodName, lastPart, lastPartCanon, entdesc, pathdesc, 'no-param')
185
885
  }
886
+ }
186
887
 
187
- if (0 < Object.entries(transform).length) {
188
- op[opname].transform = transform
888
+ // /api/foo/{param}/action
889
+ else if (
890
+ thirdLastPartCanon === cmp
891
+ || thirdLastPartCanon === ment.origcmp
892
+ || thirdLastPartCanon === entname
893
+ ) {
894
+ if (isParam(secondLastPart) && !isParam(lastPart)) {
895
+ updateAction(methodName, lastPart, lastPartCanon, entdesc, pathdesc,
896
+ 'ent-param-2nd-last')
189
897
  }
898
+ }
190
899
 
191
- // if ('/v2/users/{user_id}/enrollment' === pathStr) {
192
- // console.log('ENT')
193
- // console.dir(entdesc, { depth: null })
194
- // }
195
- // })
196
- // }
197
- })
900
+ // /api/foo/{param}/action/subaction
901
+ else if (
902
+ fourthLastPartCanon === cmp
903
+ || fourthLastPartCanon === ment.origcmp
904
+ || fourthLastPartCanon === entname
905
+ ) {
906
+ if (isParam(thirdLastPart) && !isParam(secondLastPart) && !isParam(lastPart)) {
907
+ const oldActionName = secondLastPart + '/' + lastPart
908
+ const actionName = secondLastPartCanon + '_' + lastPartCanon
909
+ updateAction(methodName, oldActionName, actionName, entdesc, pathdesc,
910
+ 'ent-param-3rd-last')
911
+ }
912
+ }
198
913
 
199
- // console.log('USER')
200
- // console.dir(entityDescs.user, { depth: null })
914
+ debugpath(pathStr, methodName, 'FIND-ACTIONS', cmp, parts, pathdesc.action, pathdesc.why_action)
201
915
 
202
- return entityDescs
916
+ // return pathdesc.action
203
917
  }
204
918
 
205
919
 
206
- function resolveEntity(
207
- entityDescs: Record<string, EntityDesc>,
208
- // pathDef: Record<string, any>,
209
- pathStr: string,
210
- methodDef: Record<string, any>,
211
- methodStr: string,
212
- why_ent: string[]
213
- ): EntityDesc | undefined {
920
+ function ResolveOperation(spec: TaskSpec) {
921
+ const mdesc: MethodDesc = spec.node.val
922
+ const ment: MethodEntityDesc = mdesc.MethodEntity
214
923
 
215
- let entdesc: EntityDesc
216
- let entname: string = ''
217
- let origentname: string = ''
924
+ const pathStr = mdesc.path
925
+ const work = spec.data.work
218
926
 
219
- const why_name: string[] = []
927
+ const parts: string[] = work.pathmap[pathStr].parts
220
928
 
221
- const m = pathStr.match(/\/([a-zA-Z0-1_-]+)(\/\{([a-zA-Z0-1_-]+)\})?$/)
222
- if (m) {
223
- let pathName = m[1]
224
- let pathParam = m[3]
929
+ const entname = mdesc.MethodEntity.entname
930
+ const entdesc = work.entmap[entname]
931
+
932
+ const methodName = mdesc.method
933
+
934
+ const why_op: string[] = ment.why_op = []
935
+
936
+ let opname = METHOD_IDOP[methodName]
937
+ let standard_opname = opname
938
+
939
+ if (null == opname) {
940
+ why_op.push('no-op:' + methodName)
941
+ return
942
+ }
943
+
944
+ // REVIEW: using POST and PUT in non-restian ways is too wierd to handle consistently
945
+ // correct using guide customizations
946
+
947
+ // Sometimes POST is used to update, not create. Attempt to identify this.
948
+ // And sometimes vice versa for PUT
949
+ // const id_param_offset = ment.pm?.expr?.endsWith('/t/') ? 1 : 0
950
+ // const has_end_id_param =
951
+ // entname == canonize(parts[parts.length - 2 - id_param_offset])
952
+ // && parts[parts.length - 1 - id_param_offset]?.toLowerCase().endsWith('id}')
953
+
954
+
955
+ if ('load' === standard_opname) {
956
+ const islist = isListResponse(mdesc, pathStr, why_op)
957
+ opname = islist ? 'list' : opname
958
+ }
959
+
960
+ /*
961
+ else if (
962
+ 'create' === standard_opname
963
+ && has_end_id_param
964
+ ) {
965
+ opname = 'update'
966
+ why_op.push('id-present')
967
+ }
968
+
969
+ else if (
970
+ 'update' === standard_opname
971
+ && !has_end_id_param
972
+ ) {
973
+ opname = 'create'
974
+ why_op.push('no-id-present')
975
+ }
976
+ */
977
+
978
+
979
+ else {
980
+ why_op.push('not-load')
981
+ }
982
+
983
+ // why.push('ent=' + entdesc.name)
984
+
985
+ ment.opname = opname
986
+ ment.why_opname = why_op
987
+
988
+ const op = entdesc.path[pathStr].op
989
+
990
+ const opdef = {
991
+ method: methodName,
992
+ why_op: why_op.join(';')
993
+ }
994
+
995
+ if (null == op[opname]) {
996
+ op[opname] = opdef
997
+ }
998
+
999
+ // Conflicting methods for same operation
1000
+ // METHOD_CONSIDER_ORDER wins
1001
+ // Add operation using method name
1002
+ else {
1003
+ op[methodName.toLowerCase()] = opdef
1004
+ }
1005
+
1006
+ debugpath(pathStr, methodName, 'ResolveOperation', standard_opname, opname, why_op, op)
1007
+ }
1008
+
1009
+
1010
+ function ResolveTransform(spec: TaskSpec) {
1011
+ const mdesc = spec.node.val
1012
+ const ment = mdesc.MethodEntity
225
1013
 
1014
+ const pathStr = mdesc.path
1015
+ const work = spec.data.work
226
1016
 
227
- origentname = snakify(pathName)
228
- entname = depluralize(origentname)
1017
+ const entname = mdesc.MethodEntity.entname
1018
+ const entdesc = work.entmap[entname]
229
1019
 
230
- // Check schema
231
- const compname = resolveComponentName(entname, methodDef, methodStr, pathStr, why_name)
232
- if (compname) {
233
- origentname = snakify(compname)
234
- entname = depluralize(origentname)
235
- why_ent.push('cmp:' + entname)
1020
+ // const pathdesc = spec.data.work.pathmap[pathStr]
1021
+ const pathdesc = entdesc.path[pathStr]
1022
+
1023
+ const methodName = mdesc.method
1024
+
1025
+ const opname = ment.opname
1026
+
1027
+ const op = pathdesc.op
1028
+
1029
+ const transform: Record<string, any> = {
1030
+ req: undefined,
1031
+ res: undefined,
1032
+ }
1033
+
1034
+ const resokdef = mdesc.responses?.[200] || mdesc.responses?.[201]
1035
+ const resprops = getResponseSchema(resokdef)?.properties
1036
+ debugpath(pathStr, methodName, 'TRANSFORM-RES', keysof(resprops))
1037
+
1038
+ if (resprops) {
1039
+ if (resprops[entdesc.origname]) {
1040
+ transform.res = '`body.' + entdesc.origname + '`'
236
1041
  }
237
- else {
238
- why_ent.push('path:' + m[1])
239
- why_name.push('path:' + m[1])
1042
+ else if (resprops[entdesc.name]) {
1043
+ transform.res = '`body.' + entdesc.name + '`'
240
1044
  }
1045
+ }
241
1046
 
242
- entdesc = (entityDescs[entname] = entityDescs[entname] || {
243
- name: entname,
244
- id: Math.random(),
245
- alias: {}
246
- })
1047
+ const reqprops = getRequestBodySchema(mdesc.requestBody)
1048
+ debugpath(pathStr, methodName, 'TRANSFORM-REQ', keysof(reqprops))
1049
+ if (reqprops) {
1050
+ if (reqprops[entdesc.origname]) {
1051
+ transform.req = { [entdesc.origname]: '`reqdata`' }
1052
+ }
1053
+ else if (reqprops[entdesc.origname]) {
1054
+ transform.req = { [entdesc.origname]: '`reqdata`' }
1055
+ }
247
1056
 
248
- if (null != pathParam) {
249
- const pathParamCanon = snakify(pathParam)
250
- if ('id' != pathParamCanon) {
251
- entdesc.alias.id = pathParamCanon
252
- entdesc.alias[pathParamCanon] = 'id'
1057
+ }
1058
+
1059
+ if (!isempty(transform)) {
1060
+ op[opname].transform = transform
1061
+ }
1062
+ }
1063
+
1064
+
1065
+
1066
+ function BuildEntity(spec: TaskSpec) {
1067
+ const entdesc = spec.node.val
1068
+ // console.log('BUILD-ENTITY')
1069
+ // console.dir(entdesc, { depth: null })
1070
+
1071
+ const guide: Guide = spec.data.guide
1072
+ guide.metrics.count.entity++
1073
+
1074
+ const entityMap: Record<string, GuideEntity> = guide.entity
1075
+
1076
+ const path: Record<string, GuidePath> = {}
1077
+
1078
+ const rename_param = (pathdesc: any) => {
1079
+ // console.log('RENAME-PATHDESC', pathdesc)
1080
+ const out: Record<string, GuideRenameParam> = {}
1081
+ each(pathdesc.rename.param, (item: any) => {
1082
+ out[item.key$] = {
1083
+ target: item.val$,
1084
+ why_rename: pathdesc.why_rename.why_param[item.key$]
253
1085
  }
1086
+ })
1087
+ return out
1088
+ }
1089
+
1090
+ each(entdesc.path, (pathdesc: any, pathstr: string) => {
1091
+ const guidepath = {
1092
+ why_path: pathdesc.why_path,
1093
+ action: pathdesc.action,
1094
+ rename: {
1095
+ param: rename_param(pathdesc)
1096
+ },
1097
+ op: pathdesc.op
254
1098
  }
1099
+ path[pathstr] = guidepath
1100
+ })
1101
+
1102
+ entityMap[entdesc.name] = {
1103
+ name: entdesc.name,
1104
+ orig: entdesc.origcmp,
1105
+ path,
255
1106
  }
256
1107
 
257
- // Can't figure out the entity
1108
+ }
1109
+
1110
+
1111
+
1112
+
1113
+
1114
+
1115
+
1116
+ function entityPathMatch_tpte(
1117
+ data: { def: any, guide: any, work: any },
1118
+ pm: PathMatch,
1119
+ mdesc: any,
1120
+ why: string[]
1121
+ ) {
1122
+ const ment = mdesc.MethodEntity
1123
+
1124
+ const pathNameIndex = 2
1125
+
1126
+ why.push('path=t/p/t/')
1127
+ const origPathName = pm[pathNameIndex]
1128
+ let entname = canonize(origPathName)
1129
+ let ecm = undefined
1130
+
1131
+ if (null != ment.cmp) {
1132
+ ecm = entityCmpMatch(data, entname, mdesc, why)
1133
+ entname = ecm.name
1134
+ why.push('has-cmp=' + ecm.orig)
1135
+ }
1136
+
1137
+ else if (probableEntityMethod(data, ment, pm, why)) {
1138
+ ecm = entityCmpMatch(data, entname, mdesc, why)
1139
+ if (ecm.cmpish) {
1140
+ entname = ecm.name
1141
+ why.push('prob-ent=' + ecm.orig)
1142
+ }
1143
+ else if (endsWithCmp(data, pm)) {
1144
+ entname = canonize(getelem(pm, -1))
1145
+ why.push('prob-ent-last=' + ecm.orig)
1146
+ }
1147
+ else if (0 < findPathsWithPrefix(data, pm.path, { strict: true })) {
1148
+ entname = canonize(getelem(pm, -1))
1149
+ why.push('prob-ent-prefix=' + ecm.orig)
1150
+ }
1151
+ else {
1152
+ entname = canonize(getelem(pm, -3)) + '_' + entname
1153
+ why.push('prob-ent-part')
1154
+ }
1155
+ }
1156
+
1157
+ // Probably an entity action suffix
258
1158
  else {
259
- console.log('NO ENTTIY', pathStr)
260
- return
1159
+ why.push('prob-ent-act')
1160
+ entname = canonize(getelem(pm, -3))
261
1161
  }
262
1162
 
1163
+ return entname
1164
+ }
263
1165
 
264
- // entdesc.plural = origentname
265
- entdesc.origname = origentname
1166
+ function endsWithCmp(data: any, pm: PathMatch) {
1167
+ const last = canonize(getelem(pm, -1))
1168
+ return isOrigCmp(data, last)
1169
+ }
266
1170
 
267
- names(entdesc, entname)
268
1171
 
269
- entdesc.alias = entdesc.alias || {}
1172
+ function isOrigCmp(data: any, name: string) {
1173
+ return null != data.metrics.count.origcmprefs[name]
1174
+ }
270
1175
 
271
- entdesc.path = (entdesc.path || {})
272
- entdesc.path[pathStr] = entdesc.path[pathStr] || {}
273
- entdesc.path[pathStr].op = entdesc.path[pathStr].op || {}
274
1176
 
275
- if (null == entdesc.why_name) {
276
- entdesc.why_name = why_name
1177
+ function entityOccursInPath(parts: string[], entname: string) {
1178
+ let partsLower = parts.map(p => p.toLowerCase())
1179
+ partsLower = partsLower.filter(p => '{' !== p[0]).map(p => canonize(p))
1180
+ return !partsLower.reduce((a: boolean, p: string) => (a && p !== entname), true)
1181
+ }
1182
+
1183
+
1184
+ function entityPathMatch_tpe(
1185
+ data: { def: any, guide: any, work: any },
1186
+ pm: PathMatch, mdesc: any, why: string[]
1187
+ ) {
1188
+ const ment = mdesc.MethodEntity
1189
+ const pathNameIndex = 0
1190
+
1191
+ why.push('path=t/p/')
1192
+ const origPathName = pm[pathNameIndex]
1193
+ // let entname = fixEntName(origPathName)
1194
+ let entname = canonize(origPathName)
1195
+
1196
+ if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
1197
+ let ecm = entityCmpMatch(data, entname, mdesc, why)
1198
+ entname = ecm.name
1199
+ }
1200
+ else {
1201
+ why.push('ent-act')
1202
+ }
1203
+
1204
+ return entname
1205
+ }
1206
+
1207
+
1208
+ function entityPathMatch_pte(
1209
+ data: { def: any, guide: any, work: any },
1210
+ pm: PathMatch, mdesc: any, why: string[]
1211
+ ) {
1212
+ const ment = mdesc.MethodEntity
1213
+ const pathNameIndex = 1
1214
+
1215
+ why.push('path=p/t/')
1216
+ const origPathName = pm[pathNameIndex]
1217
+ let entname = canonize(origPathName)
1218
+
1219
+ if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
1220
+ let ecm = entityCmpMatch(data, entname, mdesc, why)
1221
+ entname = ecm.name
1222
+ }
1223
+ else {
1224
+ why.push('ent-act')
277
1225
  }
278
1226
 
279
- return entdesc
1227
+ return entname
280
1228
  }
281
1229
 
282
1230
 
283
- const REQKIND: any = {
284
- get: 'res',
285
- post: 'req',
286
- put: 'req',
287
- patch: 'req',
1231
+ function entityPathMatch_te(
1232
+ data: { def: any, guide: any, work: any },
1233
+ pm: PathMatch, mdesc: any, why: string[]
1234
+ ) {
1235
+ const ment = mdesc.MethodEntity
1236
+ const pathNameIndex = 0
1237
+
1238
+ why.push('path=t/')
1239
+ const origPathName = pm[pathNameIndex]
1240
+ // let entname = fixEntName(origPathName)
1241
+ let entname = canonize(origPathName)
1242
+
1243
+ if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
1244
+ let ecm = entityCmpMatch(data, entname, mdesc, why)
1245
+ entname = ecm.name
1246
+ }
1247
+ else {
1248
+ why.push('ent-act')
1249
+ }
1250
+
1251
+ return entname
288
1252
  }
289
1253
 
290
1254
 
291
- function resolveComponentName(
292
- entname: string,
293
- methodDef: Record<string, any>,
294
- methodStr: string,
295
- pathStr: string,
296
- why_name: string[]
297
- ): string | undefined {
298
- let compname: string | undefined = undefined
1255
+ function entityPathMatch_tpp(
1256
+ data: { def: any, guide: any, work: any },
1257
+ pm: PathMatch, mdesc: any, why: string[]
1258
+ ) {
1259
+ const ment = mdesc.MethodEntity
1260
+ const pathNameIndex = 0
299
1261
 
300
- let xrefs = find(methodDef, 'x-ref')
301
- .filter(xref => xref.val.includes('schema'))
1262
+ why.push('path=t/p/p')
1263
+ const origPathName = pm[pathNameIndex]
1264
+ // let entname = fixEntName(origPathName)
1265
+ let entname = canonize(origPathName)
302
1266
 
303
- // TODO: identify non-ent schemas
304
- .filter(xref => !xref.val.includes('Meta'))
1267
+ if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
1268
+ let ecm = entityCmpMatch(data, entname, mdesc, why)
1269
+ entname = ecm.name
1270
+ }
1271
+ else {
1272
+ why.push('ent-act')
1273
+ }
305
1274
 
306
- .sort((a, b) => a.path.length - b.path.length)
1275
+ return entname
1276
+ }
307
1277
 
308
- // console.log('RCN', pathStr, methodStr, xrefs.map(x => [x.val, x.path.length]))
309
1278
 
310
- let first = xrefs[0]?.val
1279
+ function getRequestBodySchema(requestBody: any) {
1280
+ return requestBody?.content?.['application/json']?.schema ??
1281
+ requestBody?.schema
1282
+ }
311
1283
 
312
- if (null != first) {
313
- let xrefm = (first as string).match(/\/components\/schemas\/(.+)$/)
314
- if (xrefm) {
315
- why_name.push('cmp')
316
- compname = xrefm[1]
317
- }
1284
+ function getResponseSchema(response: any) {
1285
+ return response?.content?.['application/json']?.schema ??
1286
+ response?.schema
1287
+ }
1288
+
1289
+
1290
+ // No entity component was found, but there still might be an entity.
1291
+ function probableEntityMethod(
1292
+ data: { def: any },
1293
+ mdesc: any,
1294
+ pm: PathMatch,
1295
+ why: string[]
1296
+ ) {
1297
+ const request = mdesc.requestBody
1298
+ const reqSchema = getRequestBodySchema(request)
1299
+
1300
+ const response = mdesc.responses?.['201'] || mdesc.responses?.['200']
1301
+ const resSchema = getResponseSchema(response)
1302
+ const noResponse = null == resSchema && null != mdesc.responses?.['204']
1303
+
1304
+ let prob_why = ''
1305
+
1306
+ let probent = false
1307
+
1308
+ if (noResponse) {
1309
+ // No response at all means not an action, thus probably an entity.
1310
+ prob_why = 'nores'
1311
+ probent = true
318
1312
  }
319
1313
 
320
- if (null != compname) {
321
- compname = depluralize(snakify(compname))
1314
+ else if (null != reqSchema) {
1315
+ if (
1316
+ 'POST' === mdesc.method
1317
+ && !pm.expr.endsWith('/p/')
1318
+
1319
+ // A real entity would probably occur in at least one other t/p path
1320
+ // otherwise this is probably an action
1321
+ && (1 < Object.keys(data.def.paths).filter(path =>
1322
+ path.includes('/' + pm[pm.length - 1] + '/')).length)
1323
+ ) {
1324
+ prob_why = 'post'
1325
+ probent = true
1326
+ }
322
1327
 
323
- // Assume sub schemas suffixes are not real entities
324
- if (compname.includes(entname)) {
325
- compname = compname.slice(0, compname.indexOf(entname) + entname.length)
1328
+ else if (
1329
+ ('PUT' === mdesc.method || 'PATCH' === mdesc.method)
1330
+ && pm.expr.endsWith('/p/')
1331
+ ) {
1332
+ prob_why = 'putish'
1333
+ probent = true
326
1334
  }
327
1335
  }
1336
+ else if ('GET' === mdesc.method) {
1337
+ prob_why = 'get'
1338
+ probent = true
1339
+ }
328
1340
 
329
- return compname
330
- }
1341
+ const rescodes = Object.keys(mdesc.responses ?? {})
331
1342
 
1343
+ debugpath(mdesc.path, mdesc.method, 'PROBABLE-ENTITY-RESPONSE',
1344
+ { mdesc, responses: rescodes, probent, prob_why })
332
1345
 
333
- function resolveOpName(
334
- methodStr: string,
335
- methodDef: any,
336
- pathStr: string,
337
- entdesc: EntityDesc,
338
- why: string[]
339
- )
340
- : string | undefined {
341
- // console.log('ROP', pathStr, methodDef)
1346
+ why.push('entres=' + probent + '/' + rescodes + ('' === prob_why ? '' : '/' + prob_why))
342
1347
 
1348
+ return probent
1349
+ }
343
1350
 
344
- let opname = METHOD_IDOP[methodStr]
345
- if (null == opname) {
346
- why.push('no-op:' + methodStr)
347
- return
1351
+
1352
+ function entityCmpMatch(
1353
+ data: { def: any, guide: any, work: any },
1354
+ entname: string,
1355
+ mdesc: any,
1356
+ why: string[]
1357
+ ): {
1358
+ name: string,
1359
+ orig: string,
1360
+ cmpish: boolean,
1361
+ pathish: boolean,
1362
+ } {
1363
+ const ment = mdesc.MethodEntity
1364
+
1365
+ let out = {
1366
+ name: entname,
1367
+ orig: ment.origcmp ?? entname,
1368
+ cmpish: false,
1369
+ pathish: true,
348
1370
  }
349
1371
 
350
- if ('load' === opname) {
351
- const islist = isListResponse(methodDef, pathStr, entdesc, why)
352
- opname = islist ? 'list' : opname
1372
+ // console.log('ECM-A', out, ment)
1373
+
1374
+ const cmpInfrequent = (
1375
+ ment.method_rate < IS_ENTCMP_METHOD_RATE
1376
+ || ment.path_rate < IS_ENTCMP_PATH_RATE
1377
+ )
1378
+
1379
+ if (
1380
+ null != ment.cmp
1381
+ && entname != ment.cmp
1382
+ && !ment.cmp.startsWith(entname)
1383
+ ) {
1384
+ if (cmpInfrequent) {
1385
+ why.push('cmp-primary')
1386
+ out.name = ment.cmp
1387
+ out.orig = ment.origcmp
1388
+ out.cmpish = true
1389
+ out.pathish = false
1390
+ why.push('cmp-infreq')
1391
+ }
1392
+ else if (cmpOccursInPath(data, ment.cmp)) {
1393
+ why.push('cmp-path')
1394
+ out.name = ment.cmp
1395
+ out.orig = ment.origcmp
1396
+ out.cmpish = true
1397
+ out.pathish = false
1398
+ why.push('cmp-inpath')
1399
+ }
1400
+ else {
1401
+ why.push('path-over-cmp')
1402
+ }
1403
+ }
353
1404
 
354
- // console.log('ISLIST', entdesc.name, methodStr, opname, pathStr)
1405
+ else if (
1406
+ 'DELETE' === mdesc.method
1407
+ && null == ment.cmp
1408
+ ) {
1409
+ let cmps: { cmp: string, origcmp: string }[] =
1410
+ findcmps(data, mdesc.path, ['responses'], { uniq: true })
1411
+
1412
+ if (1 === cmps.length) {
1413
+ out.name = cmps[0].cmp
1414
+ out.orig = cmps[0].origcmp
1415
+ out.cmpish = true
1416
+ out.pathish = false
1417
+ why.push('cmp-found-delete')
1418
+ }
1419
+ else {
1420
+ why.push('path-primary-delete')
1421
+ }
355
1422
  }
1423
+
356
1424
  else {
357
- why.push('not-load')
1425
+ why.push('path-primary')
358
1426
  }
359
1427
 
360
- return opname
1428
+ debugpath(mdesc.path, mdesc.method, 'ENTITY-CMP-NAME', mdesc.path,
1429
+ mdesc.method, entname + '->', out, why, ment,
1430
+ IS_ENTCMP_METHOD_RATE, IS_ENTCMP_PATH_RATE)
1431
+
1432
+ // console.log('ECM-Z', out, why, ment)
1433
+
1434
+ return out
361
1435
  }
362
1436
 
363
1437
 
1438
+ function cmpOccursInPath(data: { def: any, work: any }, cmpname: string): boolean {
1439
+ if (null == data.work.potentialCmpsFromPaths) {
1440
+ data.work.potentialCmpsFromPaths = {}
1441
+ each(data.def.paths, (_pathdef: PathDef, pathstr: string) => {
1442
+ const parts: string[] = data.work.pathmap[pathstr].parts
1443
+
1444
+ parts
1445
+ .filter(p => !p.startsWith('{'))
1446
+ .map(p => data.work.potentialCmpsFromPaths[canonize(p)] = true)
1447
+ })
1448
+ }
1449
+
1450
+ return null != data.work.potentialCmpsFromPaths[cmpname]
1451
+ }
1452
+
1453
+
1454
+
1455
+
1456
+
364
1457
  function isListResponse(
365
- methodDef: Record<string, any>,
1458
+ mdesc: Record<string, any>,
366
1459
  pathStr: string,
367
- entdesc: EntityDesc,
368
1460
  why: string[]
369
1461
  ): boolean {
1462
+ const ment = mdesc.MethodEntity
1463
+ const pm = ment.pm
370
1464
 
371
- const caught = capture(methodDef, {
372
- responses: {
373
- '`$ANY`': { content: { 'application/json': { schema: '`$CAPTURE`' } } },
374
- }
375
- })
376
-
377
- const schema = caught.schema
378
1465
  let islist = false
1466
+ let schema
379
1467
 
380
- if (null == schema) {
381
- why.push('no-schema')
1468
+ if (pm && pm.expr.endsWith('p/')) {
1469
+ why.push('end-param')
382
1470
  }
383
1471
  else {
384
- if (schema.type === 'array') {
385
- why.push('array')
386
- islist = true
1472
+
1473
+ let caught = capture(mdesc, {
1474
+ responses:
1475
+ // '`$ANY`': { content: { 'application/json': { schema: '`$CAPTURE`' } } },
1476
+ ['`$SELECT`', { '$KEY': { '`$OR`': ['200', '201'] } },
1477
+ { content: { 'application/json': { schema: '`$CAPTURE`' } } }],
1478
+
1479
+ })
1480
+
1481
+ schema = caught.schema
1482
+
1483
+ if (null == schema) {
1484
+ caught = capture(mdesc, {
1485
+ responses:
1486
+ // '`$ANY`': { content: { 'application/json': { schema: '`$CAPTURE`' } } },
1487
+ ['`$SELECT`', { '$KEY': { '`$OR`': ['200', '201'] } },
1488
+ { schema: '`$CAPTURE`' }],
1489
+
1490
+ })
1491
+ schema = caught.schema
387
1492
  }
388
1493
 
389
- if (!islist) {
390
- const properties = schema.properties || {}
391
- each(properties, (prop) => {
392
- if (prop.type === 'array') {
1494
+ if (null == schema) {
1495
+ why.push('no-schema')
1496
+ }
1497
+ else {
1498
+ if (schema.type === 'array') {
1499
+ why.push('array')
1500
+ islist = true
1501
+ }
393
1502
 
394
- if (1 === size(properties)) {
395
- why.push('one-prop:' + prop.key$)
396
- islist = true
397
- }
1503
+ if (!islist) {
1504
+ const properties = resolveSchemaProperties(schema)
398
1505
 
399
- if (2 === size(properties) &&
400
- ('data' === prop.key$ ||
401
- 'list' === prop.key$)
402
- ) {
403
- why.push('two-prop:' + prop.key$)
1506
+ each(properties, (prop) => {
1507
+ if (prop.type === 'array') {
1508
+ why.push('array-prop:' + prop.key$)
404
1509
  islist = true
405
1510
  }
1511
+ })
1512
+ }
406
1513
 
407
- if (prop.key$ === entdesc.name) {
408
- why.push('name:' + entdesc.origname)
409
- islist = true
410
- }
1514
+ if (!islist) {
1515
+ why.push('not-list')
1516
+ }
1517
+ }
1518
+ }
411
1519
 
412
- if (prop.key$ === entdesc.origname) {
413
- why.push('origname:' + entdesc.origname)
414
- islist = true
415
- }
416
1520
 
417
- const listent = listedEntity(prop)
418
- if (listent === entdesc.name) {
419
- why.push('listent:' + listent)
420
- islist = true
421
- }
1521
+ debugpath(pathStr, mdesc.method, 'IS-LIST', islist, why, schema)
422
1522
 
1523
+ return islist
1524
+ }
423
1525
 
424
- // if ('/v2/users' === pathStr) {
425
- // console.log('islistresponse', islist, pathStr, entdesc.name, listedEntity(prop), properties)
426
- // }
427
- }
428
- })
1526
+
1527
+ function resolveSchemaProperties(schema: any) {
1528
+ let properties: Record<string, any> = {}
1529
+
1530
+ // This is definitely heuristic!
1531
+ if (schema.allOf) {
1532
+ for (let i = schema.allOf.length - 1; -1 < i; --i) {
1533
+ properties = merge([properties, schema.allOf[i].properties || {}])
1534
+ }
1535
+ }
1536
+
1537
+ if (schema.properties) {
1538
+ properties = merge([properties, schema.properties])
1539
+ }
1540
+
1541
+ return properties
1542
+ }
1543
+
1544
+
1545
+
1546
+
1547
+
1548
+
1549
+
1550
+
1551
+ function updateAction(
1552
+ methodName: string,
1553
+ oldParam: string,
1554
+ actionName: string,
1555
+ entityDesc: EntityDesc,
1556
+ pathdesc: EntityPathDesc,
1557
+ why: string
1558
+ ) {
1559
+ if (
1560
+ // Entity not already encoding action.
1561
+ !entityDesc.name.endsWith(canonize(actionName))
1562
+ && null == pathdesc.action[actionName]
1563
+ ) {
1564
+ pathdesc.action[actionName] = {
1565
+ // kind: '`$BOOLEAN`',
1566
+ why_action: ['ent', `${entityDesc.name}`, `${why}`, `${oldParam}`, `${methodName}`]
429
1567
  }
1568
+ }
1569
+ }
1570
+
430
1571
 
431
- if (!islist) {
432
- why.push('not-list')
1572
+
1573
+ function updateParamRename(
1574
+ ctx: ApiDefContext,
1575
+ data: { def: any, guide: any, work: any },
1576
+ path: string,
1577
+ method: string,
1578
+ paramRenameCapture: {
1579
+ rename: Record<string, string>,
1580
+ why: Record<string, string[]>,
1581
+ },
1582
+ oldParamName: string,
1583
+ newParamName: string,
1584
+ why: string,
1585
+ ) {
1586
+ const existingNewName = paramRenameCapture.rename[oldParamName]
1587
+ const existingWhy = paramRenameCapture.why[oldParamName]
1588
+
1589
+ debugpath(path, method, 'UPDATE-PARAM-RENAME', path, oldParamName, newParamName, existingNewName)
1590
+
1591
+ if (null == existingNewName) {
1592
+ paramRenameCapture.rename[oldParamName] = newParamName
1593
+ if (!existingWhy.includes(why)) {
1594
+ existingWhy.push(why)
433
1595
  }
1596
+ }
1597
+ else if (newParamName == existingNewName) {
1598
+ // if (!existingWhy.includes(why)) {
1599
+ // existingWhy.push(why)
434
1600
  // }
435
1601
  }
1602
+ else {
1603
+ ctx.warn({
1604
+ paramRenameCapture, oldParamName, newParamName, why,
1605
+ note: 'Param rename mismatch: existing: ' +
1606
+ oldParamName + ' -> ' + existingNewName + ' (why: ' + existingNewName + ') ' +
1607
+ ' proposed: ' + newParamName + ' (why: ' + why + ') ' +
1608
+ 'for path: ' + path + '. method: ' + method
1609
+ })
1610
+ }
1611
+ }
436
1612
 
437
- return islist
1613
+
1614
+
1615
+ function isParam(partStr: string) {
1616
+ return '{' === partStr[0] && '}' === partStr[partStr.length - 1]
438
1617
  }
439
1618
 
440
1619
 
441
- function listedEntity(prop: any) {
442
- const xref = prop?.items?.['x-ref']
443
- const m = 'string' === typeof xref && xref.match(/^#\/components\/schemas\/(.+)$/)
444
- if (m) {
445
- return depluralize(snakify(m[1]))
1620
+ /*
1621
+ function fixEntName(origName: string) {
1622
+ if (null == origName) {
1623
+ return origName
1624
+ }
1625
+ return depluralize(snakify(origName))
1626
+ }
1627
+ */
1628
+
1629
+
1630
+ function findcmps(
1631
+ data: { def: any, work: any, guide: any },
1632
+ pathStr: string,
1633
+ underprops: string[],
1634
+ opts?: { uniq?: boolean }
1635
+ ): { cmp: string, origcmp: string }[] {
1636
+ const cmplist: string[] = []
1637
+ const cmpset = new Set<string>()
1638
+
1639
+ // TODO: cache in ctx.work
1640
+
1641
+ each(data.def.paths[pathStr])
1642
+ .map((md: MethodDef) => {
1643
+ underprops.map((up: string) => {
1644
+ let found = find((md as any)[up], 'x-ref')
1645
+
1646
+
1647
+ found.map((xref: { val: string }) => {
1648
+ // console.log('FINDCMPS', pathStr, (md as any).key$, up, xref.val)
1649
+ let m = xref.val.match(/\/(components\/schemas|definitions)\/(.+)$/)
1650
+ if (m) {
1651
+ cmplist.push(m[2])
1652
+ cmpset.add(m[2])
1653
+ }
1654
+ })
1655
+ })
1656
+ })
1657
+ // console.log('FOUNDCMPS', cmps)
1658
+ return (opts?.uniq ? Array.from(cmpset) : cmplist).map(n =>
1659
+ ({ cmp: canonize(n), origcmp: n }))
1660
+ }
1661
+
1662
+
1663
+ function makeMethodEntityDesc(desc: Record<string, any>): MethodEntityDesc {
1664
+ let ment: MethodEntityDesc = {
1665
+ cmp: desc.cmp ?? null,
1666
+ origcmp: desc.origcmp ?? null,
1667
+ origcmpref: desc.origcmpref ?? null,
1668
+
1669
+ ref: desc.ref ?? '',
1670
+ why_cmp: desc.why_cmp ?? [],
1671
+ cmpoccur: desc.cmpoccur ?? 0,
1672
+ path_rate: desc.path_rate ?? 0,
1673
+ method_rate: desc.method_rate ?? 0,
1674
+ entname: desc.entname ?? '',
1675
+ why_op: desc.why_op ?? [],
1676
+ rename: desc.rename ?? { param: {} },
1677
+ why_rename: desc.why_rename ?? { why_param: {} },
1678
+ rename_orig: desc.rename_orig ?? [],
1679
+ opname: desc.opname ?? '',
1680
+ why_opname: desc.why_opname ?? [],
1681
+ }
1682
+ return ment
1683
+ }
1684
+
1685
+
1686
+ function findPotentialSchemaRefs(pathStr: string, methodName: string, responses: any) {
1687
+ const xrefs: string[] = []
1688
+ const rescodes = ['200', '201']
1689
+ for (let rescode of rescodes) {
1690
+ const schema = getResponseSchema(responses[rescode])
1691
+ if (null != schema) {
1692
+ if (null != schema['x-ref']) {
1693
+ xrefs.push(schema['x-ref'])
1694
+ }
1695
+ else if ('array' === schema.type && null != schema.items?.['x-ref']) {
1696
+ xrefs.push(schema.items?.['x-ref'])
1697
+ }
1698
+ }
446
1699
  }
1700
+
1701
+ debugpath(pathStr, methodName, 'POTENTIAL-SCHEMA-REFS', xrefs)
1702
+ return xrefs
447
1703
  }
448
1704
 
449
1705
 
1706
+ function hasMethod(def: any, pathStr: string, methodName: string) {
1707
+ const pathDef = def?.paths?.[pathStr]
1708
+ const found = (
1709
+ null != pathDef
1710
+ && (
1711
+ null != pathDef[methodName.toLowerCase()]
1712
+ || null != pathDef[methodName.toUpperCase()]
1713
+ )
1714
+ )
1715
+ console.log('hasMethod', pathStr, methodName, found)
1716
+ return found
1717
+ }
1718
+
1719
+
1720
+
1721
+ /*
1722
+ // Some decisions require the full list of potential entities.
1723
+ function reviewEntityDescs(ctx: ApiDefContext, result: any) {
1724
+ const guide: Guide = result.data.guide
1725
+ const metrics = guide.metrics
1726
+ const entityDescs = result.data.work.entmap
1727
+
1728
+ if (0 < metrics.count.cmp) {
1729
+ items(entityDescs).map(([entname, entdesc]: [string, EntityDesc]) => {
1730
+
1731
+ // Entities with a single path and single op and no cmp are suspicious
1732
+
1733
+ const pathmap = entdesc.path
1734
+ const pathcount = size(pathmap)
1735
+ const hascmp = null != entdesc.cmp?.namedesc
1736
+
1737
+ if (
1738
+ 1 === pathcount
1739
+ && !hascmp
1740
+ ) {
1741
+ let pathdesc: EntityPathDesc = each(pathmap)[0]
1742
+ const pathStr = (pathdesc as any).key$
1743
+
1744
+ if (1 === size(pathdesc.op)) {
1745
+ let op = pathdesc.op
1746
+
1747
+ // console.log('REVIEW', entdesc.name, pathcount, hascmp, op)
1748
+
1749
+ if (op.create) {
1750
+
1751
+ // Entities without "good" components
1752
+ if (
1753
+ entname.includes('_')
1754
+ && pathdesc.pm.expr.endsWith('p/t/')
1755
+ ) {
1756
+ const lastpart = canonize(getelem(pathdesc.pm, -1))
1757
+ const tgtent = entityDescs[lastpart]
1758
+
1759
+ // console.log('REVIEW', entname, entdesc.cmp, size(pathmap), lastpart, realent)
1760
+
1761
+ if (
1762
+ null != tgtent
1763
+ && tgtent.name !== entname
1764
+ && (
1765
+ null == tgtent.cmp
1766
+ || lastpart == tgtent.cmp
1767
+ )
1768
+ ) {
1769
+
1770
+ // Actually a known component
1771
+ // console.dir(entdesc, { depth: null })
1772
+
1773
+
1774
+ const realent = guide.entity[entname]
1775
+ const realpathmap = realent.path
1776
+ let realpath = realpathmap[pathStr]
1777
+
1778
+ if (null == realpath) {
1779
+ realpath = realpathmap[pathStr] = pathdesc
1780
+ }
1781
+ else if (null == realpath.op?.create) {
1782
+ realpath.op = (realpath.op ?? {})
1783
+ realpath.op.create = pathdesc.op.create
1784
+ }
1785
+
1786
+ realpath.op.create.why_op =
1787
+ 'was/create/A:' + entname + ':' + realpath.op.create.why_op
1788
+
1789
+ delete entityDescs[entname]
1790
+
1791
+ // console.log('REPLACE', entname, realent.name, realpath)
1792
+ }
1793
+
1794
+ }
1795
+ }
1796
+
1797
+ else if (op.remove) {
1798
+ const otherents: EntityDesc[] = each(entityDescs)
1799
+ .filter((ed: EntityDesc) => ed !== entdesc && each(ed.path)
1800
+ .filter(epd => epd.key$ === pathStr).length)
1801
+
1802
+ const otherent = 1 === otherents.length ? otherents[0] : null
1803
+
1804
+ // console.log('OTHERENT', pathStr, otherents.length, otherent)
1805
+
1806
+ if (null != otherent && null != otherent.cmp) {
1807
+ const otherpath = otherent.path[pathStr]
1808
+
1809
+ if (null == otherpath.op.remove) {
1810
+ otherpath.op.remove = op.remove
1811
+ otherpath.op.remove.why_op =
1812
+ 'was/delete/A:' + entname + ':' + op.remove.why_op
1813
+ delete entityDescs[entname]
1814
+ }
1815
+ }
1816
+ }
1817
+
1818
+ debugpath(pathdesc.pm.path, null,
1819
+ 'REVIEW-ENTITY', formatJSONIC(entdesc, { hsepd: 0, $: true, color: true }))
1820
+
1821
+
1822
+ }
1823
+ }
1824
+ })
1825
+ }
1826
+ }
1827
+ */
1828
+
450
1829
 
451
1830
  export {
452
1831
  heuristic01