@voxgig/apidef 2.4.1 → 3.1.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.
Files changed (108) hide show
  1. package/dist/apidef.d.ts +7 -2
  2. package/dist/apidef.js +190 -114
  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 +39 -12
  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 +87 -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 +1122 -234
  28. package/dist/guide/heuristic01.js.map +1 -1
  29. package/dist/model.d.ts +90 -0
  30. package/dist/model.js +4 -0
  31. package/dist/model.js.map +1 -0
  32. package/dist/parse.d.ts +4 -3
  33. package/dist/parse.js +40 -46
  34. package/dist/parse.js.map +1 -1
  35. package/dist/resolver.js +2 -1
  36. package/dist/resolver.js.map +1 -1
  37. package/dist/transform/args.d.ts +3 -0
  38. package/dist/transform/args.js +54 -0
  39. package/dist/transform/args.js.map +1 -0
  40. package/dist/transform/clean.d.ts +2 -2
  41. package/dist/transform/clean.js +28 -3
  42. package/dist/transform/clean.js.map +1 -1
  43. package/dist/transform/entity.d.ts +9 -1
  44. package/dist/transform/entity.js +57 -41
  45. package/dist/transform/entity.js.map +1 -1
  46. package/dist/transform/field.d.ts +1 -1
  47. package/dist/transform/field.js +89 -65
  48. package/dist/transform/field.js.map +1 -1
  49. package/dist/transform/flow.d.ts +3 -0
  50. package/dist/transform/flow.js +26 -0
  51. package/dist/transform/flow.js.map +1 -0
  52. package/dist/transform/flowstep.d.ts +3 -0
  53. package/dist/transform/flowstep.js +145 -0
  54. package/dist/transform/flowstep.js.map +1 -0
  55. package/dist/transform/operation.d.ts +3 -3
  56. package/dist/transform/operation.js +101 -296
  57. package/dist/transform/operation.js.map +1 -1
  58. package/dist/transform/select.d.ts +3 -0
  59. package/dist/transform/select.js +65 -0
  60. package/dist/transform/select.js.map +1 -0
  61. package/dist/transform/top.js +24 -2
  62. package/dist/transform/top.js.map +1 -1
  63. package/dist/transform.d.ts +1 -1
  64. package/dist/transform.js +4 -0
  65. package/dist/transform.js.map +1 -1
  66. package/dist/tsconfig.tsbuildinfo +1 -1
  67. package/dist/types.d.ts +115 -14
  68. package/dist/types.js +4 -2
  69. package/dist/types.js.map +1 -1
  70. package/dist/utility.d.ts +34 -2
  71. package/dist/utility.js +444 -6
  72. package/dist/utility.js.map +1 -1
  73. package/model/apidef.jsonic +76 -1
  74. package/model/guide.jsonic +14 -44
  75. package/package.json +19 -16
  76. package/src/apidef.ts +258 -122
  77. package/src/builder/entity/{apiEntity.ts → entity.ts} +18 -11
  78. package/src/builder/entity/info.ts +53 -0
  79. package/src/builder/entity.ts +9 -35
  80. package/src/builder/flow/flowHeuristic01.ts +46 -12
  81. package/src/builder/flow.ts +54 -13
  82. package/src/def.ts +91 -0
  83. package/src/desc.ts +154 -0
  84. package/src/guide/guide.ts +208 -134
  85. package/src/guide/heuristic01.ts +1653 -272
  86. package/src/model.ts +143 -0
  87. package/src/parse.ts +50 -59
  88. package/src/resolver.ts +3 -1
  89. package/src/schematron.ts.off +317 -0
  90. package/src/transform/args.ts +98 -0
  91. package/src/transform/clean.ts +45 -11
  92. package/src/transform/entity.ts +96 -50
  93. package/src/transform/field.ts +136 -75
  94. package/src/transform/flow.ts +59 -0
  95. package/src/transform/flowstep.ts +242 -0
  96. package/src/transform/operation.ts +119 -419
  97. package/src/transform/select.ts +119 -0
  98. package/src/transform/top.ts +46 -4
  99. package/src/transform.ts +8 -4
  100. package/src/types.ts +181 -5
  101. package/src/utility.ts +567 -9
  102. package/dist/builder/entity/apiEntity.d.ts +0 -3
  103. package/dist/builder/entity/apiEntity.js.map +0 -1
  104. package/dist/builder/entity/def.d.ts +0 -3
  105. package/dist/builder/entity/def.js +0 -19
  106. package/dist/builder/entity/def.js.map +0 -1
  107. package/src/builder/entity/def.ts +0 -44
  108. 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,1546 @@ 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
+ // Only specify transforms if they are not defaults
1030
+ const transform: Record<string, any> = {
1031
+ req: undefined,
1032
+ res: undefined,
1033
+ }
1034
+
1035
+ const resokdef = mdesc.responses?.[200] || mdesc.responses?.[201]
1036
+ const resprops = getResponseSchema(resokdef)?.properties
1037
+ debugpath(pathStr, methodName, 'TRANSFORM-RES', keysof(resprops))
1038
+
1039
+ // console.log('APIDEF-resprops', resprops)
1040
+
1041
+ if (resprops) {
1042
+ if (resprops[entdesc.origname]) {
1043
+ transform.res = '`body.' + entdesc.origname + '`'
236
1044
  }
237
- else {
238
- why_ent.push('path:' + m[1])
239
- why_name.push('path:' + m[1])
1045
+ else if (resprops[entdesc.name]) {
1046
+ transform.res = '`body.' + entdesc.name + '`'
240
1047
  }
1048
+ }
241
1049
 
242
- entdesc = (entityDescs[entname] = entityDescs[entname] || {
243
- name: entname,
244
- id: Math.random(),
245
- alias: {}
246
- })
1050
+ const reqprops = getRequestBodySchema(mdesc.requestBody)
1051
+ debugpath(pathStr, methodName, 'TRANSFORM-REQ', keysof(reqprops))
1052
+ if (reqprops) {
1053
+ if (reqprops[entdesc.origname]) {
1054
+ transform.req = { [entdesc.origname]: '`reqdata`' }
1055
+ }
1056
+ else if (reqprops[entdesc.name]) {
1057
+ transform.req = { [entdesc.name]: '`reqdata`' }
1058
+ }
1059
+ }
247
1060
 
248
- if (null != pathParam) {
249
- const pathParamCanon = snakify(pathParam)
250
- if ('id' != pathParamCanon) {
251
- entdesc.alias.id = pathParamCanon
252
- entdesc.alias[pathParamCanon] = 'id'
1061
+ if (!isempty(transform)) {
1062
+ op[opname].transform = transform
1063
+ }
1064
+ }
1065
+
1066
+
1067
+
1068
+ function BuildEntity(spec: TaskSpec) {
1069
+ const entdesc = spec.node.val
1070
+ // console.log('BUILD-ENTITY')
1071
+ // console.dir(entdesc, { depth: null })
1072
+
1073
+ const guide: Guide = spec.data.guide
1074
+ guide.metrics.count.entity++
1075
+
1076
+ const entityMap: Record<string, GuideEntity> = guide.entity
1077
+
1078
+ const path: Record<string, GuidePath> = {}
1079
+
1080
+ const rename_param = (pathdesc: any) => {
1081
+ // console.log('RENAME-PATHDESC', pathdesc)
1082
+ const out: Record<string, GuideRenameParam> = {}
1083
+ each(pathdesc.rename.param, (item: any) => {
1084
+ out[item.key$] = {
1085
+ target: item.val$,
1086
+ why_rename: pathdesc.why_rename.why_param[item.key$]
253
1087
  }
1088
+ })
1089
+ return out
1090
+ }
1091
+
1092
+ each(entdesc.path, (pathdesc: any, pathstr: string) => {
1093
+ const guidepath = {
1094
+ why_path: pathdesc.why_path,
1095
+ action: pathdesc.action,
1096
+ rename: {
1097
+ param: rename_param(pathdesc)
1098
+ },
1099
+ op: pathdesc.op
254
1100
  }
1101
+ path[pathstr] = guidepath
1102
+ })
1103
+
1104
+ entityMap[entdesc.name] = {
1105
+ name: entdesc.name,
1106
+ orig: entdesc.origcmp,
1107
+ path,
255
1108
  }
256
1109
 
257
- // Can't figure out the entity
1110
+ }
1111
+
1112
+
1113
+
1114
+
1115
+
1116
+
1117
+
1118
+ function entityPathMatch_tpte(
1119
+ data: { def: any, guide: any, work: any },
1120
+ pm: PathMatch,
1121
+ mdesc: any,
1122
+ why: string[]
1123
+ ) {
1124
+ const ment = mdesc.MethodEntity
1125
+
1126
+ const pathNameIndex = 2
1127
+
1128
+ why.push('path=t/p/t/')
1129
+ const origPathName = pm[pathNameIndex]
1130
+ let entname = canonize(origPathName)
1131
+ let ecm = undefined
1132
+
1133
+ if (null != ment.cmp) {
1134
+ ecm = entityCmpMatch(data, entname, mdesc, why)
1135
+ entname = ecm.name
1136
+ why.push('has-cmp=' + ecm.orig)
1137
+ }
1138
+
1139
+ else if (probableEntityMethod(data, ment, pm, why)) {
1140
+ ecm = entityCmpMatch(data, entname, mdesc, why)
1141
+ if (ecm.cmpish) {
1142
+ entname = ecm.name
1143
+ why.push('prob-ent=' + ecm.orig)
1144
+ }
1145
+ else if (endsWithCmp(data, pm)) {
1146
+ entname = canonize(getelem(pm, -1))
1147
+ why.push('prob-ent-last=' + ecm.orig)
1148
+ }
1149
+ else if (0 < findPathsWithPrefix(data, pm.path, { strict: true })) {
1150
+ entname = canonize(getelem(pm, -1))
1151
+ why.push('prob-ent-prefix=' + ecm.orig)
1152
+ }
1153
+ else {
1154
+ entname = canonize(getelem(pm, -3)) + '_' + entname
1155
+ why.push('prob-ent-part')
1156
+ }
1157
+ }
1158
+
1159
+ // Probably an entity action suffix
258
1160
  else {
259
- console.log('NO ENTTIY', pathStr)
260
- return
1161
+ why.push('prob-ent-act')
1162
+ entname = canonize(getelem(pm, -3))
261
1163
  }
262
1164
 
1165
+ return entname
1166
+ }
263
1167
 
264
- // entdesc.plural = origentname
265
- entdesc.origname = origentname
1168
+ function endsWithCmp(data: any, pm: PathMatch) {
1169
+ const last = canonize(getelem(pm, -1))
1170
+ return isOrigCmp(data, last)
1171
+ }
266
1172
 
267
- names(entdesc, entname)
268
1173
 
269
- entdesc.alias = entdesc.alias || {}
1174
+ function isOrigCmp(data: any, name: string) {
1175
+ return null != data.metrics.count.origcmprefs[name]
1176
+ }
270
1177
 
271
- entdesc.path = (entdesc.path || {})
272
- entdesc.path[pathStr] = entdesc.path[pathStr] || {}
273
- entdesc.path[pathStr].op = entdesc.path[pathStr].op || {}
274
1178
 
275
- if (null == entdesc.why_name) {
276
- entdesc.why_name = why_name
1179
+ function entityOccursInPath(parts: string[], entname: string) {
1180
+ let partsLower = parts.map(p => p.toLowerCase())
1181
+ partsLower = partsLower.filter(p => '{' !== p[0]).map(p => canonize(p))
1182
+ return !partsLower.reduce((a: boolean, p: string) => (a && p !== entname), true)
1183
+ }
1184
+
1185
+
1186
+ function entityPathMatch_tpe(
1187
+ data: { def: any, guide: any, work: any },
1188
+ pm: PathMatch, mdesc: any, why: string[]
1189
+ ) {
1190
+ const ment = mdesc.MethodEntity
1191
+ const pathNameIndex = 0
1192
+
1193
+ why.push('path=t/p/')
1194
+ const origPathName = pm[pathNameIndex]
1195
+ // let entname = fixEntName(origPathName)
1196
+ let entname = canonize(origPathName)
1197
+
1198
+ if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
1199
+ let ecm = entityCmpMatch(data, entname, mdesc, why)
1200
+ entname = ecm.name
1201
+ }
1202
+ else {
1203
+ why.push('ent-act')
1204
+ }
1205
+
1206
+ return entname
1207
+ }
1208
+
1209
+
1210
+ function entityPathMatch_pte(
1211
+ data: { def: any, guide: any, work: any },
1212
+ pm: PathMatch, mdesc: any, why: string[]
1213
+ ) {
1214
+ const ment = mdesc.MethodEntity
1215
+ const pathNameIndex = 1
1216
+
1217
+ why.push('path=p/t/')
1218
+ const origPathName = pm[pathNameIndex]
1219
+ let entname = canonize(origPathName)
1220
+
1221
+ if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
1222
+ let ecm = entityCmpMatch(data, entname, mdesc, why)
1223
+ entname = ecm.name
1224
+ }
1225
+ else {
1226
+ why.push('ent-act')
277
1227
  }
278
1228
 
279
- return entdesc
1229
+ return entname
280
1230
  }
281
1231
 
282
1232
 
283
- const REQKIND: any = {
284
- get: 'res',
285
- post: 'req',
286
- put: 'req',
287
- patch: 'req',
1233
+ function entityPathMatch_te(
1234
+ data: { def: any, guide: any, work: any },
1235
+ pm: PathMatch, mdesc: any, why: string[]
1236
+ ) {
1237
+ const ment = mdesc.MethodEntity
1238
+ const pathNameIndex = 0
1239
+
1240
+ why.push('path=t/')
1241
+ const origPathName = pm[pathNameIndex]
1242
+ // let entname = fixEntName(origPathName)
1243
+ let entname = canonize(origPathName)
1244
+
1245
+ if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
1246
+ let ecm = entityCmpMatch(data, entname, mdesc, why)
1247
+ entname = ecm.name
1248
+ }
1249
+ else {
1250
+ why.push('ent-act')
1251
+ }
1252
+
1253
+ return entname
288
1254
  }
289
1255
 
290
1256
 
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
1257
+ function entityPathMatch_tpp(
1258
+ data: { def: any, guide: any, work: any },
1259
+ pm: PathMatch, mdesc: any, why: string[]
1260
+ ) {
1261
+ const ment = mdesc.MethodEntity
1262
+ const pathNameIndex = 0
299
1263
 
300
- let xrefs = find(methodDef, 'x-ref')
301
- .filter(xref => xref.val.includes('schema'))
1264
+ why.push('path=t/p/p')
1265
+ const origPathName = pm[pathNameIndex]
1266
+ // let entname = fixEntName(origPathName)
1267
+ let entname = canonize(origPathName)
302
1268
 
303
- // TODO: identify non-ent schemas
304
- .filter(xref => !xref.val.includes('Meta'))
1269
+ if (null != ment.cmp || probableEntityMethod(data, mdesc, pm, why)) {
1270
+ let ecm = entityCmpMatch(data, entname, mdesc, why)
1271
+ entname = ecm.name
1272
+ }
1273
+ else {
1274
+ why.push('ent-act')
1275
+ }
305
1276
 
306
- .sort((a, b) => a.path.length - b.path.length)
1277
+ return entname
1278
+ }
307
1279
 
308
- // console.log('RCN', pathStr, methodStr, xrefs.map(x => [x.val, x.path.length]))
309
1280
 
310
- let first = xrefs[0]?.val
1281
+ function getRequestBodySchema(requestBody: any) {
1282
+ return requestBody?.content?.['application/json']?.schema ??
1283
+ requestBody?.schema
1284
+ }
311
1285
 
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
- }
1286
+ function getResponseSchema(response: any) {
1287
+ return response?.content?.['application/json']?.schema ??
1288
+ response?.schema
1289
+ }
1290
+
1291
+
1292
+ // No entity component was found, but there still might be an entity.
1293
+ function probableEntityMethod(
1294
+ data: { def: any },
1295
+ mdesc: any,
1296
+ pm: PathMatch,
1297
+ why: string[]
1298
+ ) {
1299
+ const request = mdesc.requestBody
1300
+ const reqSchema = getRequestBodySchema(request)
1301
+
1302
+ const response = mdesc.responses?.['201'] || mdesc.responses?.['200']
1303
+ const resSchema = getResponseSchema(response)
1304
+ const noResponse = null == resSchema && null != mdesc.responses?.['204']
1305
+
1306
+ let prob_why = ''
1307
+
1308
+ let probent = false
1309
+
1310
+ if (noResponse) {
1311
+ // No response at all means not an action, thus probably an entity.
1312
+ prob_why = 'nores'
1313
+ probent = true
318
1314
  }
319
1315
 
320
- if (null != compname) {
321
- compname = depluralize(snakify(compname))
1316
+ else if (null != reqSchema) {
1317
+ if (
1318
+ 'POST' === mdesc.method
1319
+ && !pm.expr.endsWith('/p/')
1320
+
1321
+ // A real entity would probably occur in at least one other t/p path
1322
+ // otherwise this is probably an action
1323
+ && (1 < Object.keys(data.def.paths).filter(path =>
1324
+ path.includes('/' + pm[pm.length - 1] + '/')).length)
1325
+ ) {
1326
+ prob_why = 'post'
1327
+ probent = true
1328
+ }
322
1329
 
323
- // Assume sub schemas suffixes are not real entities
324
- if (compname.includes(entname)) {
325
- compname = compname.slice(0, compname.indexOf(entname) + entname.length)
1330
+ else if (
1331
+ ('PUT' === mdesc.method || 'PATCH' === mdesc.method)
1332
+ && pm.expr.endsWith('/p/')
1333
+ ) {
1334
+ prob_why = 'putish'
1335
+ probent = true
326
1336
  }
327
1337
  }
1338
+ else if ('GET' === mdesc.method) {
1339
+ prob_why = 'get'
1340
+ probent = true
1341
+ }
328
1342
 
329
- return compname
330
- }
1343
+ const rescodes = Object.keys(mdesc.responses ?? {})
331
1344
 
1345
+ debugpath(mdesc.path, mdesc.method, 'PROBABLE-ENTITY-RESPONSE',
1346
+ { mdesc, responses: rescodes, probent, prob_why })
332
1347
 
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)
1348
+ why.push('entres=' + probent + '/' + rescodes + ('' === prob_why ? '' : '/' + prob_why))
342
1349
 
1350
+ return probent
1351
+ }
343
1352
 
344
- let opname = METHOD_IDOP[methodStr]
345
- if (null == opname) {
346
- why.push('no-op:' + methodStr)
347
- return
1353
+
1354
+ function entityCmpMatch(
1355
+ data: { def: any, guide: any, work: any },
1356
+ entname: string,
1357
+ mdesc: any,
1358
+ why: string[]
1359
+ ): {
1360
+ name: string,
1361
+ orig: string,
1362
+ cmpish: boolean,
1363
+ pathish: boolean,
1364
+ } {
1365
+ const ment = mdesc.MethodEntity
1366
+
1367
+ let out = {
1368
+ name: entname,
1369
+ orig: ment.origcmp ?? entname,
1370
+ cmpish: false,
1371
+ pathish: true,
348
1372
  }
349
1373
 
350
- if ('load' === opname) {
351
- const islist = isListResponse(methodDef, pathStr, entdesc, why)
352
- opname = islist ? 'list' : opname
1374
+ // console.log('ECM-A', out, ment)
1375
+
1376
+ const cmpInfrequent = (
1377
+ ment.method_rate < IS_ENTCMP_METHOD_RATE
1378
+ || ment.path_rate < IS_ENTCMP_PATH_RATE
1379
+ )
1380
+
1381
+ if (
1382
+ null != ment.cmp
1383
+ && entname != ment.cmp
1384
+ && !ment.cmp.startsWith(entname)
1385
+ ) {
1386
+ if (cmpInfrequent) {
1387
+ why.push('cmp-primary')
1388
+ out.name = ment.cmp
1389
+ out.orig = ment.origcmp
1390
+ out.cmpish = true
1391
+ out.pathish = false
1392
+ why.push('cmp-infreq')
1393
+ }
1394
+ else if (cmpOccursInPath(data, ment.cmp)) {
1395
+ why.push('cmp-path')
1396
+ out.name = ment.cmp
1397
+ out.orig = ment.origcmp
1398
+ out.cmpish = true
1399
+ out.pathish = false
1400
+ why.push('cmp-inpath')
1401
+ }
1402
+ else {
1403
+ why.push('path-over-cmp')
1404
+ }
1405
+ }
353
1406
 
354
- // console.log('ISLIST', entdesc.name, methodStr, opname, pathStr)
1407
+ else if (
1408
+ 'DELETE' === mdesc.method
1409
+ && null == ment.cmp
1410
+ ) {
1411
+ let cmps: { cmp: string, origcmp: string }[] =
1412
+ findcmps(data, mdesc.path, ['responses'], { uniq: true })
1413
+
1414
+ if (1 === cmps.length) {
1415
+ out.name = cmps[0].cmp
1416
+ out.orig = cmps[0].origcmp
1417
+ out.cmpish = true
1418
+ out.pathish = false
1419
+ why.push('cmp-found-delete')
1420
+ }
1421
+ else {
1422
+ why.push('path-primary-delete')
1423
+ }
355
1424
  }
1425
+
356
1426
  else {
357
- why.push('not-load')
1427
+ why.push('path-primary')
358
1428
  }
359
1429
 
360
- return opname
1430
+ debugpath(mdesc.path, mdesc.method, 'ENTITY-CMP-NAME', mdesc.path,
1431
+ mdesc.method, entname + '->', out, why, ment,
1432
+ IS_ENTCMP_METHOD_RATE, IS_ENTCMP_PATH_RATE)
1433
+
1434
+ // console.log('ECM-Z', out, why, ment)
1435
+
1436
+ return out
361
1437
  }
362
1438
 
363
1439
 
1440
+ function cmpOccursInPath(data: { def: any, work: any }, cmpname: string): boolean {
1441
+ if (null == data.work.potentialCmpsFromPaths) {
1442
+ data.work.potentialCmpsFromPaths = {}
1443
+ each(data.def.paths, (_pathdef: PathDef, pathstr: string) => {
1444
+ const parts: string[] = data.work.pathmap[pathstr].parts
1445
+
1446
+ parts
1447
+ .filter(p => !p.startsWith('{'))
1448
+ .map(p => data.work.potentialCmpsFromPaths[canonize(p)] = true)
1449
+ })
1450
+ }
1451
+
1452
+ return null != data.work.potentialCmpsFromPaths[cmpname]
1453
+ }
1454
+
1455
+
1456
+
1457
+
1458
+
364
1459
  function isListResponse(
365
- methodDef: Record<string, any>,
1460
+ mdesc: Record<string, any>,
366
1461
  pathStr: string,
367
- entdesc: EntityDesc,
368
1462
  why: string[]
369
1463
  ): boolean {
1464
+ const ment = mdesc.MethodEntity
1465
+ const pm = ment.pm
370
1466
 
371
- const caught = capture(methodDef, {
372
- responses: {
373
- '`$ANY`': { content: { 'application/json': { schema: '`$CAPTURE`' } } },
374
- }
375
- })
376
-
377
- const schema = caught.schema
378
1467
  let islist = false
1468
+ let schema
379
1469
 
380
- if (null == schema) {
381
- why.push('no-schema')
1470
+ if (pm && pm.expr.endsWith('p/')) {
1471
+ why.push('end-param')
382
1472
  }
383
1473
  else {
384
- if (schema.type === 'array') {
385
- why.push('array')
386
- islist = true
1474
+
1475
+ let caught = capture(mdesc, {
1476
+ responses:
1477
+ // '`$ANY`': { content: { 'application/json': { schema: '`$CAPTURE`' } } },
1478
+ ['`$SELECT`', { '$KEY': { '`$OR`': ['200', '201'] } },
1479
+ { content: { 'application/json': { schema: '`$CAPTURE`' } } }],
1480
+
1481
+ })
1482
+
1483
+ schema = caught.schema
1484
+
1485
+ if (null == schema) {
1486
+ caught = capture(mdesc, {
1487
+ responses:
1488
+ // '`$ANY`': { content: { 'application/json': { schema: '`$CAPTURE`' } } },
1489
+ ['`$SELECT`', { '$KEY': { '`$OR`': ['200', '201'] } },
1490
+ { schema: '`$CAPTURE`' }],
1491
+
1492
+ })
1493
+ schema = caught.schema
387
1494
  }
388
1495
 
389
- if (!islist) {
390
- const properties = schema.properties || {}
391
- each(properties, (prop) => {
392
- if (prop.type === 'array') {
1496
+ if (null == schema) {
1497
+ why.push('no-schema')
1498
+ }
1499
+ else {
1500
+ if (schema.type === 'array') {
1501
+ why.push('array')
1502
+ islist = true
1503
+ }
393
1504
 
394
- if (1 === size(properties)) {
395
- why.push('one-prop:' + prop.key$)
396
- islist = true
397
- }
1505
+ if (!islist) {
1506
+ const properties = resolveSchemaProperties(schema)
398
1507
 
399
- if (2 === size(properties) &&
400
- ('data' === prop.key$ ||
401
- 'list' === prop.key$)
402
- ) {
403
- why.push('two-prop:' + prop.key$)
1508
+ each(properties, (prop) => {
1509
+ if (prop.type === 'array') {
1510
+ why.push('array-prop:' + prop.key$)
404
1511
  islist = true
405
1512
  }
1513
+ })
1514
+ }
406
1515
 
407
- if (prop.key$ === entdesc.name) {
408
- why.push('name:' + entdesc.origname)
409
- islist = true
410
- }
1516
+ if (!islist) {
1517
+ why.push('not-list')
1518
+ }
1519
+ }
1520
+ }
411
1521
 
412
- if (prop.key$ === entdesc.origname) {
413
- why.push('origname:' + entdesc.origname)
414
- islist = true
415
- }
416
1522
 
417
- const listent = listedEntity(prop)
418
- if (listent === entdesc.name) {
419
- why.push('listent:' + listent)
420
- islist = true
421
- }
1523
+ debugpath(pathStr, mdesc.method, 'IS-LIST', islist, why, schema)
422
1524
 
1525
+ return islist
1526
+ }
423
1527
 
424
- // if ('/v2/users' === pathStr) {
425
- // console.log('islistresponse', islist, pathStr, entdesc.name, listedEntity(prop), properties)
426
- // }
427
- }
428
- })
1528
+
1529
+ function resolveSchemaProperties(schema: any) {
1530
+ let properties: Record<string, any> = {}
1531
+
1532
+ // This is definitely heuristic!
1533
+ if (schema.allOf) {
1534
+ for (let i = schema.allOf.length - 1; -1 < i; --i) {
1535
+ properties = merge([properties, schema.allOf[i].properties || {}])
1536
+ }
1537
+ }
1538
+
1539
+ if (schema.properties) {
1540
+ properties = merge([properties, schema.properties])
1541
+ }
1542
+
1543
+ return properties
1544
+ }
1545
+
1546
+
1547
+
1548
+
1549
+
1550
+
1551
+
1552
+
1553
+ function updateAction(
1554
+ methodName: string,
1555
+ oldParam: string,
1556
+ actionName: string,
1557
+ entityDesc: EntityDesc,
1558
+ pathdesc: EntityPathDesc,
1559
+ why: string
1560
+ ) {
1561
+ if (
1562
+ // Entity not already encoding action.
1563
+ !entityDesc.name.endsWith(canonize(actionName))
1564
+ && null == pathdesc.action[actionName]
1565
+ ) {
1566
+ pathdesc.action[actionName] = {
1567
+ // kind: '`$BOOLEAN`',
1568
+ why_action: ['ent', `${entityDesc.name}`, `${why}`, `${oldParam}`, `${methodName}`]
429
1569
  }
1570
+ }
1571
+ }
1572
+
430
1573
 
431
- if (!islist) {
432
- why.push('not-list')
1574
+
1575
+ function updateParamRename(
1576
+ ctx: ApiDefContext,
1577
+ data: { def: any, guide: any, work: any },
1578
+ path: string,
1579
+ method: string,
1580
+ paramRenameCapture: {
1581
+ rename: Record<string, string>,
1582
+ why: Record<string, string[]>,
1583
+ },
1584
+ oldParamName: string,
1585
+ newParamName: string,
1586
+ why: string,
1587
+ ) {
1588
+ const existingNewName = paramRenameCapture.rename[oldParamName]
1589
+ const existingWhy = paramRenameCapture.why[oldParamName]
1590
+
1591
+ debugpath(path, method, 'UPDATE-PARAM-RENAME', path, oldParamName, newParamName, existingNewName)
1592
+
1593
+ if (null == existingNewName) {
1594
+ paramRenameCapture.rename[oldParamName] = newParamName
1595
+ if (!existingWhy.includes(why)) {
1596
+ existingWhy.push(why)
433
1597
  }
1598
+ }
1599
+ else if (newParamName == existingNewName) {
1600
+ // if (!existingWhy.includes(why)) {
1601
+ // existingWhy.push(why)
434
1602
  // }
435
1603
  }
1604
+ else {
1605
+ ctx.warn({
1606
+ paramRenameCapture, oldParamName, newParamName, why,
1607
+ note: 'Param rename mismatch: existing: ' +
1608
+ oldParamName + ' -> ' + existingNewName + ' (why: ' + existingNewName + ') ' +
1609
+ ' proposed: ' + newParamName + ' (why: ' + why + ') ' +
1610
+ 'for path: ' + path + '. method: ' + method
1611
+ })
1612
+ }
1613
+ }
436
1614
 
437
- return islist
1615
+
1616
+
1617
+ function isParam(partStr: string) {
1618
+ return '{' === partStr[0] && '}' === partStr[partStr.length - 1]
438
1619
  }
439
1620
 
440
1621
 
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]))
1622
+ /*
1623
+ function fixEntName(origName: string) {
1624
+ if (null == origName) {
1625
+ return origName
1626
+ }
1627
+ return depluralize(snakify(origName))
1628
+ }
1629
+ */
1630
+
1631
+
1632
+ function findcmps(
1633
+ data: { def: any, work: any, guide: any },
1634
+ pathStr: string,
1635
+ underprops: string[],
1636
+ opts?: { uniq?: boolean }
1637
+ ): { cmp: string, origcmp: string }[] {
1638
+ const cmplist: string[] = []
1639
+ const cmpset = new Set<string>()
1640
+
1641
+ // TODO: cache in ctx.work
1642
+
1643
+ each(data.def.paths[pathStr])
1644
+ .map((md: MethodDef) => {
1645
+ underprops.map((up: string) => {
1646
+ let found = find((md as any)[up], 'x-ref')
1647
+
1648
+
1649
+ found.map((xref: { val: string }) => {
1650
+ // console.log('FINDCMPS', pathStr, (md as any).key$, up, xref.val)
1651
+ let m = xref.val.match(/\/(components\/schemas|definitions)\/(.+)$/)
1652
+ if (m) {
1653
+ cmplist.push(m[2])
1654
+ cmpset.add(m[2])
1655
+ }
1656
+ })
1657
+ })
1658
+ })
1659
+ // console.log('FOUNDCMPS', cmps)
1660
+ return (opts?.uniq ? Array.from(cmpset) : cmplist).map(n =>
1661
+ ({ cmp: canonize(n), origcmp: n }))
1662
+ }
1663
+
1664
+
1665
+ function makeMethodEntityDesc(desc: Record<string, any>): MethodEntityDesc {
1666
+ let ment: MethodEntityDesc = {
1667
+ cmp: desc.cmp ?? null,
1668
+ origcmp: desc.origcmp ?? null,
1669
+ origcmpref: desc.origcmpref ?? null,
1670
+
1671
+ ref: desc.ref ?? '',
1672
+ why_cmp: desc.why_cmp ?? [],
1673
+ cmpoccur: desc.cmpoccur ?? 0,
1674
+ path_rate: desc.path_rate ?? 0,
1675
+ method_rate: desc.method_rate ?? 0,
1676
+ entname: desc.entname ?? '',
1677
+ why_op: desc.why_op ?? [],
1678
+ rename: desc.rename ?? { param: {} },
1679
+ why_rename: desc.why_rename ?? { why_param: {} },
1680
+ rename_orig: desc.rename_orig ?? [],
1681
+ opname: desc.opname ?? '',
1682
+ why_opname: desc.why_opname ?? [],
1683
+ }
1684
+ return ment
1685
+ }
1686
+
1687
+
1688
+ function findPotentialSchemaRefs(pathStr: string, methodName: string, responses: any) {
1689
+ const xrefs: string[] = []
1690
+ const rescodes = ['200', '201']
1691
+ for (let rescode of rescodes) {
1692
+ const schema = getResponseSchema(responses[rescode])
1693
+ if (null != schema) {
1694
+ if (null != schema['x-ref']) {
1695
+ xrefs.push(schema['x-ref'])
1696
+ }
1697
+ else if ('array' === schema.type && null != schema.items?.['x-ref']) {
1698
+ xrefs.push(schema.items?.['x-ref'])
1699
+ }
1700
+ }
446
1701
  }
1702
+
1703
+ debugpath(pathStr, methodName, 'POTENTIAL-SCHEMA-REFS', xrefs)
1704
+ return xrefs
447
1705
  }
448
1706
 
449
1707
 
1708
+ function hasMethod(def: any, pathStr: string, methodName: string) {
1709
+ const pathDef = def?.paths?.[pathStr]
1710
+ const found = (
1711
+ null != pathDef
1712
+ && (
1713
+ null != pathDef[methodName.toLowerCase()]
1714
+ || null != pathDef[methodName.toUpperCase()]
1715
+ )
1716
+ )
1717
+ console.log('hasMethod', pathStr, methodName, found)
1718
+ return found
1719
+ }
1720
+
1721
+
1722
+
1723
+ /*
1724
+ // Some decisions require the full list of potential entities.
1725
+ function reviewEntityDescs(ctx: ApiDefContext, result: any) {
1726
+ const guide: Guide = result.data.guide
1727
+ const metrics = guide.metrics
1728
+ const entityDescs = result.data.work.entmap
1729
+
1730
+ if (0 < metrics.count.cmp) {
1731
+ items(entityDescs).map(([entname, entdesc]: [string, EntityDesc]) => {
1732
+
1733
+ // Entities with a single path and single op and no cmp are suspicious
1734
+
1735
+ const pathmap = entdesc.path
1736
+ const pathcount = size(pathmap)
1737
+ const hascmp = null != entdesc.cmp?.namedesc
1738
+
1739
+ if (
1740
+ 1 === pathcount
1741
+ && !hascmp
1742
+ ) {
1743
+ let pathdesc: EntityPathDesc = each(pathmap)[0]
1744
+ const pathStr = (pathdesc as any).key$
1745
+
1746
+ if (1 === size(pathdesc.op)) {
1747
+ let op = pathdesc.op
1748
+
1749
+ // console.log('REVIEW', entdesc.name, pathcount, hascmp, op)
1750
+
1751
+ if (op.create) {
1752
+
1753
+ // Entities without "good" components
1754
+ if (
1755
+ entname.includes('_')
1756
+ && pathdesc.pm.expr.endsWith('p/t/')
1757
+ ) {
1758
+ const lastpart = canonize(getelem(pathdesc.pm, -1))
1759
+ const tgtent = entityDescs[lastpart]
1760
+
1761
+ // console.log('REVIEW', entname, entdesc.cmp, size(pathmap), lastpart, realent)
1762
+
1763
+ if (
1764
+ null != tgtent
1765
+ && tgtent.name !== entname
1766
+ && (
1767
+ null == tgtent.cmp
1768
+ || lastpart == tgtent.cmp
1769
+ )
1770
+ ) {
1771
+
1772
+ // Actually a known component
1773
+ // console.dir(entdesc, { depth: null })
1774
+
1775
+
1776
+ const realent = guide.entity[entname]
1777
+ const realpathmap = realent.path
1778
+ let realpath = realpathmap[pathStr]
1779
+
1780
+ if (null == realpath) {
1781
+ realpath = realpathmap[pathStr] = pathdesc
1782
+ }
1783
+ else if (null == realpath.op?.create) {
1784
+ realpath.op = (realpath.op ?? {})
1785
+ realpath.op.create = pathdesc.op.create
1786
+ }
1787
+
1788
+ realpath.op.create.why_op =
1789
+ 'was/create/A:' + entname + ':' + realpath.op.create.why_op
1790
+
1791
+ delete entityDescs[entname]
1792
+
1793
+ // console.log('REPLACE', entname, realent.name, realpath)
1794
+ }
1795
+
1796
+ }
1797
+ }
1798
+
1799
+ else if (op.remove) {
1800
+ const otherents: EntityDesc[] = each(entityDescs)
1801
+ .filter((ed: EntityDesc) => ed !== entdesc && each(ed.path)
1802
+ .filter(epd => epd.key$ === pathStr).length)
1803
+
1804
+ const otherent = 1 === otherents.length ? otherents[0] : null
1805
+
1806
+ // console.log('OTHERENT', pathStr, otherents.length, otherent)
1807
+
1808
+ if (null != otherent && null != otherent.cmp) {
1809
+ const otherpath = otherent.path[pathStr]
1810
+
1811
+ if (null == otherpath.op.remove) {
1812
+ otherpath.op.remove = op.remove
1813
+ otherpath.op.remove.why_op =
1814
+ 'was/delete/A:' + entname + ':' + op.remove.why_op
1815
+ delete entityDescs[entname]
1816
+ }
1817
+ }
1818
+ }
1819
+
1820
+ debugpath(pathdesc.pm.path, null,
1821
+ 'REVIEW-ENTITY', formatJSONIC(entdesc, { hsepd: 0, $: true, color: true }))
1822
+
1823
+
1824
+ }
1825
+ }
1826
+ })
1827
+ }
1828
+ }
1829
+ */
1830
+
450
1831
 
451
1832
  export {
452
1833
  heuristic01