@voxgig/apidef 6.3.7 → 6.3.8
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 +2 -2
- package/dist/apidef.js +21 -5
- package/dist/apidef.js.map +1 -1
- package/dist/builder/entity/entity.js.map +1 -1
- package/dist/builder/entity/info.js.map +1 -1
- package/dist/builder/entity.js.map +1 -1
- package/dist/builder/flow/flowHeuristic01.js.map +1 -1
- package/dist/builder/flow.js.map +1 -1
- package/dist/guide/guide.js +49 -6
- package/dist/guide/guide.js.map +1 -1
- package/dist/guide/heuristic01.js +99 -89
- package/dist/guide/heuristic01.js.map +1 -1
- package/dist/parse.js +151 -18
- package/dist/parse.js.map +1 -1
- package/dist/resolver.js.map +1 -1
- package/dist/transform/args.js.map +1 -1
- package/dist/transform/clean.js.map +1 -1
- package/dist/transform/entity.js.map +1 -1
- package/dist/transform/field.js +14 -0
- package/dist/transform/field.js.map +1 -1
- package/dist/transform/flow.js.map +1 -1
- package/dist/transform/flowstep.js.map +1 -1
- package/dist/transform/operation.js.map +1 -1
- package/dist/transform/select.js.map +1 -1
- package/dist/transform/top.d.ts +2 -1
- package/dist/transform/top.js +32 -2
- package/dist/transform/top.js.map +1 -1
- package/dist/transform.d.ts +8 -7
- package/dist/transform.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +97 -84
- package/dist/types.js.map +1 -1
- package/dist/utility.d.ts +6 -1
- package/dist/utility.js +169 -8
- package/dist/utility.js.map +1 -1
- package/package.json +8 -8
- package/src/apidef.ts +22 -4
- package/src/guide/guide.ts +50 -6
- package/src/guide/heuristic01.ts +37 -28
- package/src/parse.ts +161 -20
- package/src/transform/field.ts +16 -1
- package/src/transform/top.ts +37 -2
- package/src/tsconfig.json +1 -0
- package/src/types.ts +5 -0
- package/src/utility.ts +186 -8
package/src/guide/heuristic01.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { each } from 'jostraca'
|
|
|
7
7
|
|
|
8
8
|
import { size, merge, getelem, isempty, items, keysof } from '@voxgig/struct'
|
|
9
9
|
|
|
10
|
+
import { isEntityWrapperProp, envelopeProp, closedBodyTransform } from '../utility'
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
import {
|
|
12
14
|
ApiDefContext,
|
|
@@ -1126,17 +1128,48 @@ function ResolveTransform(spec: TaskSpec) {
|
|
|
1126
1128
|
else if (isEntityWrapperProp(resprops[entdesc.name])) {
|
|
1127
1129
|
transform.res = '`body.' + entdesc.name + '`'
|
|
1128
1130
|
}
|
|
1131
|
+
else {
|
|
1132
|
+
// The wrapper is often named for the CARDINALITY rather than the
|
|
1133
|
+
// entity — `{item: {...}}` from a load, `{items: [...]}` from a list —
|
|
1134
|
+
// which the entity-name rules above cannot see. Left unwrapped, list()
|
|
1135
|
+
// hands back the envelope where the caller expects an array, and the
|
|
1136
|
+
// envelope key is mistaken for a field of the entity.
|
|
1137
|
+
const envelope = envelopeProp(resprops, opname)
|
|
1138
|
+
if (null != envelope) {
|
|
1139
|
+
transform.res = '`body.' + envelope + '`'
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1129
1142
|
}
|
|
1130
1143
|
|
|
1131
|
-
|
|
1144
|
+
// The SCHEMA is what closedBodyTransform needs (it reads
|
|
1145
|
+
// additionalProperties); the wrapper-name checks need its PROPERTIES. They
|
|
1146
|
+
// used to share one value and index the schema itself, so
|
|
1147
|
+
// `schema['todoitem']` was always undefined and the entity-name request
|
|
1148
|
+
// envelope was never detected — while the Go port read `.properties` and
|
|
1149
|
+
// did detect it. That divergence was inert only because `req` was never
|
|
1150
|
+
// serialised; now that it is, the two implementations would emit different
|
|
1151
|
+
// request bodies for the same spec.
|
|
1152
|
+
const reqschema = getRequestBodySchema(mdesc.requestBody)
|
|
1153
|
+
const reqprops = reqschema?.properties
|
|
1132
1154
|
debugpath(pathStr, methodName, 'TRANSFORM-REQ', keysof(reqprops))
|
|
1133
|
-
if (
|
|
1134
|
-
if (reqprops[entdesc.origname]) {
|
|
1155
|
+
if (reqschema) {
|
|
1156
|
+
if (null != reqprops?.[entdesc.origname]) {
|
|
1135
1157
|
transform.req = { [entdesc.origname]: '`reqdata`' }
|
|
1136
1158
|
}
|
|
1137
|
-
else if (reqprops[entdesc.name]) {
|
|
1159
|
+
else if (null != reqprops?.[entdesc.name]) {
|
|
1138
1160
|
transform.req = { [entdesc.name]: '`reqdata`' }
|
|
1139
1161
|
}
|
|
1162
|
+
else {
|
|
1163
|
+
// A CLOSED body schema names every property the server will accept, so
|
|
1164
|
+
// the body is those properties — not the whole request payload. The
|
|
1165
|
+
// payload also carries the op's PATH params (`id` for
|
|
1166
|
+
// `PUT /item/{id}`), and a closed shape rejects the entire request over
|
|
1167
|
+
// that one extra key: every update came back 400 with `invalid-data`.
|
|
1168
|
+
const body = closedBodyTransform(reqschema)
|
|
1169
|
+
if (null != body) {
|
|
1170
|
+
transform.req = body
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1140
1173
|
}
|
|
1141
1174
|
|
|
1142
1175
|
if (!isempty(transform) && null != op[opname]) {
|
|
@@ -1365,30 +1398,6 @@ function getResponseSchema(response: any) {
|
|
|
1365
1398
|
}
|
|
1366
1399
|
|
|
1367
1400
|
|
|
1368
|
-
// A response property only "wraps" the entity when it is itself a structured
|
|
1369
|
-
// value that could contain the entity: an object, an array, a $ref, or a
|
|
1370
|
-
// composed (allOf/oneOf/anyOf) schema. A scalar property (string, integer,
|
|
1371
|
-
// number, boolean) that merely shares the entity's name is a field of the
|
|
1372
|
-
// entity, not a wrapper, so the response must not be unwrapped down to it.
|
|
1373
|
-
function isEntityWrapperProp(propSchema: any): boolean {
|
|
1374
|
-
if (null == propSchema || 'object' !== typeof propSchema) {
|
|
1375
|
-
return false
|
|
1376
|
-
}
|
|
1377
|
-
if (null != propSchema.$ref) {
|
|
1378
|
-
return true
|
|
1379
|
-
}
|
|
1380
|
-
if (null != propSchema.properties ||
|
|
1381
|
-
null != propSchema.items ||
|
|
1382
|
-
null != propSchema.allOf ||
|
|
1383
|
-
null != propSchema.oneOf ||
|
|
1384
|
-
null != propSchema.anyOf) {
|
|
1385
|
-
return true
|
|
1386
|
-
}
|
|
1387
|
-
const t = propSchema.type
|
|
1388
|
-
return 'object' === t || 'array' === t
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
1401
|
function inferEntityName(
|
|
1393
1402
|
mdesc: any,
|
|
1394
1403
|
parts: string[],
|
package/src/parse.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/* Copyright (c) 2024-2025 Voxgig, MIT License */
|
|
2
2
|
|
|
3
|
-
import { Jsonic } from 'jsonic'
|
|
4
|
-
import { Yaml } from '@
|
|
5
|
-
|
|
6
|
-
import { decircular } from '@voxgig/util'
|
|
3
|
+
import { Jsonic } from '@tabnas/jsonic'
|
|
4
|
+
import { Yaml } from '@tabnas/yaml'
|
|
7
5
|
|
|
8
6
|
import { relativizePath } from './utility'
|
|
9
7
|
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
// NOTE: @tabnas/yaml types its Plugin against @tabnas/parser, while
|
|
10
|
+
// Jsonic.use expects @tabnas/jsonic's own (structurally identical) Plugin
|
|
11
|
+
// type - hence the cast.
|
|
12
|
+
const yamlParser = Jsonic.make().use(Yaml as any)
|
|
12
13
|
|
|
13
14
|
// Matches any line that is not purely a YAML comment or whitespace.
|
|
14
15
|
const RE_HAS_CONTENT = /^\s*[^#\s]/m
|
|
@@ -100,12 +101,80 @@ async function parseOpenAPI(source: any, _meta?: any) {
|
|
|
100
101
|
// Single-pass: add x-ref properties and resolve $ref pointers together.
|
|
101
102
|
addXRefsAndResolve(parsed, parsed)
|
|
102
103
|
|
|
103
|
-
const def =
|
|
104
|
+
const def = decycle(parsed)
|
|
104
105
|
|
|
105
106
|
return def
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
|
|
110
|
+
// Break reference cycles so the parsed spec stays JSON-serializable, WITHOUT
|
|
111
|
+
// destroying the structure sharing that $ref inlining deliberately creates.
|
|
112
|
+
//
|
|
113
|
+
// @voxgig/util's decircular() rebuilds the tree — it allocates a fresh object
|
|
114
|
+
// per *visit*, so a component reachable by k distinct paths is copied k times
|
|
115
|
+
// and the result is the tree-expansion of the DAG, size O(fanout^depth). That
|
|
116
|
+
// is catastrophic on exactly the shape real specs have (components reused
|
|
117
|
+
// across nesting levels): a 2.3 KB spec with 12 levels and 3 refs per level
|
|
118
|
+
// expanded to a 172 MB model, and 13 levels exhausted a 2 GB heap.
|
|
119
|
+
//
|
|
120
|
+
// Cycles are real here — inlining a self-referential schema makes the copy's
|
|
121
|
+
// `properties` the same object as the original's, so the copy contains
|
|
122
|
+
// itself — so they still have to be cut. This does it in place: only the edge
|
|
123
|
+
// that closes a cycle is replaced (with decircular's marker string, so the
|
|
124
|
+
// output shape is unchanged), every other node is visited exactly once and
|
|
125
|
+
// left shared. Linear in the number of distinct nodes.
|
|
126
|
+
function decycle(root: any) {
|
|
127
|
+
// Entry path of each node on the current ancestor chain; presence in this
|
|
128
|
+
// map is what identifies a back-edge. Nodes are removed on the way out, so
|
|
129
|
+
// a node reachable twice as a *sibling* is not treated as a cycle.
|
|
130
|
+
const onPath = new Map<any, string[]>()
|
|
131
|
+
// Fully-processed nodes. Revisiting one is legitimate sharing, not a cycle,
|
|
132
|
+
// and must not be walked (or copied) again.
|
|
133
|
+
const done = new WeakSet<any>()
|
|
134
|
+
const path: string[] = []
|
|
135
|
+
|
|
136
|
+
const walk = (node: any) => {
|
|
137
|
+
if (null == node || 'object' !== typeof node) return
|
|
138
|
+
if (done.has(node)) return
|
|
139
|
+
|
|
140
|
+
// The YAML parser hands back null-prototype objects. decircular() used to
|
|
141
|
+
// launder them into plain objects as a side effect of rebuilding the tree;
|
|
142
|
+
// parse()'s result is public, so keep that contract (callers reasonably
|
|
143
|
+
// expect `hasOwnProperty` etc. on a parsed spec) rather than leaking the
|
|
144
|
+
// parser's internal shape now that nothing is rebuilt.
|
|
145
|
+
if (!Array.isArray(node) && null === Object.getPrototypeOf(node)) {
|
|
146
|
+
Object.setPrototypeOf(node, Object.prototype)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
onPath.set(node, path.slice())
|
|
150
|
+
|
|
151
|
+
const keys = Array.isArray(node) ?
|
|
152
|
+
node.map((_: any, i: number) => i) : Object.keys(node)
|
|
153
|
+
|
|
154
|
+
for (const key of keys as any[]) {
|
|
155
|
+
const val = node[key]
|
|
156
|
+
if (null == val || 'object' !== typeof val) continue
|
|
157
|
+
|
|
158
|
+
const cyclePath = onPath.get(val)
|
|
159
|
+
if (undefined !== cyclePath) {
|
|
160
|
+
node[key] = `[Circular *${cyclePath.join('.')}]`
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
path.push(String(key))
|
|
165
|
+
walk(val)
|
|
166
|
+
path.pop()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
onPath.delete(node)
|
|
170
|
+
done.add(node)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
walk(root)
|
|
174
|
+
return root
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
109
178
|
// Single-pass tree walk that:
|
|
110
179
|
// 1. Preserves original $ref values as x-ref
|
|
111
180
|
// 2. Resolves $ref JSON pointers in-place
|
|
@@ -117,6 +186,20 @@ async function parseOpenAPI(source: any, _meta?: any) {
|
|
|
117
186
|
// across every site that referenced the same component. (A deep clone is
|
|
118
187
|
// deliberately avoided: schemas can be self-referential, which would make
|
|
119
188
|
// cloning non-terminating.)
|
|
189
|
+
// Keywords sitting beside a `$ref` on the *referring* node. OpenAPI 3.1 and
|
|
190
|
+
// JSON Schema 2020-12 both allow them (`description`, `required`,
|
|
191
|
+
// constraints, ...) and they still apply, so inlining must not drop them.
|
|
192
|
+
// Applied over the resolved target, so the local statement wins.
|
|
193
|
+
function refSiblings(node: any): any {
|
|
194
|
+
const out: any = {}
|
|
195
|
+
for (const k of Object.keys(node)) {
|
|
196
|
+
if ('$ref' === k) continue
|
|
197
|
+
out[k] = node[k]
|
|
198
|
+
}
|
|
199
|
+
return out
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
120
203
|
function addXRefsAndResolve(obj: any, root: any, visited?: WeakSet<any>) {
|
|
121
204
|
if (!obj || typeof obj !== 'object') return
|
|
122
205
|
if (!visited) visited = new WeakSet()
|
|
@@ -131,7 +214,7 @@ function addXRefsAndResolve(obj: any, root: any, visited?: WeakSet<any>) {
|
|
|
131
214
|
const xref = item.$ref
|
|
132
215
|
const resolved = resolvePointer(root, xref)
|
|
133
216
|
if (resolved !== undefined) {
|
|
134
|
-
obj[i] = { ...resolved, 'x-ref': xref }
|
|
217
|
+
obj[i] = { ...resolved, ...refSiblings(item), 'x-ref': xref }
|
|
135
218
|
addXRefsAndResolve(obj[i], root, visited)
|
|
136
219
|
} else {
|
|
137
220
|
item['x-ref'] = xref
|
|
@@ -150,7 +233,7 @@ function addXRefsAndResolve(obj: any, root: any, visited?: WeakSet<any>) {
|
|
|
150
233
|
const xref = val.$ref
|
|
151
234
|
const resolved = resolvePointer(root, xref)
|
|
152
235
|
if (resolved !== undefined) {
|
|
153
|
-
obj[key] = { ...resolved, 'x-ref': xref }
|
|
236
|
+
obj[key] = { ...resolved, ...refSiblings(val), 'x-ref': xref }
|
|
154
237
|
addXRefsAndResolve(obj[key], root, visited)
|
|
155
238
|
} else {
|
|
156
239
|
val['x-ref'] = xref
|
|
@@ -165,22 +248,80 @@ function addXRefsAndResolve(obj: any, root: any, visited?: WeakSet<any>) {
|
|
|
165
248
|
}
|
|
166
249
|
|
|
167
250
|
|
|
168
|
-
// Follow a JSON pointer like "#/components/schemas/Planet"
|
|
251
|
+
// Follow a JSON pointer like "#/components/schemas/Planet".
|
|
252
|
+
//
|
|
253
|
+
// Alias components — `Foo: { $ref: '#/components/schemas/Bar' }` — are a
|
|
254
|
+
// normal OpenAPI idiom, so a pointer can land on another bare $ref node.
|
|
255
|
+
// Follow the chain to its end rather than returning the intermediate: the
|
|
256
|
+
// caller inlines `{ ...resolved }`, and a `$ref` *string* key in that spread
|
|
257
|
+
// is never followed by the object-valued recursion in addXRefsAndResolve, so
|
|
258
|
+
// stopping early yields a schema with no properties and every field is
|
|
259
|
+
// silently dropped. Whether that happened used to depend on whether `paths`
|
|
260
|
+
// or `components` came first in the document, because resolution reads a root
|
|
261
|
+
// the same walk is still mutating.
|
|
262
|
+
//
|
|
263
|
+
// `seen` holds pointer strings (not object identities) so a self- or
|
|
264
|
+
// mutually-referential alias cycle terminates instead of looping forever.
|
|
169
265
|
function resolvePointer(root: any, ref: string): any {
|
|
170
|
-
|
|
266
|
+
const seen = new Set<string>()
|
|
267
|
+
// Keywords sitting beside a `$ref` along the chain, outermost first.
|
|
268
|
+
// OpenAPI 3.1 / JSON Schema 2020-12 allow `$ref` to carry siblings
|
|
269
|
+
// (`description`, `required`, constraints, ...) and they still apply, so
|
|
270
|
+
// following the chain must not discard them. Merged onto the final target
|
|
271
|
+
// below, outermost last so the most specific alias wins.
|
|
272
|
+
const siblings: any[] = []
|
|
273
|
+
let current: any = undefined
|
|
274
|
+
let pointer = ref
|
|
275
|
+
|
|
276
|
+
for (; ;) {
|
|
277
|
+
if (!pointer.startsWith('#/')) return undefined
|
|
278
|
+
if (seen.has(pointer)) return undefined
|
|
279
|
+
seen.add(pointer)
|
|
280
|
+
|
|
281
|
+
const parts = pointer
|
|
282
|
+
.substring(2)
|
|
283
|
+
.split('/')
|
|
284
|
+
.map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'))
|
|
285
|
+
|
|
286
|
+
current = root
|
|
287
|
+
for (const part of parts) {
|
|
288
|
+
if (current == null || typeof current !== 'object') return undefined
|
|
289
|
+
current = current[part]
|
|
290
|
+
}
|
|
171
291
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
292
|
+
// Landed on another alias: follow it. Anything else is the target.
|
|
293
|
+
if (current != null &&
|
|
294
|
+
'object' === typeof current &&
|
|
295
|
+
!Array.isArray(current) &&
|
|
296
|
+
'string' === typeof current.$ref) {
|
|
297
|
+
const sib: any = {}
|
|
298
|
+
let hasSib = false
|
|
299
|
+
for (const k of Object.keys(current)) {
|
|
300
|
+
if ('$ref' === k) continue
|
|
301
|
+
sib[k] = current[k]
|
|
302
|
+
hasSib = true
|
|
303
|
+
}
|
|
304
|
+
if (hasSib) siblings.push(sib)
|
|
305
|
+
pointer = current.$ref
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
176
308
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
309
|
+
if (0 === siblings.length) {
|
|
310
|
+
// No siblings anywhere on the chain: return the target itself, so
|
|
311
|
+
// multiple references keep sharing one object (see the note on
|
|
312
|
+
// addXRefsAndResolve — a fresh copy per site would defeat that).
|
|
313
|
+
return current
|
|
314
|
+
}
|
|
182
315
|
|
|
183
|
-
|
|
316
|
+
// Siblings present: a merged view is necessarily a new object. Apply
|
|
317
|
+
// deepest-first so the outermost alias's keywords win.
|
|
318
|
+
const merged: any = (null != current && 'object' === typeof current &&
|
|
319
|
+
!Array.isArray(current)) ? { ...current } : {}
|
|
320
|
+
for (let i = siblings.length - 1; 0 <= i; i--) {
|
|
321
|
+
Object.assign(merged, siblings[i])
|
|
322
|
+
}
|
|
323
|
+
return merged
|
|
324
|
+
}
|
|
184
325
|
}
|
|
185
326
|
|
|
186
327
|
|
package/src/transform/field.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { each, getx } from 'jostraca'
|
|
|
4
4
|
|
|
5
5
|
import type { TransformResult, Transform } from '../transform'
|
|
6
6
|
|
|
7
|
-
import { validator, canonize, inferFieldType, normalizeFieldName } from '../utility'
|
|
7
|
+
import { validator, canonize, inferFieldType, normalizeFieldName, envelopeProp } from '../utility'
|
|
8
8
|
|
|
9
9
|
import { KIT } from '../types'
|
|
10
10
|
|
|
@@ -150,6 +150,21 @@ function findFieldDefs(
|
|
|
150
150
|
fieldSets = getx(responses, '201 content "application/json" schema') ??
|
|
151
151
|
getx(responses, '201 schema')
|
|
152
152
|
}
|
|
153
|
+
|
|
154
|
+
// Single-entity responses get the same treatment the list branch above
|
|
155
|
+
// already gives collections: a body that is only an envelope around the
|
|
156
|
+
// entity — `{item: {...}}` — describes the WRAPPER, not the entity, so
|
|
157
|
+
// its sole property would otherwise be harvested as a field. That is
|
|
158
|
+
// how an entity `todoitem` ended up with a required `item` field of
|
|
159
|
+
// type object, which then appeared in the generated create/update data
|
|
160
|
+
// types. envelopeProp applies the same two rules used to pick the
|
|
161
|
+
// response transform, so the field list and the transform agree.
|
|
162
|
+
if ('list' != mop.name) {
|
|
163
|
+
const envelope = envelopeProp(fieldSets?.properties, mop.name)
|
|
164
|
+
if (null != envelope) {
|
|
165
|
+
fieldSets = fieldSets.properties[envelope]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
153
168
|
}
|
|
154
169
|
|
|
155
170
|
// A QUERY (RFC 10008) request body is a filter/query schema, not the
|
package/src/transform/top.ts
CHANGED
|
@@ -44,6 +44,13 @@ const topTransform = async function(
|
|
|
44
44
|
kit.info = stringifyInfoScalars(def.info ?? {})
|
|
45
45
|
kit.info.servers = stringifyInfoScalars(def.servers ?? [])
|
|
46
46
|
|
|
47
|
+
// Guarantee at least one sentence of API description. Many specs (e.g. the
|
|
48
|
+
// readme.io-hosted Bluefin APIs) ship a placeholder `info.description` of
|
|
49
|
+
// "." — letterless, useless prose. When the description is empty or has no
|
|
50
|
+
// letters, synthesise a sentence from the title so the api-info.aontu (and
|
|
51
|
+
// the docs generated from it) never carry an empty/degenerate description.
|
|
52
|
+
kit.info.description = ensureDescription(kit.info)
|
|
53
|
+
|
|
47
54
|
// Public APIs that declare NO authentication (no security schemes, no
|
|
48
55
|
// top-level `security`, and no per-operation `security`) get an explicit
|
|
49
56
|
// no-auth signal in the model. Downstream sdkgen reads it via
|
|
@@ -120,6 +127,31 @@ const topTransform = async function(
|
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
|
|
130
|
+
// True when the text carries at least one alphabetic character — i.e. it is
|
|
131
|
+
// real prose rather than a placeholder like "." / "---" / whitespace.
|
|
132
|
+
function hasLetters(text: string): boolean {
|
|
133
|
+
return /[a-zA-Z]/.test(text)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
// A non-empty, at-least-one-sentence description for the API. Keeps the spec's
|
|
138
|
+
// own `info.description` when it is real prose; otherwise synthesises a sentence
|
|
139
|
+
// from the title (falling back to a generic sentence when even that is
|
|
140
|
+
// missing). Never returns an empty or letterless string.
|
|
141
|
+
function ensureDescription(info: any): string {
|
|
142
|
+
const current = 'string' === typeof info.description ? info.description.trim() : ''
|
|
143
|
+
if ('' !== current && hasLetters(current)) {
|
|
144
|
+
return info.description
|
|
145
|
+
}
|
|
146
|
+
const title = 'string' === typeof info.title ? info.title.trim() : ''
|
|
147
|
+
if ('' === title || !hasLetters(title)) {
|
|
148
|
+
return 'Client SDK for this API.'
|
|
149
|
+
}
|
|
150
|
+
// Avoid a redundant "… Api API." when the title already names itself an API.
|
|
151
|
+
return 'The ' + title + (/\bapi\b/i.test(title) ? '' : ' API') + '.'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
123
155
|
// A short one-line description of the API's purpose: the spec's
|
|
124
156
|
// `info.summary` (OpenAPI 3.1) when present, else the first prose sentence
|
|
125
157
|
// of `info.description` with leading markdown headings/blank lines stripped
|
|
@@ -129,12 +161,14 @@ function resolveSummary(def: any): string | undefined {
|
|
|
129
161
|
const info = def?.info ?? {}
|
|
130
162
|
|
|
131
163
|
const explicit = 'string' === typeof info.summary ? info.summary.trim() : ''
|
|
132
|
-
if ('' !== explicit) {
|
|
164
|
+
if ('' !== explicit && hasLetters(explicit)) {
|
|
133
165
|
return firstSentence(explicit)
|
|
134
166
|
}
|
|
135
167
|
|
|
136
168
|
const desc = 'string' === typeof info.description ? info.description : ''
|
|
137
|
-
|
|
169
|
+
// Treat letterless prose (a bare "." placeholder, "---", …) as no summary
|
|
170
|
+
// rather than surfacing it verbatim.
|
|
171
|
+
if ('' === desc.trim() || !hasLetters(desc)) {
|
|
138
172
|
return undefined
|
|
139
173
|
}
|
|
140
174
|
|
|
@@ -418,6 +452,7 @@ export {
|
|
|
418
452
|
topTransform,
|
|
419
453
|
resolveSecurity,
|
|
420
454
|
resolveSummary,
|
|
455
|
+
ensureDescription,
|
|
421
456
|
resolveWebsite,
|
|
422
457
|
homepageFromServer,
|
|
423
458
|
findAuthPrefix,
|
package/src/tsconfig.json
CHANGED
package/src/types.ts
CHANGED
|
@@ -160,6 +160,11 @@ type Metrics = {
|
|
|
160
160
|
|
|
161
161
|
type ApiDefContext = {
|
|
162
162
|
fs: any,
|
|
163
|
+
// True only when the caller supplied ApiDefOptions.fs (e.g. memfs), as
|
|
164
|
+
// opposed to the node:fs default. aontu/@tabnas/multisource treats the
|
|
165
|
+
// presence of an injected fs as "paths are POSIX", so forwarding the real
|
|
166
|
+
// node:fs breaks include resolution on Windows. See guide/guide.ts.
|
|
167
|
+
fsInjected: boolean,
|
|
163
168
|
log: any,
|
|
164
169
|
spec: any,
|
|
165
170
|
opts: any,
|