@voxgig/apidef 5.9.0 → 5.11.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.
@@ -10,8 +10,11 @@ main: kit: entity: &: {
10
10
 
11
11
  name: key()
12
12
  active: *true
13
-
14
- id: {
13
+
14
+ # Present only when the spec models an id for this entity (response /
15
+ # request schema declares one, or examples imply one). Public APIs that
16
+ # return payloads without an id leave this undefined.
17
+ id?: {
15
18
  field: string
16
19
  name: string
17
20
  }
@@ -41,13 +44,46 @@ main: kit: entity: &: {
41
44
 
42
45
  parts: [ &: string ]
43
46
 
47
+ # Each arg kind (path params, query, header, cookie) has the same
48
+ # element shape. `kind` is constrained to the kind tag matching the
49
+ # array it appears in; `example` is optional and captures a spec-
50
+ # provided example value used by test generators.
44
51
  args: {
45
- params: [ &: {
52
+ params?: [ &: {
53
+ active: *true
54
+ kind: 'param'
55
+ name: string
56
+ orig?: string
57
+ reqd: boolean
58
+ type: top
59
+ example?: top
60
+ } ]
61
+ query?: [ &: {
62
+ active: *true
63
+ kind: 'query'
64
+ name: string
65
+ orig?: string
66
+ reqd: boolean
67
+ type: top
68
+ example?: top
69
+ } ]
70
+ header?: [ &: {
71
+ active: *true
72
+ kind: 'header'
73
+ name: string
74
+ orig?: string
75
+ reqd: boolean
76
+ type: top
77
+ example?: top
78
+ } ]
79
+ cookie?: [ &: {
46
80
  active: *true
47
- kind: string
81
+ kind: 'cookie'
48
82
  name: string
83
+ orig?: string
49
84
  reqd: boolean
50
85
  type: top
86
+ example?: top
51
87
  } ]
52
88
  }
53
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voxgig/apidef",
3
- "version": "5.9.0",
3
+ "version": "5.11.0",
4
4
  "main": "dist/apidef.js",
5
5
  "type": "commonjs",
6
6
  "types": "dist/apidef.d.ts",
package/src/apidef.ts CHANGED
@@ -284,8 +284,21 @@ function ApiDef(opts: ApiDefOptions) {
284
284
  warn.history.map(n => formatJSONIC(n)).join('\n\n'))
285
285
  }
286
286
 
287
+ // apidef writes model source files (entity, flow, guide jsonics) into
288
+ // .sdk/model/. Downstream actions (sdkgen, etc.) read those via
289
+ // sdk.jsonic @-includes, so voxgig-model has to re-resolve the model
290
+ // before the post-step actions run. Signal reload whenever jostraca
291
+ // wrote or merged any files; if nothing changed on disk,
292
+ // voxgig-model's resolveModel cache short-circuits the re-read.
293
+ const jfiles = jres?.files
294
+ const reload = !!jfiles && (
295
+ (jfiles.written?.length ?? 0) > 0 ||
296
+ (jfiles.merged?.length ?? 0) > 0
297
+ )
298
+
287
299
  return {
288
300
  ok: true,
301
+ reload,
289
302
  err: null,
290
303
  start,
291
304
  end: Date.now(),
@@ -398,17 +411,22 @@ export type {
398
411
 
399
412
  export type {
400
413
  OpName,
414
+ ArgKind,
415
+ NamesCluster,
401
416
  ModelEntityRelations,
402
417
  ModelOpMap,
403
418
  ModelFieldOp,
404
419
  ModelField,
405
420
  ModelArg,
406
- ModelTarget,
421
+ ModelPoint,
407
422
  ModelOp,
408
423
  ModelEntity,
409
424
  Model,
410
425
  ModelEntityFlow,
411
426
  ModelEntityFlowStep,
427
+ ModelEntityFlowStepInput,
428
+ ModelEntityFlowStepValidator,
429
+ ModelEntityFlowStepSpec,
412
430
  } from './model'
413
431
 
414
432
 
@@ -17,7 +17,7 @@ import {
17
17
  import type {
18
18
  ModelEntity,
19
19
  ModelOp,
20
- ModelTarget,
20
+ ModelPoint,
21
21
  } from '../../model'
22
22
 
23
23
  import {
@@ -196,7 +196,7 @@ function resolveBasicEntityFlow(ctx: any, entity: any) {
196
196
  }
197
197
 
198
198
 
199
- function findMainLoadPoint(op: ModelOp): ModelTarget | undefined {
199
+ function findMainLoadPoint(op: ModelOp): ModelPoint | undefined {
200
200
  let cands = op.points.filter(a => '{id}' === getelem(a.parts, -1))
201
201
  return cands[0]
202
202
  }
package/src/model.ts CHANGED
@@ -9,18 +9,41 @@ import type { MethodName } from './types'
9
9
  type OpName = 'load' | 'list' | 'create' | 'update' | 'remove' | 'patch' | 'head' | 'options'
10
10
 
11
11
 
12
- type Model = {
12
+ // Argument kinds supported on operation points.
13
+ type ArgKind = 'param' | 'query' | 'header' | 'cookie'
14
+
15
+
16
+ // jostraca's `names()` helper sticks several stylised forms of the project
17
+ // name onto an object: `name`, `Name`, `NAME`, plus snake/dash variants.
18
+ // Templates that reach for any of these expect the cluster to be present.
19
+ type NamesCluster = {
13
20
  name: string
21
+ Name: string
22
+ NAME: string
23
+ }
24
+
25
+
26
+ // Top-level unified API model produced by apidef + voxgig-model. Templates
27
+ // access the kit through `model.main.kit.<thing>`. The `info`, `config`,
28
+ // `feature`, and `target` kits remain `any` for now because their shapes are
29
+ // less stable than entity/flow — Phase 2 of the refactor types them.
30
+ type Model = NamesCluster & {
14
31
  origin?: string
32
+ def?: string
15
33
 
16
- const: {
17
- // TODO: remove
18
- Name: string
34
+ const: NamesCluster & {
35
+ year?: number
19
36
  }
20
37
 
21
38
  main: {
22
39
  kit: {
40
+ info: any
41
+ config: any
23
42
  entity: Record<string, ModelEntity>
43
+ feature: Record<string, any>
44
+ flow: Record<string, ModelEntityFlow>
45
+ target: Record<string, any>
46
+ option?: Record<string, any>
24
47
  }
25
48
  }
26
49
  }
@@ -52,18 +75,27 @@ type ModelField = {
52
75
  }
53
76
 
54
77
 
55
- // Operation argument/parameter definition
78
+ // Operation argument/parameter definition.
79
+ // `example` captures a value the spec advertises (parameter `example`,
80
+ // the first entry of `examples`, or `schema.example`/`schema.default`).
81
+ // Test generators use this for required params in live test setup so the
82
+ // generated request actually satisfies the API contract.
56
83
  type ModelArg = {
57
84
  name: string
58
85
  orig: string
59
86
  type: any // @voxgig/struct validation schema
60
- kind: 'param' | 'query' | 'header' | 'cookie'
87
+ kind: ArgKind
61
88
  reqd: boolean
89
+ example?: any
62
90
  }
63
91
 
64
92
 
65
- // Point implementation of an operation
66
- type ModelTarget = {
93
+ // One concrete HTTP endpoint that can satisfy an operation. An entity op
94
+ // (load/list/create/...) carries an array of these — apidef chooses
95
+ // between them at runtime via `select.exist` matching against reqmatch /
96
+ // reqdata. (Originally named `ModelTarget`; renamed for consistency with
97
+ // the field name `points` and the runtime utility `MakePoint`.)
98
+ type ModelPoint = {
67
99
  orig: string
68
100
  method: MethodName
69
101
  parts: string[]
@@ -93,19 +125,31 @@ type ModelTarget = {
93
125
  // Operation definition
94
126
  type ModelOp = {
95
127
  name: OpName
96
- points: ModelTarget[]
128
+ points: ModelPoint[]
97
129
  }
98
130
 
99
131
 
100
- // Entity definition - core model entity with operations and fields
132
+ // Entity definition - core model entity with operations and fields.
133
+ // `id` is present only when the OpenAPI response/request schema declares
134
+ // (or examples imply) an `id` field on the entity. Public APIs that return
135
+ // payloads without an id (e.g. read-only feeds) leave it undefined.
136
+ //
137
+ // `Name`, `NAME` etc. are stamped on by jostraca's `names()` helper after
138
+ // apidef hands the model to the generator. They're typed as optional here
139
+ // so apidef's transform code can construct entities without them; template
140
+ // code should reach for them through `nom(entity, 'Name')` rather than
141
+ // direct property access, which both works pre-`names()` and lets us
142
+ // remove the optional later.
101
143
  type ModelEntity = {
102
- name: string,
103
- op: ModelOpMap,
104
- fields: ModelField[],
105
- id: {
106
- name: string,
107
- field: string,
108
- },
144
+ name: string
145
+ Name?: string
146
+ NAME?: string
147
+ op: ModelOpMap
148
+ fields: ModelField[]
149
+ id?: {
150
+ name: string
151
+ field: string
152
+ }
109
153
  relations: ModelEntityRelations
110
154
  }
111
155
 
@@ -116,36 +160,69 @@ type ModelEntityFlow = {
116
160
  kind: string
117
161
  // args: Record<string, string>
118
162
  step: ModelEntityFlowStep[]
163
+ active?: boolean
164
+ }
165
+
166
+
167
+ // Per-step input cluster. Test-generators name the variables they emit by
168
+ // reading these slots, falling back to derived defaults. All fields are
169
+ // optional — `newFlowStep` in transform/flowstep.ts guarantees the input
170
+ // object itself exists, so consumers don't need to null-check `step.input`.
171
+ type ModelEntityFlowStepInput = {
172
+ ref?: string
173
+ entvar?: string
174
+ matchvar?: string
175
+ datavar?: string
176
+ listvar?: string
177
+ resdatavar?: string
178
+ markdefvar?: string
179
+ srcdatavar?: string
180
+ suffix?: string
181
+ textfield?: string
182
+ id?: any
183
+ [extra: string]: any
184
+ }
185
+
186
+
187
+ // Validators and specs are user-supplied callables identified by the
188
+ // `apply` discriminator; `def` is the validator-specific options bag.
189
+ type ModelEntityFlowStepValidator = {
190
+ apply: string
191
+ def: Record<string, any>
192
+ }
193
+
194
+ type ModelEntityFlowStepSpec = {
195
+ apply: string
196
+ def: Record<string, any>
119
197
  }
120
198
 
121
199
 
122
200
  type ModelEntityFlowStep = {
123
201
  op: OpName
124
- input: Record<string, any>
202
+ input: ModelEntityFlowStepInput
125
203
  match: Record<string, any>
126
204
  data: Record<string, any>
127
- spec: {
128
- apply: string
129
- def: Record<string, any>
130
- }[]
131
- valid: {
132
- apply: string
133
- def: Record<string, any>
134
- }[]
205
+ spec: ModelEntityFlowStepSpec[]
206
+ valid: ModelEntityFlowStepValidator[]
135
207
  }
136
208
 
137
209
 
138
210
  export type {
139
211
  OpName,
212
+ ArgKind,
213
+ NamesCluster,
140
214
  Model,
141
215
  ModelEntityRelations,
142
216
  ModelOpMap,
143
217
  ModelFieldOp,
144
218
  ModelField,
145
219
  ModelArg,
146
- ModelTarget,
220
+ ModelPoint,
147
221
  ModelOp,
148
222
  ModelEntity,
149
223
  ModelEntityFlow,
150
224
  ModelEntityFlowStep,
225
+ ModelEntityFlowStepInput,
226
+ ModelEntityFlowStepValidator,
227
+ ModelEntityFlowStepSpec,
151
228
  }
@@ -21,7 +21,7 @@ import type {
21
21
  OpName,
22
22
  ModelOp,
23
23
  ModelEntity,
24
- ModelTarget,
24
+ ModelPoint,
25
25
  ModelArg,
26
26
  } from '../model'
27
27
 
@@ -38,16 +38,16 @@ const argsTransform: Transform = async function(
38
38
 
39
39
  each(kit.entity, (ment: ModelEntity, entname: string) => {
40
40
  each(ment.op, (mop: ModelOp, opname: OpName) => {
41
- each(mop.points, (mtarget: ModelTarget) => {
41
+ each(mop.points, (mpoint: ModelPoint) => {
42
42
  const argdefs: ParameterDef[] = []
43
43
 
44
- const pathdef: PathDef = def.paths[mtarget.orig]
44
+ const pathdef: PathDef = def.paths[mpoint.orig]
45
45
  argdefs.push(...(pathdef.parameters ?? []))
46
46
 
47
- const opdef: MethodDef = (pathdef as any)[mtarget.method.toLowerCase()]
47
+ const opdef: MethodDef = (pathdef as any)[mpoint.method.toLowerCase()]
48
48
  argdefs.push(...(opdef?.parameters ?? []))
49
49
 
50
- resolveArgs(ment, mop, mtarget, argdefs)
50
+ resolveArgs(ment, mop, mpoint, argdefs)
51
51
  })
52
52
 
53
53
  })
@@ -67,7 +67,7 @@ const ARG_KIND: Record<string, ModelArg["kind"]> = {
67
67
  }
68
68
 
69
69
 
70
- function resolveArgs(ment: ModelEntity, mop: ModelOp, mtarget: ModelTarget, argdefs: ParameterDef[]) {
70
+ function resolveArgs(ment: ModelEntity, mop: ModelOp, mpoint: ModelPoint, argdefs: ParameterDef[]) {
71
71
  const touchedKeys = new Set<string>()
72
72
 
73
73
  each(argdefs, (argdef: ParameterDef) => {
@@ -79,7 +79,7 @@ function resolveArgs(ment: ModelEntity, mop: ModelOp, mtarget: ModelTarget, argd
79
79
  // Rename map can be keyed by either the spec original (camelCase) or by
80
80
  // the snakified form depending on which path went through heuristic01.
81
81
  // Try both before falling through to `orig`.
82
- const renameMap = mtarget.rename[kind]
82
+ const renameMap = mpoint.rename[kind]
83
83
  const name = renameMap?.[specName] ?? renameMap?.[orig] ?? orig
84
84
  const marg: ModelArg = {
85
85
  name,
@@ -89,12 +89,17 @@ function resolveArgs(ment: ModelEntity, mop: ModelOp, mtarget: ModelTarget, argd
89
89
  reqd: !!argdef.required
90
90
  }
91
91
 
92
+ const example = resolveArgExample(argdef)
93
+ if (undefined !== example) {
94
+ marg.example = example
95
+ }
96
+
92
97
  if (argdef.nullable) {
93
98
  marg.type = ['`$ONE`', '`$NULL`', marg.type]
94
99
  }
95
100
 
96
- const argsKey = (marg.kind === 'param' ? 'params' : marg.kind) as keyof typeof mtarget.args
97
- let kindargs = (mtarget.args[argsKey] = mtarget.args[argsKey] ?? [])
101
+ const argsKey = (marg.kind === 'param' ? 'params' : marg.kind) as keyof typeof mpoint.args
102
+ let kindargs = (mpoint.args[argsKey] = mpoint.args[argsKey] ?? [])
98
103
  kindargs.push(marg)
99
104
  touchedKeys.add(argsKey)
100
105
  })
@@ -102,11 +107,39 @@ function resolveArgs(ment: ModelEntity, mop: ModelOp, mtarget: ModelTarget, argd
102
107
  // Sort once after all args are collected
103
108
  const cmp = (a: ModelArg, b: ModelArg) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
104
109
  for (const key of touchedKeys) {
105
- mtarget.args[key as keyof typeof mtarget.args]?.sort(cmp)
110
+ mpoint.args[key as keyof typeof mpoint.args]?.sort(cmp)
106
111
  }
107
112
  }
108
113
 
109
114
 
115
+ // OpenAPI lets specs advertise example values four ways:
116
+ // parameter.example (single value, OAS 3.0+)
117
+ // parameter.examples (named-example object, take first .value)
118
+ // parameter.schema.example (single value on the schema)
119
+ // parameter.schema.default (default value)
120
+ // Pick the first one we find so test generators can produce valid live
121
+ // requests even when the parameter is required and has no other source.
122
+ function resolveArgExample(argdef: any): any {
123
+ if (undefined !== argdef?.example) return argdef.example
124
+
125
+ const examples = argdef?.examples
126
+ if (examples && 'object' === typeof examples) {
127
+ for (const v of Object.values(examples)) {
128
+ if (v && 'object' === typeof v && undefined !== (v as any).value) {
129
+ return (v as any).value
130
+ }
131
+ }
132
+ }
133
+
134
+ const schema = argdef?.schema
135
+ if (schema) {
136
+ if (undefined !== schema.example) return schema.example
137
+ if (undefined !== schema.default) return schema.default
138
+ }
139
+
140
+ return undefined
141
+ }
142
+
110
143
 
111
144
  export {
112
145
  argsTransform,
@@ -41,10 +41,6 @@ const entityTransform: Transform = async function(
41
41
  name: entname,
42
42
  op: {},
43
43
  fields: [],
44
- id: {
45
- name: 'id',
46
- field: 'id',
47
- },
48
44
  relations,
49
45
  }
50
46
 
@@ -20,7 +20,7 @@ import type {
20
20
  OpName,
21
21
  ModelOp,
22
22
  ModelEntity,
23
- ModelTarget,
23
+ ModelPoint,
24
24
  ModelField,
25
25
  } from '../model'
26
26
 
@@ -43,10 +43,10 @@ const fieldTransform: Transform = async function(
43
43
  for (let opname of opFieldPrecedence) {
44
44
  const mop = ment.op[opname]
45
45
  if (mop) {
46
- const mtargets = mop.points
46
+ const mpoints = mop.points
47
47
 
48
- for (let mtarget of mtargets) {
49
- const opfields = resolveOpFields(ment, mop, mtarget, def)
48
+ for (let mpoint of mpoints) {
49
+ const opfields = resolveOpFields(ment, mop, mpoint, def)
50
50
 
51
51
  for (let opfield of opfields) {
52
52
  if (!seen[opfield.name]) {
@@ -54,7 +54,7 @@ const fieldTransform: Transform = async function(
54
54
  seen[opfield.name] = opfield
55
55
  }
56
56
  else {
57
- mergeField(ment, mop, mtarget, def, seen[opfield.name], opfield)
57
+ mergeField(ment, mop, mpoint, def, seen[opfield.name], opfield)
58
58
  }
59
59
  }
60
60
  }
@@ -65,6 +65,15 @@ const fieldTransform: Transform = async function(
65
65
  return a.name < b.name ? -1 : a.name > b.name ? 1 : 0
66
66
  })
67
67
 
68
+ // Mark the entity as having an id only when the spec actually declares one.
69
+ // Downstream (test generators, fixture builders) gate id-specific code on
70
+ // this presence so that public read-only APIs without ids don't get
71
+ // bogus id assertions.
72
+ const idField = fields.find((f: ModelField) => 'id' === f.name)
73
+ if (idField) {
74
+ ment.id = { name: 'id', field: 'id' }
75
+ }
76
+
68
77
  msg += ment.name + ' '
69
78
  })
70
79
 
@@ -76,11 +85,11 @@ const fieldTransform: Transform = async function(
76
85
  function resolveOpFields(
77
86
  ment: ModelEntity,
78
87
  mop: ModelOp,
79
- mtarget: ModelTarget,
88
+ mpoint: ModelPoint,
80
89
  def: any
81
90
  ): ModelField[] {
82
91
  const mfields: ModelField[] = []
83
- const fielddefs = findFieldDefs(ment, mop, mtarget, def)
92
+ const fielddefs = findFieldDefs(ment, mop, mpoint, def)
84
93
 
85
94
  for (let fielddef of fielddefs) {
86
95
  const fieldname = (fielddef as any).key$ as string
@@ -101,13 +110,13 @@ function resolveOpFields(
101
110
  function findFieldDefs(
102
111
  _ment: ModelEntity,
103
112
  mop: ModelOp,
104
- mtarget: ModelTarget,
113
+ mpoint: ModelPoint,
105
114
  def: any
106
115
  ): SchemaDef[] {
107
116
  const fielddefs: SchemaDef[] = []
108
- const pathdef = def.paths[mtarget.orig]
117
+ const pathdef = def.paths[mpoint.orig]
109
118
 
110
- const method = mtarget.method.toLowerCase()
119
+ const method = mpoint.method.toLowerCase()
111
120
  const opdef: any = pathdef[method]
112
121
 
113
122
  if (opdef) {
@@ -303,7 +312,7 @@ function inferTypeFromValue(value: any): string {
303
312
  function mergeField(
304
313
  ment: ModelEntity,
305
314
  mop: ModelOp,
306
- mtarget: ModelTarget,
315
+ mpoint: ModelPoint,
307
316
  def: any,
308
317
  exisingField: ModelField,
309
318
  newField: ModelField
@@ -19,7 +19,7 @@ import type {
19
19
  OpName,
20
20
  ModelOpMap,
21
21
  ModelOp,
22
- ModelTarget,
22
+ ModelPoint,
23
23
  } from '../model'
24
24
 
25
25
 
@@ -146,7 +146,7 @@ function resolveOp(opname: OpName, gent: GuideEntity): undefined | ModelOp {
146
146
  // rewrote the freshly-renamed `{id}` into `{project_id}` again).
147
147
  const parts = p.parts
148
148
 
149
- const mtarget: ModelTarget = {
149
+ const mpoint: ModelPoint = {
150
150
  orig: p.orig,
151
151
  parts,
152
152
  rename: p.rename,
@@ -158,10 +158,10 @@ function resolveOp(opname: OpName, gent: GuideEntity): undefined | ModelOp {
158
158
  }
159
159
  }
160
160
 
161
- mtarget.transform.req = mtarget.transform.req ?? '`reqdata`'
162
- mtarget.transform.res = mtarget.transform.res ?? '`body`'
161
+ mpoint.transform.req = mpoint.transform.req ?? '`reqdata`'
162
+ mpoint.transform.res = mpoint.transform.res ?? '`body`'
163
163
 
164
- return mtarget
164
+ return mpoint
165
165
  })
166
166
  }
167
167
  }
@@ -20,7 +20,7 @@ import type {
20
20
  OpName,
21
21
  ModelOp,
22
22
  ModelEntity,
23
- ModelTarget,
23
+ ModelPoint,
24
24
  ModelArg,
25
25
  } from '../model'
26
26
 
@@ -36,9 +36,9 @@ const selectTransform: Transform = async function(
36
36
 
37
37
  each(kit.entity, (ment: ModelEntity, _entname: string) => {
38
38
  each(ment.op, (mop: ModelOp, _opname: OpName) => {
39
- each(mop.points, (mtarget: ModelTarget) => {
40
- const pdef: PathDef = def.paths[mtarget.orig]
41
- resolveSelect(guide, ment, mop, mtarget, pdef)
39
+ each(mop.points, (mpoint: ModelPoint) => {
40
+ const pdef: PathDef = def.paths[mpoint.orig]
41
+ resolveSelect(guide, ment, mop, mpoint, pdef)
42
42
  })
43
43
  if (null != mop.points && 0 < mop.points.length) {
44
44
  sortPoints(guide, ment, mop)
@@ -56,11 +56,11 @@ function resolveSelect(
56
56
  guide: Guide,
57
57
  ment: ModelEntity,
58
58
  _mop: ModelOp,
59
- mtarget: ModelTarget,
59
+ mpoint: ModelPoint,
60
60
  _pdef: PathDef
61
61
  ) {
62
- const select: any = mtarget.select
63
- const margs: any = mtarget.args
62
+ const select: any = mpoint.select
63
+ const margs: any = mpoint.args
64
64
 
65
65
  const argkinds = ['params', 'query', 'header', 'cookie']
66
66
 
@@ -75,7 +75,7 @@ function resolveSelect(
75
75
  select.exist.sort()
76
76
 
77
77
  const gent = guide.entity[ment.name]
78
- const gpath = gent.path[mtarget.orig]
78
+ const gpath = gent.path[mpoint.orig]
79
79
 
80
80
  if (gpath.action) {
81
81
  const actname = Object.keys(gpath.action).sort()[0]
@@ -94,12 +94,12 @@ function sortPoints(
94
94
  mop: ModelOp,
95
95
  ) {
96
96
  // Cache joined exist strings to avoid recomputing on every comparison.
97
- const existCache = new Map<ModelTarget, string>()
97
+ const existCache = new Map<ModelPoint, string>()
98
98
  for (const pt of mop.points) {
99
99
  existCache.set(pt, pt.select.exist.join('\t'))
100
100
  }
101
101
 
102
- mop.points.sort((a: ModelTarget, b: ModelTarget) => {
102
+ mop.points.sort((a: ModelPoint, b: ModelPoint) => {
103
103
  // longest exist len first
104
104
  let order = b.select.exist.length - a.select.exist.length
105
105
  if (0 === order) {
@@ -41,8 +41,8 @@ const topTransform = async function(
41
41
  const { apimodel, def } = ctx
42
42
  const kit: KitModel = apimodel.main[KIT]
43
43
 
44
- kit.info = def.info
45
- kit.info.servers = def.servers ?? []
44
+ kit.info = stringifyInfoScalars(def.info ?? {})
45
+ kit.info.servers = stringifyInfoScalars(def.servers ?? [])
46
46
 
47
47
  // Swagger 2.0
48
48
  if (def.host) {
@@ -55,6 +55,30 @@ const topTransform = async function(
55
55
  }
56
56
 
57
57
 
58
+ // OpenAPI's `info` object (and the `servers` array) declares every scalar
59
+ // leaf as a string. YAML/JSON parsers don't enforce that — `version: 2`
60
+ // without quotes parses as the number 2, `version: true` as a boolean.
61
+ // Apidef's downstream schema (apidef.jsonic) unifies info fields as
62
+ // `string`, so non-string scalars cause an aontu unify failure during
63
+ // model resolution. Normalise scalar leaves to strings here, at the
64
+ // model-build boundary, rather than relax the schema.
65
+ function stringifyInfoScalars(node: any): any {
66
+ if (null == node) return node
67
+ if (Array.isArray(node)) return node.map(stringifyInfoScalars)
68
+ if ('object' === typeof node) {
69
+ const out: Record<string, any> = {}
70
+ for (const [k, v] of Object.entries(node)) {
71
+ out[k] = stringifyInfoScalars(v)
72
+ }
73
+ return out
74
+ }
75
+ if ('number' === typeof node || 'boolean' === typeof node) {
76
+ return String(node)
77
+ }
78
+ return node
79
+ }
80
+
81
+
58
82
  export {
59
83
  topTransform
60
84
  }