@voxgig/apidef 2.0.2 → 2.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.
package/src/guide.ts CHANGED
@@ -3,38 +3,65 @@ import Path from 'node:path'
3
3
 
4
4
  import { File, Content, each } from 'jostraca'
5
5
 
6
+ import { merge } from '@voxgig/struct'
7
+
6
8
 
7
9
  import { heuristic01 } from './guide/heuristic01'
8
10
 
11
+ import {
12
+ getdlog,
13
+ } from './utility'
14
+
15
+
16
+ // Log non-fatal wierdness.
17
+ const dlog = getdlog('apidef', __filename)
9
18
 
10
19
  async function resolveGuide(ctx: any) {
11
- let guide: Record<string, any> = ctx.model.main.api.guide
20
+ let baseguide: Record<string, any> = {}
21
+ let override: Record<string, any> = ctx.model.main.api.guide
12
22
 
13
23
  if ('heuristic01' === ctx.opts.strategy) {
14
- guide = await heuristic01(ctx)
24
+ baseguide = await heuristic01(ctx)
15
25
  }
16
26
  else {
17
27
  throw new Error('Unknown guide strategy: ' + ctx.opts.strategy)
18
28
  }
19
29
 
30
+ // Override generated base guide with custom hints
31
+ let guide = merge([{}, baseguide, override])
32
+
33
+ // TODO: this is a hack!!!
34
+ // Instead, update @voxgig/model, so that builders can request a reload of the entire
35
+ // model. This allows builders to modify the model for later buidlers
36
+ // during a single generation pass.
37
+
38
+
20
39
  guide = cleanGuide(guide)
21
40
 
22
- ctx.model.main.api.guide = guide
41
+
42
+ // TODO: FIX: sdk.jsonic should have final version of guide
43
+ if (ctx.model.main?.api) {
44
+ ctx.model.main.api.guide = guide
45
+ }
46
+ else {
47
+ dlog('missing', 'ctx.model.main.api')
48
+ }
23
49
 
24
50
  const guideFile =
25
51
  Path.join(ctx.opts.folder,
26
- (null == ctx.opts.outprefix ? '' : ctx.opts.outprefix) + 'guide.jsonic')
52
+ (null == ctx.opts.outprefix ? '' : ctx.opts.outprefix) + 'base-guide.jsonic')
27
53
 
28
54
  const guideBlocks = [
29
55
  '# Guide',
30
56
  '',
31
57
  'main: api: guide: { ',
32
- '',
58
+
33
59
  ]
34
60
 
35
61
 
36
- guideBlocks.push(...each(guide.entity, (entity, entityname) => {
37
- guideBlocks.push(`\nentity: ${entityname}: {`)
62
+ guideBlocks.push(...each(baseguide.entity, (entity, entityname) => {
63
+ guideBlocks.push(`
64
+ entity: ${entityname}: {`)
38
65
 
39
66
  each(entity.path, (path, pathname) => {
40
67
  guideBlocks.push(` path: '${pathname}': op: {`)
@@ -58,8 +85,8 @@ async function resolveGuide(ctx: any) {
58
85
  const guideSrc = guideBlocks.join('\n')
59
86
 
60
87
  return () => {
61
- File({ name: Path.basename(guideFile) }, () => Content(guideSrc))
62
-
88
+ // Save base guide for reference
89
+ File({ name: '../def/' + Path.basename(guideFile) }, () => Content(guideSrc))
63
90
  }
64
91
  }
65
92
 
@@ -70,7 +97,19 @@ function cleanGuide(guide: Record<string, any>): Record<string, any> {
70
97
  entity: {}
71
98
  }
72
99
 
100
+ const exclude_entity = guide.exclude?.entity?.split(',') || []
101
+ const include_entity = guide.include?.entity?.split(',') || []
102
+
73
103
  each(guide.entity, (entity: any, name: string) => {
104
+ if (exclude_entity.includes(name)) {
105
+ return
106
+ }
107
+ if (exclude_entity.includes('*')) {
108
+ if (!include_entity.includes(name)) {
109
+ return
110
+ }
111
+ }
112
+
74
113
  let ent: any = clean.entity[name] = clean.entity[name] = { name, path: {} }
75
114
 
76
115
  each(entity.path, (path: any, pathname: string) => {
package/src/parse.ts CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  import { bundleFromString, createConfig } from '@redocly/openapi-core'
4
4
 
5
+ import { each, snakify } from 'jostraca'
6
+
7
+
8
+ import { depluralize } from './utility'
5
9
 
6
10
 
7
11
  // Parse an API definition source into a JSON sructure.
@@ -16,12 +20,51 @@ async function parse(kind: string, source: any, meta?: any) {
16
20
 
17
21
 
18
22
  async function parseOpenAPI(source: any, meta?: any) {
19
- const config = await createConfig(meta?.config || {})
20
- let bundle
23
+ const base = meta?.config || {}
24
+ const config: any = await createConfig(base)
21
25
 
22
- bundle = await bundleFromString({
26
+ // First pass: parse without dereferencing to preserve $refs
27
+ const bundleWithRefs = await bundleFromString({
23
28
  source,
24
29
  config,
30
+ dereference: false,
31
+ })
32
+
33
+ // Walk the tree and add x-ref properties
34
+ const seen = new WeakSet()
35
+ let refCount = 0
36
+
37
+ function addXRefs(obj: any, path: string = '') {
38
+ if (!obj || typeof obj !== 'object' || seen.has(obj)) return
39
+ seen.add(obj)
40
+
41
+ if (Array.isArray(obj)) {
42
+ obj.forEach((item, index) => addXRefs(item, `${path}[${index}]`))
43
+ } else {
44
+ // Check for $ref property
45
+ if (obj.$ref && typeof obj.$ref === 'string') {
46
+ obj['x-ref'] = obj.$ref
47
+ refCount++
48
+ }
49
+
50
+ // Recursively process all properties
51
+ for (const [key, value] of Object.entries(obj)) {
52
+ if (value && typeof value === 'object') {
53
+ addXRefs(value, path ? `${path}.${key}` : key)
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ addXRefs(bundleWithRefs.bundle.parsed)
60
+
61
+ // Serialize back to string with x-refs preserved
62
+ const sourceWithXRefs = JSON.stringify(bundleWithRefs.bundle.parsed)
63
+
64
+ // Second pass: parse with dereferencing
65
+ const bundle = await bundleFromString({
66
+ source: sourceWithXRefs,
67
+ config,
25
68
  dereference: true,
26
69
  })
27
70
 
@@ -31,6 +74,65 @@ async function parseOpenAPI(source: any, meta?: any) {
31
74
  }
32
75
 
33
76
 
77
+
78
+
79
+
80
+
81
+
82
+ // Make consistent changes to support semantic entities.
83
+ function rewrite(def: any) {
84
+ const paths = def.paths
85
+ each(paths, (path) => {
86
+ each(path.parameters, (param: any) => {
87
+
88
+ let new_param = param.name
89
+ let new_path = path.key$
90
+
91
+ // Rename param if nane is "id", and not the final param.
92
+ // Rewrite /foo/{id}/bar as /foo/{foo_id}/bar.
93
+ // Avoids ambiguity with bar id.
94
+ if (param.name.match(/^id$/i)) {
95
+ let m = path.key$.match(/\/([^\/]+)\/{id\}\/[^\/]/)
96
+
97
+ if (m) {
98
+ const parent = depluralize(snakify(m[1]))
99
+ new_param = `${parent}_id`
100
+ new_path = path.key$.replace('{id}', '{' + new_param + '}')
101
+ }
102
+ }
103
+ else {
104
+ new_param = depluralize(snakify(param.name))
105
+ new_path = path.key$.replace('{' + param.name + '}', '{' + new_param + '}')
106
+ }
107
+
108
+ let pathdef = paths[path.key$]
109
+ delete paths[path.key$]
110
+
111
+ paths[new_path] = pathdef
112
+ path.key$ = new_path
113
+
114
+ param.name = new_param
115
+ })
116
+ })
117
+
118
+
119
+ sortkeys(def, 'paths')
120
+ sortkeys(def, 'components')
121
+
122
+ return def
123
+ }
124
+
125
+
126
+ function sortkeys(obj: any, prop: string) {
127
+ const sorted: any = {}
128
+ const sorted_keys = Object.keys(obj[prop]).sort()
129
+ for (let sk of sorted_keys) {
130
+ sorted[sk] = obj[prop][sk]
131
+ }
132
+ obj[prop] = sorted
133
+ }
134
+
34
135
  export {
35
- parse
136
+ parse,
137
+ rewrite,
36
138
  }
@@ -0,0 +1,28 @@
1
+
2
+ import { each, getx } from 'jostraca'
3
+
4
+ import type { TransformResult } from '../transform'
5
+
6
+ import { walk } from '@voxgig/struct'
7
+
8
+
9
+
10
+ const cleanTransform = async function(
11
+ ctx: any,
12
+ ): Promise<TransformResult> {
13
+ const { apimodel } = ctx
14
+
15
+ walk(apimodel, (k: any, v: any) => {
16
+ if ('string' === typeof k && k.includes('$')) {
17
+ return undefined
18
+ }
19
+ return v
20
+ })
21
+
22
+ return { ok: true, msg: 'clean' }
23
+ }
24
+
25
+
26
+ export {
27
+ cleanTransform
28
+ }
@@ -69,7 +69,8 @@ function fieldbuild(
69
69
  field.name = property.key$
70
70
  fixName(field, field.name)
71
71
 
72
- field.type = property.type
72
+ // field.type = property.type
73
+ resolveFieldType(entityModel, field, property)
73
74
  fixName(field, field.type, 'type')
74
75
 
75
76
  field.short = property.description
@@ -93,6 +94,31 @@ function fieldbuild(
93
94
  }
94
95
 
95
96
 
97
+ // Resovles a heuristic "primary" type which subsumes the more detailed type.
98
+ // The primary type is only: string, number, boolean, null, object, array
99
+ function resolveFieldType(entity: any, field: any, property: any) {
100
+ const ptt = typeof property.type
101
+
102
+ if ('string' === ptt) {
103
+ field.type = property.type
104
+ }
105
+ else if (Array.isArray(property.type)) {
106
+ field.type =
107
+ (property.type.filter((t: string) => 'null' != t).sort()[0]) ||
108
+ property.type[0] ||
109
+ 'string'
110
+ field.typelist = property.type
111
+ }
112
+ else if ('undefined' === ptt && null != property.enum) {
113
+ field.type = 'string'
114
+ field.enum = property.enum
115
+ }
116
+ else {
117
+ throw new Error(
118
+ `APIDEF: Unsupported property type: ${property.type} (${entity.name}.${field.name})`)
119
+ }
120
+ }
121
+
96
122
 
97
123
  export {
98
124
  fieldTransform
@@ -1,11 +1,14 @@
1
1
 
2
2
 
3
- import { each, getx } from 'jostraca'
3
+ import { each, getx, snakify } from 'jostraca'
4
+
5
+ import { transform, setprop, getprop } from '@voxgig/struct'
4
6
 
5
7
  import type { TransformResult } from '../transform'
6
8
 
7
9
  import { fixName, OPKIND } from '../transform'
8
10
 
11
+ import { depluralize } from '../utility'
9
12
 
10
13
 
11
14
  const operationTransform = async function(
@@ -31,7 +34,6 @@ const operationTransform = async function(
31
34
  entity: any,
32
35
  model: any
33
36
  ) => {
34
-
35
37
  const paramSpec: any = paramMap[paramDef.name] = {
36
38
  required: paramDef.required
37
39
  }
@@ -143,12 +145,13 @@ const operationTransform = async function(
143
145
  content: any,
144
146
  schema: any,
145
147
  propkeys: any
146
- ) => {
148
+ ): [any, string] => {
147
149
  let why = 'default'
148
150
  let transform: any = '`body`'
151
+ const properties = schema?.properties
149
152
 
150
- if (null == content || null == schema || null == propkeys) {
151
- return transform
153
+ if (null == content || null == schema || null == propkeys || null == properties) {
154
+ return [transform, why]
152
155
  }
153
156
 
154
157
  const opname = op.key$
@@ -162,9 +165,12 @@ const operationTransform = async function(
162
165
  else {
163
166
  // Use sub property that is an array
164
167
  for (let pk of propkeys) {
165
- if ('array' === schema.properties[pk]?.type) {
168
+ if ('array' === properties[pk]?.type) {
166
169
  why = 'list-single-array:' + pk
167
170
  transform = '`body.' + pk + '`'
171
+
172
+ // TODO: if each item has prop === name of entity, use that, get with $EACH
173
+
168
174
  break
169
175
  }
170
176
  }
@@ -173,14 +179,14 @@ const operationTransform = async function(
173
179
  }
174
180
  else {
175
181
  if ('object' === schema.type) {
176
- if (null == schema.properties.id) {
182
+ if (null == properties.id) {
177
183
  if (1 === propkeys.length) {
178
184
  why = 'map-single-prop:' + propkeys[0]
179
185
  transform = '`body.' + propkeys[0] + '`'
180
186
  }
181
187
  else {
182
188
  for (let pk of propkeys) {
183
- if (schema.properties[pk].properties?.id) {
189
+ if (properties[pk]?.properties?.id) {
184
190
  why = 'map-sub-prop:' + pk
185
191
  transform = '`body.' + pk + '`'
186
192
  break
@@ -205,12 +211,13 @@ const operationTransform = async function(
205
211
  content: any,
206
212
  schema: any,
207
213
  propkeys: any
208
- ) => {
209
- let transform: any = '`data`'
214
+ ): [any, string] => {
215
+ let transform: any = '`reqdata`'
210
216
  let why = 'default'
217
+ const properties = schema?.properties
211
218
 
212
- if (null == content || null == schema || null == propkeys) {
213
- return transform
219
+ if (null == content || null == schema || null == propkeys || null == properties) {
220
+ return [transform, why]
214
221
  }
215
222
 
216
223
  const opname = op.key$
@@ -219,14 +226,14 @@ const operationTransform = async function(
219
226
  if ('array' !== schema.type) {
220
227
  if (1 === propkeys.length) {
221
228
  why = 'list-single-prop:' + propkeys[0]
222
- transform = { [propkeys[0]]: '`data`' }
229
+ transform = { [propkeys[0]]: '`reqdata`' }
223
230
  }
224
231
  else {
225
232
  // Use sub property that is an array
226
233
  for (let pk of propkeys) {
227
- if ('array' === schema.properties[pk]?.type) {
234
+ if ('array' === properties[pk]?.type) {
228
235
  why = 'list-single-array:' + pk
229
- transform = { [pk]: '`data`' }
236
+ transform = { [pk]: '`reqdata`' }
230
237
  break
231
238
  }
232
239
  }
@@ -235,16 +242,16 @@ const operationTransform = async function(
235
242
  }
236
243
  else {
237
244
  if ('object' === schema.type) {
238
- if (null == schema.properties.id) {
245
+ if (null == properties.id) {
239
246
  if (1 === propkeys.length) {
240
247
  why = 'map-single-prop:' + propkeys[0]
241
- transform = { [propkeys[0]]: '`data`' }
248
+ transform = { [propkeys[0]]: '`reqdata`' }
242
249
  }
243
250
  else {
244
251
  for (let pk of propkeys) {
245
- if (schema.properties[pk].properties?.id) {
252
+ if (properties[pk]?.properties?.id) {
246
253
  why = 'map-sub-prop:' + pk
247
- transform = { [pk]: '`data`' }
254
+ transform = { [pk]: '`reqdata`' }
248
255
  break
249
256
  }
250
257
  }
@@ -269,8 +276,10 @@ const operationTransform = async function(
269
276
  const [reqform, reqform_COMMENT] =
270
277
  resolveTransform(entityModel, op, kind, 'reqform', pathdef)
271
278
 
272
- const opModel = entityModel.op[opname] = {
279
+ const opModel = {
273
280
  path: path.key$,
281
+ pathalt: ([] as any[]),
282
+
274
283
  method,
275
284
  kind,
276
285
  param: {},
@@ -289,24 +298,64 @@ const operationTransform = async function(
289
298
 
290
299
  fixName(opModel, op.key$)
291
300
 
301
+ let params: any[] = []
302
+
292
303
  // Params are in the path
293
304
  if (0 < path.params$.length) {
294
- let params = getx(pathdef[method], 'parameters?in=path') || []
295
- if (Array.isArray(params)) {
296
- params.reduce((a: any, p: any) =>
297
- (paramBuilder(a, p, opModel, entityModel,
298
- pathdef, op, path, entity, model), a), opModel.param)
299
- }
305
+ let sharedparams = getx(pathdef, 'parameters?in=path') || []
306
+ params = sharedparams.concat(
307
+ getx(pathdef[method], 'parameters?in=path') || []
308
+ )
309
+
310
+ // if (Array.isArray(params)) {
311
+ params.reduce((a: any, p: any) =>
312
+ (paramBuilder(a, p, opModel, entityModel,
313
+ pathdef, op, path, entity, model), a), opModel.param)
314
+ //}
300
315
  }
301
316
 
302
317
  // Queries are after the ?
303
- let queries = getx(pathdef[op.val$], 'parameters?in!=path') || []
304
- if (Array.isArray(queries)) {
305
- queries.reduce((a: any, p: any) =>
306
- (queryBuilder(a, p, opModel, entityModel,
307
- pathdef, op, path, entity, model), a), opModel.query)
318
+ let sharedqueries = getx(pathdef, 'parameters?in!=path') || []
319
+ let queries = sharedqueries.concat(getx(pathdef[method], 'parameters?in!=path') || [])
320
+ queries.reduce((a: any, p: any) =>
321
+ (queryBuilder(a, p, opModel, entityModel,
322
+ pathdef, op, path, entity, model), a), opModel.query)
323
+
324
+ let pathalt: any[] = []
325
+ const pathselector = makePathSelector(path.key$) // , params)
326
+ let before = false
327
+
328
+ if (null != entityModel.op[opname]) {
329
+ pathalt = entityModel.op[opname].pathalt
330
+
331
+ // Ordering for pathalts: most to least paramrs, then alphanumberic
332
+ for (let i = 0; i < pathalt.length; i++) {
333
+ let current = pathalt[i]
334
+ before =
335
+ pathselector.pn$ > current.pn$ ||
336
+ (pathselector.pn$ === current.pn$ &&
337
+ pathselector.path <= current.path)
338
+
339
+ if (before) {
340
+ pathalt = [
341
+ ...pathalt.slice(0, i),
342
+ pathselector,
343
+ ...pathalt.slice(i),
344
+ ]
345
+ break
346
+ }
347
+ }
348
+ }
349
+
350
+ if (!before) {
351
+ pathalt.push(pathselector)
308
352
  }
309
353
 
354
+ opModel.path = pathalt[pathalt.length - 1].path
355
+ opModel.pathalt = pathalt
356
+
357
+ entityModel.op[opname] = opModel
358
+
310
359
  return opModel
311
360
  },
312
361
 
@@ -333,6 +382,22 @@ const operationTransform = async function(
333
382
 
334
383
  }
335
384
 
385
+ /*
386
+ console.dir(
387
+ transform({ guide }, {
388
+ entity: {
389
+ '`$PACK`': ['guide.entity', {
390
+ '`$KEY`': 'name',
391
+ op: {
392
+ // load: ['`$IF`', ['`$SELECT`',{path:{'`$ANY`':{op:{load:'`$EXISTS`'}}}}], {
393
+ load: ['`$IF`', 'path.*.op.load', {
394
+ path: () => 'foo'
395
+ }]
396
+ }
397
+ }]
398
+ }
399
+ }), { depth: null })
400
+ */
336
401
 
337
402
  each(guide.entity, (guideEntity: any) => {
338
403
  let opcount = 0
@@ -348,6 +413,8 @@ const operationTransform = async function(
348
413
  opcount++
349
414
  }
350
415
  })
416
+
417
+
351
418
  })
352
419
 
353
420
  msg += guideEntity.name + '=' + opcount + ' '
@@ -357,6 +424,26 @@ const operationTransform = async function(
357
424
  }
358
425
 
359
426
 
427
+ function makePathSelector(path: string) { // , params: any[]) {
428
+ let out: any = { path }
429
+
430
+ let pn$ = 0
431
+ for (const m of path.matchAll(/\/[^\/]+\/{([^}]+)\}/g)) {
432
+ const paramName = depluralize(snakify(m[1]))
433
+ out[paramName] = true
434
+ pn$++
435
+ }
436
+ out.pn$ = pn$
437
+
438
+ // for (let p of params) {
439
+ // setprop(out, p.name, getprop(out, p.name, false))
440
+ // console.log('SP', p.name, out[p.name])
441
+ // }
442
+
443
+ return out
444
+ }
445
+
446
+
360
447
  export {
361
448
  operationTransform
362
449
  }
package/src/transform.ts CHANGED
@@ -79,6 +79,7 @@ const GuideShape = Gubu({
79
79
  type Guide = ReturnType<typeof GuideShape>
80
80
 
81
81
 
82
+ /*
82
83
  async function resolveTransforms(ctx: TransformCtx): Promise<TransformSpec> {
83
84
  const { log, model: { main: { api: { guide } } } } = ctx
84
85
 
@@ -89,27 +90,27 @@ async function resolveTransforms(ctx: TransformCtx): Promise<TransformSpec> {
89
90
  // TODO: parameterize
90
91
  const defkind = 'openapi'
91
92
  const transformNames = guide.control.transform[defkind].order
92
- .split(/\s*,\s*/)
93
+ .split(/\s*,\s* /)
93
94
  .map((t: string) => t.trim())
94
- .filter((t: string) => '' != t)
95
+ .filter((t: string) => '' != t)
95
96
 
96
- log.info({
97
- point: 'transform', note: 'order: ' + transformNames.join(';'),
98
- order: transformNames
99
- })
97
+ log.info({
98
+ point: 'transform', note: 'order: ' + transformNames.join(';'),
99
+ order: transformNames
100
+ })
100
101
 
101
- try {
102
- for (const tn of transformNames) {
103
- log.debug({ what: 'transform', transform: tn, note: tn })
104
- const transform = await resolveTransform(tn, ctx)
105
- tspec.transform.push(transform)
106
- }
107
- }
108
- catch (err: any) {
109
- throw err
102
+ try {
103
+ for (const tn of transformNames) {
104
+ log.debug({ what: 'transform', transform: tn, note: tn })
105
+ const transform = await resolveTransform(tn, ctx)
106
+ tspec.transform.push(transform)
110
107
  }
108
+ }
109
+ catch (err: any) {
110
+ throw err
111
+ }
111
112
 
112
- return tspec
113
+ return tspec
113
114
  }
114
115
 
115
116
 
@@ -195,13 +196,19 @@ async function processTransforms(
195
196
 
196
197
  return pres
197
198
  }
199
+ */
198
200
 
199
201
 
200
202
 
201
203
  function fixName(base: any, name: string, prop = 'name') {
202
- base[prop.toLowerCase()] = name.toLowerCase()
203
- base[camelify(prop)] = camelify(name)
204
- base[prop.toUpperCase()] = name.toUpperCase()
204
+ if (null != base && 'object' === typeof base && 'string' === typeof name) {
205
+ base[prop.toLowerCase()] = name.toLowerCase()
206
+ base[camelify(prop)] = camelify(name)
207
+ base[prop.toUpperCase()] = name.toUpperCase()
208
+ }
209
+ else {
210
+ // record to a "wierds" log
211
+ }
205
212
  }
206
213
 
207
214
 
@@ -221,6 +228,6 @@ export {
221
228
  fixName,
222
229
  OPKIND,
223
230
  GuideShape,
224
- resolveTransforms,
225
- processTransforms,
231
+ // resolveTransforms,
232
+ // processTransforms,
226
233
  }