@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.
- package/dist/apidef.d.ts +1 -1
- package/dist/apidef.js +10 -0
- package/dist/apidef.js.map +1 -1
- package/dist/model.d.ts +49 -17
- package/dist/transform/args.js +39 -8
- package/dist/transform/args.js.map +1 -1
- package/dist/transform/entity.js +0 -4
- package/dist/transform/entity.js.map +1 -1
- package/dist/transform/field.js +18 -10
- package/dist/transform/field.js.map +1 -1
- package/dist/transform/operation.js +4 -4
- package/dist/transform/operation.js.map +1 -1
- package/dist/transform/select.js +7 -7
- package/dist/transform/select.js.map +1 -1
- package/dist/transform/top.js +26 -2
- package/dist/transform/top.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.js.map +1 -1
- package/model/apidef.jsonic +40 -4
- package/package.json +1 -1
- package/src/apidef.ts +19 -1
- package/src/builder/flow/flowHeuristic01.ts +2 -2
- package/src/model.ts +104 -27
- package/src/transform/args.ts +43 -10
- package/src/transform/entity.ts +0 -4
- package/src/transform/field.ts +20 -11
- package/src/transform/operation.ts +5 -5
- package/src/transform/select.ts +10 -10
- package/src/transform/top.ts +26 -2
- package/src/types.ts +5 -1
package/model/apidef.jsonic
CHANGED
|
@@ -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:
|
|
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
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
|
-
|
|
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
|
-
|
|
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):
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
87
|
+
kind: ArgKind
|
|
61
88
|
reqd: boolean
|
|
89
|
+
example?: any
|
|
62
90
|
}
|
|
63
91
|
|
|
64
92
|
|
|
65
|
-
//
|
|
66
|
-
|
|
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:
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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:
|
|
202
|
+
input: ModelEntityFlowStepInput
|
|
125
203
|
match: Record<string, any>
|
|
126
204
|
data: Record<string, any>
|
|
127
|
-
spec:
|
|
128
|
-
|
|
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
|
-
|
|
220
|
+
ModelPoint,
|
|
147
221
|
ModelOp,
|
|
148
222
|
ModelEntity,
|
|
149
223
|
ModelEntityFlow,
|
|
150
224
|
ModelEntityFlowStep,
|
|
225
|
+
ModelEntityFlowStepInput,
|
|
226
|
+
ModelEntityFlowStepValidator,
|
|
227
|
+
ModelEntityFlowStepSpec,
|
|
151
228
|
}
|
package/src/transform/args.ts
CHANGED
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
OpName,
|
|
22
22
|
ModelOp,
|
|
23
23
|
ModelEntity,
|
|
24
|
-
|
|
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, (
|
|
41
|
+
each(mop.points, (mpoint: ModelPoint) => {
|
|
42
42
|
const argdefs: ParameterDef[] = []
|
|
43
43
|
|
|
44
|
-
const pathdef: PathDef = def.paths[
|
|
44
|
+
const pathdef: PathDef = def.paths[mpoint.orig]
|
|
45
45
|
argdefs.push(...(pathdef.parameters ?? []))
|
|
46
46
|
|
|
47
|
-
const opdef: MethodDef = (pathdef as any)[
|
|
47
|
+
const opdef: MethodDef = (pathdef as any)[mpoint.method.toLowerCase()]
|
|
48
48
|
argdefs.push(...(opdef?.parameters ?? []))
|
|
49
49
|
|
|
50
|
-
resolveArgs(ment, mop,
|
|
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,
|
|
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 =
|
|
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
|
|
97
|
-
let kindargs = (
|
|
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
|
-
|
|
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,
|
package/src/transform/entity.ts
CHANGED
package/src/transform/field.ts
CHANGED
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
OpName,
|
|
21
21
|
ModelOp,
|
|
22
22
|
ModelEntity,
|
|
23
|
-
|
|
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
|
|
46
|
+
const mpoints = mop.points
|
|
47
47
|
|
|
48
|
-
for (let
|
|
49
|
-
const opfields = resolveOpFields(ment, mop,
|
|
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,
|
|
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
|
-
|
|
88
|
+
mpoint: ModelPoint,
|
|
80
89
|
def: any
|
|
81
90
|
): ModelField[] {
|
|
82
91
|
const mfields: ModelField[] = []
|
|
83
|
-
const fielddefs = findFieldDefs(ment, mop,
|
|
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
|
-
|
|
113
|
+
mpoint: ModelPoint,
|
|
105
114
|
def: any
|
|
106
115
|
): SchemaDef[] {
|
|
107
116
|
const fielddefs: SchemaDef[] = []
|
|
108
|
-
const pathdef = def.paths[
|
|
117
|
+
const pathdef = def.paths[mpoint.orig]
|
|
109
118
|
|
|
110
|
-
const method =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
162
|
-
|
|
161
|
+
mpoint.transform.req = mpoint.transform.req ?? '`reqdata`'
|
|
162
|
+
mpoint.transform.res = mpoint.transform.res ?? '`body`'
|
|
163
163
|
|
|
164
|
-
return
|
|
164
|
+
return mpoint
|
|
165
165
|
})
|
|
166
166
|
}
|
|
167
167
|
}
|
package/src/transform/select.ts
CHANGED
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
OpName,
|
|
21
21
|
ModelOp,
|
|
22
22
|
ModelEntity,
|
|
23
|
-
|
|
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, (
|
|
40
|
-
const pdef: PathDef = def.paths[
|
|
41
|
-
resolveSelect(guide, ment, mop,
|
|
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
|
-
|
|
59
|
+
mpoint: ModelPoint,
|
|
60
60
|
_pdef: PathDef
|
|
61
61
|
) {
|
|
62
|
-
const select: any =
|
|
63
|
-
const margs: any =
|
|
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[
|
|
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<
|
|
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:
|
|
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) {
|
package/src/transform/top.ts
CHANGED
|
@@ -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
|
}
|