@voxgig/apidef 1.1.1 → 1.3.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/apidef.ts CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  import * as Fs from 'node:fs'
4
4
  import Path from 'node:path'
5
-
5
+ import { inspect } from 'node:util'
6
6
 
7
7
  import { bundleFromString, createConfig } from '@redocly/openapi-core'
8
8
  import { FSWatcher } from 'chokidar'
9
9
  import { Aontu, Context } from 'aontu'
10
-
10
+ import { Gubu, Open, Any } from 'gubu'
11
11
  import { prettyPino, Pino } from '@voxgig/util'
12
12
 
13
13
 
@@ -19,88 +19,94 @@ import {
19
19
 
20
20
 
21
21
  type ApiDefOptions = {
22
+ def?: string
22
23
  fs?: any
23
24
  pino?: ReturnType<typeof Pino>
24
25
  debug?: boolean | string
26
+ folder?: string
27
+ meta?: Record<string, any>
28
+ outprefix?: string
25
29
  }
26
30
 
27
31
 
28
- type ApiDefSpec = {
29
- def: string
30
- model: string,
31
- kind: string,
32
- meta: Record<string, any>,
33
- guide?: any
34
- }
32
+ const ModelShape = Gubu({
33
+ def: String,
34
+ main: {
35
+ guide: {},
36
+ sdk: {},
37
+ def: {},
38
+ api: {},
39
+ }
40
+ })
41
+ const OpenModelShape = Gubu(Open(ModelShape))
42
+
43
+ type Model = ReturnType<typeof ModelShape>
44
+
45
+
46
+ const BuildShape = Gubu({
47
+ spec: {
48
+ base: '',
49
+ path: '',
50
+ debug: '',
51
+ use: {},
52
+ res: [],
53
+ require: '',
54
+ log: {},
55
+ fs: Any()
56
+ }
57
+ })
58
+ const OpenBuildShape = Gubu(Open(BuildShape))
35
59
 
60
+ type Build = ReturnType<typeof BuildShape>
36
61
 
37
- function ApiDef(opts: ApiDefOptions = {}) {
62
+
63
+
64
+ function ApiDef(opts: ApiDefOptions) {
38
65
  const fs = opts.fs || Fs
39
66
  const pino = prettyPino('apidef', opts)
40
67
 
41
68
  const log = pino.child({ cmp: 'apidef' })
42
69
 
43
70
 
44
- async function watch(spec: ApiDefSpec) {
45
- log.info({ point: 'watch-start' })
46
- log.debug({ point: 'watch-spec', spec })
47
-
48
- await generate(spec)
49
-
50
- const fsw = new FSWatcher()
51
-
52
- fsw.on('change', (...args: any[]) => {
53
- log.trace({ watch: 'change', file: args[0] })
54
- generate(spec)
55
- })
56
-
57
- log.trace({ watch: 'add', what: 'def', file: spec.def })
58
- fsw.add(spec.def)
59
-
60
- log.trace({ watch: 'add', what: 'guide', file: spec.guide })
61
- fsw.add(spec.guide)
62
- }
71
+ async function generate(spec: any) {
72
+ const start = Date.now()
63
73
 
74
+ const model: Model = OpenModelShape(spec.model)
75
+ const build: Build = OpenBuildShape(spec.build)
64
76
 
65
- async function generate(spec: ApiDefSpec) {
66
- const start = Date.now()
77
+ const buildspec = build.spec
67
78
 
68
- // TODO: validate spec
79
+ let defpath = model.def
69
80
 
70
- const defpath = Path.normalize(spec.def)
81
+ // TOOD: defpath should be independently defined
82
+ defpath = Path.join(buildspec.base, '..', 'def', defpath)
71
83
 
72
- log.info({ point: 'generate-start', note: 'defpath', defpath, start })
73
- log.debug({ point: 'generate-spec', spec })
84
+ log.info({
85
+ point: 'generate-start',
86
+ note: defpath.replace(process.cwd(), '.'), defpath, start
87
+ })
74
88
 
75
89
  // TODO: Validate spec
76
90
  const ctx = {
77
91
  log,
78
92
  spec,
79
- guide: {},
80
93
  opts,
81
94
  util: { fixName },
82
- defpath: Path.dirname(defpath)
83
- }
84
-
85
-
86
-
87
- const guide = await resolveGuide(spec, opts)
88
-
89
- if (null == guide) {
90
- return
95
+ defpath: Path.dirname(defpath),
96
+ model,
91
97
  }
92
98
 
93
-
94
- log.debug({ point: 'guide', guide })
95
-
96
- ctx.guide = guide
97
99
  const transformSpec = await resolveTransforms(ctx)
98
- log.debug({ point: 'transform', spec: transformSpec })
100
+
101
+ log.debug({
102
+ point: 'transform', spec: transformSpec,
103
+ note: log.levelVal <= 20 ? inspect(transformSpec) : ''
104
+ })
99
105
 
100
106
 
101
107
  let source
102
108
  try {
103
- source = fs.readFileSync(spec.def, 'utf8')
109
+ source = fs.readFileSync(defpath, 'utf8')
104
110
  }
105
111
  catch (err: any) {
106
112
  log.error({ read: 'fail', what: 'def', file: defpath, err })
@@ -124,7 +130,7 @@ function ApiDef(opts: ApiDefOptions = {}) {
124
130
  }
125
131
 
126
132
 
127
- const model = {
133
+ const apimodel = {
128
134
  main: {
129
135
  api: {
130
136
  entity: {}
@@ -133,36 +139,41 @@ function ApiDef(opts: ApiDefOptions = {}) {
133
139
  },
134
140
  }
135
141
 
136
- try {
137
- const def = bundle.bundle.parsed
138
- const processResult = await processTransforms(ctx, transformSpec, model, def)
142
+ const def = bundle.bundle.parsed
143
+ const processResult = await processTransforms(ctx, transformSpec, apimodel, def)
139
144
 
140
- if (!processResult.ok) {
141
- log.error({ process: 'fail', what: 'transform', result: processResult })
142
- throw new Error('Transform failed: ' + processResult.msg)
143
- }
144
- else {
145
- log.debug({ process: 'result', what: 'transform', result: processResult })
146
- }
147
- }
148
- catch (err: any) {
149
- log.error({ process: 'fail', what: 'transform', err })
150
- throw err
145
+ if (!processResult.ok) {
146
+ log.error({
147
+ fail: 'process', point: 'transform-result',
148
+ result: processResult, note: processResult.msg,
149
+ err: processResult.results[0]?.err
150
+ })
151
+
152
+ return { ok: false, name: 'apidef', processResult }
151
153
  }
152
154
 
153
- const modelapi = { main: { api: model.main.api } }
155
+ const modelapi = { main: { api: apimodel.main.api } }
154
156
  let modelSrc = JSON.stringify(modelapi, null, 2)
155
- modelSrc = modelSrc.substring(1, modelSrc.length - 1)
156
157
 
157
- writeChanged('api-model', spec.model, modelSrc)
158
+ modelSrc =
159
+ '# GENERATED FILE - DO NOT EDIT\n\n' +
160
+ modelSrc.substring(1, modelSrc.length - 1).replace(/\n /g, '\n')
158
161
 
162
+ const modelPath = Path.normalize(spec.config.model)
163
+ // console.log('modelPath', modelPath)
164
+ writeChanged('api-model', modelPath, modelSrc)
159
165
 
160
- const modelBasePath = Path.dirname(spec.model)
161
- const defFilePath = Path.join(modelBasePath, 'def.jsonic')
166
+ const modelBasePath = Path.dirname(modelPath)
167
+ const defFilePath =
168
+ Path.join(modelBasePath,
169
+ (null == opts.outprefix ? '' : opts.outprefix) + 'def-generated.jsonic')
162
170
 
163
- const modelDef = { main: { def: model.main.def } }
171
+ const modelDef = { main: { def: apimodel.main.def } }
164
172
  let modelDefSrc = JSON.stringify(modelDef, null, 2)
165
- modelDefSrc = modelDefSrc.substring(1, modelDefSrc.length - 1)
173
+
174
+ modelDefSrc =
175
+ '# GENERATED FILE - DO NOT EDIT\n\n' +
176
+ modelDefSrc.substring(1, modelDefSrc.length - 1).replace(/\n /g, '\n')
166
177
 
167
178
  writeChanged('def-model', defFilePath, modelDefSrc)
168
179
 
@@ -170,13 +181,13 @@ function ApiDef(opts: ApiDefOptions = {}) {
170
181
 
171
182
  return {
172
183
  ok: true,
173
- model,
184
+ name: 'apidef',
185
+ apimodel,
174
186
  }
175
187
  }
176
188
 
177
189
 
178
-
179
- function writeChanged(what: string, path: string, content: string) {
190
+ function writeChanged(point: string, path: string, content: string) {
180
191
  let exists = false
181
192
  let changed = false
182
193
  let action = ''
@@ -193,10 +204,12 @@ function ApiDef(opts: ApiDefOptions = {}) {
193
204
 
194
205
  changed = existingContent !== content
195
206
 
207
+ // console.log('WC', changed, path, existingContent, content)
208
+
196
209
  log.info({
197
- point: 'write-' + what,
198
- note: 'changed,file',
199
- write: 'file', what, skip: !changed, exists, changed,
210
+ point: 'write-' + point,
211
+ note: (changed ? '' : 'not-') + 'changed ' + path,
212
+ write: 'file', skip: !changed, exists, changed,
200
213
  contentLength: content.length, file: path
201
214
  })
202
215
 
@@ -207,116 +220,49 @@ function ApiDef(opts: ApiDefOptions = {}) {
207
220
  }
208
221
  catch (err: any) {
209
222
  log.error({
210
- fail: action, what, file: path, exists, changed,
223
+ fail: action, point, file: path, exists, changed,
211
224
  contentLength: content.length, err
212
225
  })
226
+ err.__logged__ = true
213
227
  throw err
214
228
  }
215
229
  }
216
230
 
217
-
218
-
219
- async function resolveGuide(spec: any, _opts: any) {
220
- if (null == spec.guide) {
221
- spec.guide = spec.def + '-guide.jsonic'
222
- }
223
-
224
- const path = Path.normalize(spec.guide)
225
- let src: string
226
-
227
- let action = ''
228
- let exists = false
229
- try {
230
-
231
- action = 'exists'
232
- let exists = fs.existsSync(path)
233
-
234
- log.debug({ read: 'file', what: 'guide', file: path, exists })
235
-
236
- if (exists) {
237
- action = 'read'
238
- src = fs.readFileSync(path, 'utf8')
239
- }
240
- else {
241
- src = `
242
- # API Specification Transform Guide
243
-
244
- @"@voxgig/apidef/model/guide.jsonic"
245
-
246
- guide: entity: {
247
-
231
+ return {
232
+ generate,
233
+ }
248
234
  }
249
235
 
250
- guide: control: transform: openapi: order: \`
251
- top,
252
- entity,
253
- operation,
254
- field,
255
- manual,
256
- \`
257
236
 
258
- `
259
- action = 'write'
260
- fs.writeFileSync(path, src)
261
- }
262
- }
263
- catch (err: any) {
264
- log.error({ fail: action, what: 'guide', file: path, exists, err })
265
- throw err
266
- }
267
-
268
- const aopts = { path }
269
- const root = Aontu(src, aopts)
270
- const hasErr = root.err && 0 < root.err.length
271
-
272
- if (hasErr) {
273
- for (let serr of root.err) {
274
- let err: any = new Error('Guide model: ' + serr.msg)
275
- err.cause$ = [serr]
276
237
 
277
- if ('syntax' === serr.why) {
278
- err.uxmsg$ = true
279
- }
238
+ ApiDef.makeBuild = async function(opts: ApiDefOptions) {
239
+ let apidef: any = undefined
280
240
 
281
- log.error({ fail: 'parse', point: 'guide-parse', file: path, err })
241
+ const outprefix = null == opts.outprefix ? '' : opts.outprefix
282
242
 
283
- if (err.uxmsg$) {
284
- return
285
- }
286
- else {
287
- err.rooterrs$ = root.err
288
- throw err
289
- }
290
- }
291
- }
243
+ const config = {
244
+ def: opts.def || 'no-def',
245
+ kind: 'openapi3',
246
+ model: opts.folder ?
247
+ (opts.folder + '/' + outprefix + 'api-generated.jsonic') : 'no-model',
248
+ meta: opts.meta || {},
249
+ }
292
250
 
293
- let genctx = new Context({ root })
294
- const guide = spec.guideModel = root.gen(genctx)
251
+ const build = async function(model: any, build: any, ctx: any) {
295
252
 
296
- // TODO: collect all errors
297
- if (genctx.err && 0 < genctx.err.length) {
298
- const err: any = new Error('Guide build error:\n' +
299
- (genctx.err.map((pe: any) => pe.msg)).join('\n'))
300
- log.error({ fail: 'build', what: 'guide', file: path, err })
301
- err.errs = () => genctx.err
302
- throw err
253
+ if (null == apidef) {
254
+ apidef = ApiDef({
255
+ ...opts,
256
+ pino: build.log,
257
+ })
303
258
  }
304
259
 
305
- const pathParts = Path.parse(path)
306
- spec.guideModelPath = Path.join(pathParts.dir, pathParts.name + '.json')
307
-
308
- const updatedSrc = JSON.stringify(guide, null, 2)
309
-
310
- writeChanged('guide-model', spec.guideModelPath, updatedSrc)
311
-
312
- return guide
260
+ return await apidef.generate({ model, build, config })
313
261
  }
314
262
 
263
+ build.step = 'pre'
315
264
 
316
- return {
317
- watch,
318
- generate,
319
- }
265
+ return build
320
266
  }
321
267
 
322
268
 
@@ -324,7 +270,7 @@ guide: control: transform: openapi: order: \`
324
270
 
325
271
  export type {
326
272
  ApiDefOptions,
327
- ApiDefSpec,
273
+ // ApiDefSpec,
328
274
  }
329
275
 
330
276
 
package/src/parse.ts ADDED
@@ -0,0 +1,36 @@
1
+ /* Copyright (c) 2024 Voxgig, MIT License */
2
+
3
+ import { bundleFromString, createConfig } from '@redocly/openapi-core'
4
+
5
+
6
+ async function parse(kind: string, source: any) {
7
+ if ('OpenAPI' === kind) {
8
+ return parseOpenAPI(source)
9
+ }
10
+ else {
11
+ throw new Error('@voxgig/apidef-parse: unknown kind: ' + kind)
12
+ }
13
+ }
14
+
15
+
16
+ async function parseOpenAPI(source: any) {
17
+ const config = await createConfig({})
18
+ let bundle
19
+
20
+ bundle = await bundleFromString({
21
+ source,
22
+ config,
23
+ dereference: true,
24
+ })
25
+
26
+ // console.dir(bundle.bundle, { depth: 1 })
27
+
28
+ const def = bundle.bundle.parsed
29
+
30
+ return def
31
+ }
32
+
33
+
34
+ export {
35
+ parse
36
+ }
@@ -2,19 +2,28 @@
2
2
 
3
3
  import { each } from 'jostraca'
4
4
 
5
- import type { TransformCtx, TransformSpec } from '../transform'
5
+ import type { TransformCtx, TransformSpec, TransformResult, Transform, Guide } from '../transform'
6
6
 
7
7
  import { fixName } from '../transform'
8
8
 
9
9
 
10
-
11
- async function entityTransform(ctx: TransformCtx, tspec: TransformSpec, model: any, def: any) {
12
- const { guide: { guide } } = ctx
10
+ const entityTransform: Transform = async function(
11
+ ctx: TransformCtx,
12
+ guide: Guide,
13
+ tspec: TransformSpec,
14
+ model: any,
15
+ def: any
16
+ ): Promise<TransformResult> {
13
17
  let msg = ''
14
18
 
19
+ // console.log('DEF', def)
20
+ // console.log('GUIDE', guide)
21
+
15
22
  each(guide.entity, (guideEntity: any) => {
23
+ const entityName = guideEntity.key$
24
+ ctx.log.debug({ point: 'guide-entity', note: entityName })
16
25
 
17
- const entityModel: any = model.main.api.entity[guideEntity.key$] = {
26
+ const entityModel: any = model.main.api.entity[entityName] = {
18
27
  op: {},
19
28
  field: {},
20
29
  cmd: {},
@@ -27,14 +36,19 @@ async function entityTransform(ctx: TransformCtx, tspec: TransformSpec, model: a
27
36
  fixName(entityModel, guideEntity.key$)
28
37
 
29
38
  each(guideEntity.path, (guidePath: any) => {
30
- const pathdef = def.paths[guidePath.key$]
39
+ const path = guidePath.key$
40
+ const pathdef = def.paths[path]
41
+
42
+ // console.log('APIDEF FIND PATH', guidePath.key$, Object.keys(def.paths),
43
+ // Object.keys(def.paths).includes(guidePath.key$))
31
44
 
32
45
  if (null == pathdef) {
33
- throw new Error('APIDEF: path not found in OpenAPI: ' + guidePath.key$ +
46
+ throw new Error('path not found in OpenAPI: ' + path +
34
47
  ' (entity: ' + guideEntity.name + ')')
35
48
  }
36
49
 
37
- guidePath.parts$ = guidePath.key$.split('/')
50
+ // TODO: is this needed?
51
+ guidePath.parts$ = path.split('/')
38
52
  guidePath.params$ = guidePath.parts$
39
53
  .filter((p: string) => p.startsWith('{'))
40
54
  .map((p: string) => p.substring(1, p.length - 1))
@@ -42,7 +56,6 @@ async function entityTransform(ctx: TransformCtx, tspec: TransformSpec, model: a
42
56
  })
43
57
 
44
58
  msg += guideEntity.name + ' '
45
-
46
59
  })
47
60
 
48
61
  return { ok: true, msg }
@@ -2,31 +2,35 @@
2
2
 
3
3
  import { each, getx } from 'jostraca'
4
4
 
5
- import type { TransformCtx, TransformSpec } from '../transform'
5
+ import type { TransformCtx, TransformSpec, TransformResult, Transform, Guide } from '../transform'
6
6
 
7
7
  import { fixName } from '../transform'
8
8
 
9
9
 
10
10
 
11
- async function fieldTransform(
11
+ const fieldTransform: Transform = async function(
12
12
  ctx: TransformCtx,
13
+ guide: Guide,
13
14
  tspec: TransformSpec,
14
15
  model: any,
15
16
  def: any
16
- ) {
17
- const { guide: { guide } } = ctx
17
+ ): Promise<TransformResult> {
18
+
18
19
  let msg = 'fields: '
19
20
 
20
21
  each(guide.entity, (guideEntity: any) => {
22
+ const entityName = guideEntity.key$
23
+ const entityModel = model.main.api.entity[entityName]
21
24
 
22
- const entityModel = model.main.api.entity[guideEntity.key$]
23
25
  let fieldCount = 0
24
26
  each(guideEntity.path, (guidePath: any) => {
25
- const pathdef = def.paths[guidePath.key$]
27
+ const path = guidePath.key$
28
+ const pathdef = def.paths[path]
26
29
 
27
30
  each(guidePath.op, (op: any) => {
31
+ const opname = op.key$
28
32
 
29
- if ('load' === op.key$) {
33
+ if ('load' === opname) {
30
34
  fieldCount = fieldbuild(entityModel, pathdef, op, guidePath, guideEntity, model)
31
35
  }
32
36
 
@@ -127,7 +131,7 @@ guide: transform: customField: {
127
131
  guide: entity: {
128
132
  pet: path: {
129
133
  '/pet/{petId}': {
130
- op:{ load: 'get', create: 'post', save: 'put' }
134
+ op:{ load: 'get', create: 'post', update: 'put' }
131
135
  }
132
136
  }
133
137
  pet: test: {
@@ -3,13 +3,20 @@ import { Jsonic } from '@jsonic/jsonic-next'
3
3
 
4
4
  import { each, getx } from 'jostraca'
5
5
 
6
- import type { TransformCtx, TransformSpec } from '../transform'
6
+ import type { TransformCtx, TransformSpec, TransformResult, Transform, Guide } from '../transform'
7
7
 
8
8
 
9
9
  const { deep } = Jsonic.util
10
10
 
11
- async function manualTransform(ctx: TransformCtx, tspec: TransformSpec, model: any, def: any) {
12
- const { guide: { guide: { manual } } } = ctx
11
+ const manualTransform = async function(
12
+ ctx: TransformCtx,
13
+ guide: Guide,
14
+ tspec: TransformSpec,
15
+ model: any,
16
+ def: any
17
+ ): Promise<TransformResult> {
18
+
19
+ const { model: { main: { guide: { manual } } } } = ctx
13
20
 
14
21
  deep(model, manual)
15
22