@specbook/codegen 0.9.1 → 0.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/package.json +2 -2
- package/src/browser.ts +2 -0
- package/src/build.ts +85 -5
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@specbook/codegen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Turns an OpenAPI or Swagger document into types, examples, schemas and metadata, in Node or in a browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"specbook",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"swagger2openapi": "7.0.8"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@specbook/core": "0.
|
|
47
|
+
"@specbook/core": "0.11.0",
|
|
48
48
|
"@types/node": "24.19.2",
|
|
49
49
|
"@types/swagger2openapi": "7.0.4",
|
|
50
50
|
"typescript": "7.0.2"
|
package/src/browser.ts
CHANGED
package/src/build.ts
CHANGED
|
@@ -73,6 +73,13 @@ interface ExampleDoc {
|
|
|
73
73
|
body?: unknown
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** What `docs/<id>.json` says about one response, for the Docs tab. */
|
|
77
|
+
export interface ResponseDoc {
|
|
78
|
+
description?: string
|
|
79
|
+
/** the media type the body is written in, as the spec names it */
|
|
80
|
+
contentType?: string
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
export interface ManifestOperation {
|
|
77
84
|
id: string
|
|
78
85
|
method: string
|
|
@@ -89,9 +96,55 @@ export interface ManifestOperation {
|
|
|
89
96
|
examples: ManifestExample[]
|
|
90
97
|
/** the spec requires credentials for this operation */
|
|
91
98
|
secured?: boolean
|
|
99
|
+
/** any one of these is enough; every scheme in one entry must apply together, with the scopes it needs */
|
|
100
|
+
security?: Record<string, string[]>[]
|
|
92
101
|
file: string
|
|
93
102
|
}
|
|
94
103
|
|
|
104
|
+
/** What a security scheme is, without addresses or descriptions: enough to say how an operation is secured. */
|
|
105
|
+
export interface ManifestSecurityScheme {
|
|
106
|
+
type: string
|
|
107
|
+
in?: string
|
|
108
|
+
name?: string
|
|
109
|
+
scheme?: string
|
|
110
|
+
openIdConnectUrl?: string
|
|
111
|
+
/** flow name -> its scope names */
|
|
112
|
+
flows?: Record<string, { scopes: string[] }>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The document's security schemes in the manifest's compact form. */
|
|
116
|
+
export function securitySchemesOf(doc: any): Record<string, ManifestSecurityScheme> {
|
|
117
|
+
const out: Record<string, ManifestSecurityScheme> = {}
|
|
118
|
+
for (const [name, s] of Object.entries<any>(doc.components?.securitySchemes ?? {})) {
|
|
119
|
+
if (!s || typeof s.type !== 'string') continue
|
|
120
|
+
const flows = Object.fromEntries(
|
|
121
|
+
Object.entries<any>(s.flows ?? {}).map(([f, v]) => [
|
|
122
|
+
f,
|
|
123
|
+
{ scopes: Object.keys(v?.scopes ?? {}) },
|
|
124
|
+
]),
|
|
125
|
+
)
|
|
126
|
+
out[name] = {
|
|
127
|
+
type: s.type,
|
|
128
|
+
...(typeof s.in === 'string' ? { in: s.in } : {}),
|
|
129
|
+
...(typeof s.name === 'string' ? { name: s.name } : {}),
|
|
130
|
+
...(typeof s.scheme === 'string' ? { scheme: s.scheme } : {}),
|
|
131
|
+
...(typeof s.openIdConnectUrl === 'string' ? { openIdConnectUrl: s.openIdConnectUrl } : {}),
|
|
132
|
+
...(Object.keys(flows).length ? { flows } : {}),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** An operation's requirements as a plain list of `{ scheme: scopes }`. */
|
|
139
|
+
function requirementsFrom(effective: unknown): Record<string, string[]>[] {
|
|
140
|
+
if (!Array.isArray(effective)) return []
|
|
141
|
+
return effective.map((r: any) =>
|
|
142
|
+
Object.fromEntries(
|
|
143
|
+
Object.entries(r ?? {}).map(([k, v]) => [k, Array.isArray(v) ? v.map(String) : []]),
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
95
148
|
/** `info.x-logo` (a Redoc convention): only absolute http(s) or data-image URLs are trusted. */
|
|
96
149
|
export function logoOf(
|
|
97
150
|
info: any,
|
|
@@ -128,6 +181,7 @@ function findJsonLike(content: Record<string, any> | undefined) {
|
|
|
128
181
|
schema: content[mime].schema as Schema | undefined,
|
|
129
182
|
encoding: enc,
|
|
130
183
|
media: content[mime],
|
|
184
|
+
mime,
|
|
131
185
|
}
|
|
132
186
|
}
|
|
133
187
|
const json = Object.keys(content).find((m) => /json/.test(m))
|
|
@@ -136,6 +190,7 @@ function findJsonLike(content: Record<string, any> | undefined) {
|
|
|
136
190
|
schema: content[json].schema as Schema | undefined,
|
|
137
191
|
encoding: 'json',
|
|
138
192
|
media: content[json],
|
|
193
|
+
mime: json,
|
|
139
194
|
}
|
|
140
195
|
const binary = Object.keys(content).find((m) => BINARY_MIME.test(m))
|
|
141
196
|
if (binary)
|
|
@@ -143,6 +198,7 @@ function findJsonLike(content: Record<string, any> | undefined) {
|
|
|
143
198
|
schema: content[binary].schema as Schema | undefined,
|
|
144
199
|
encoding: 'binary',
|
|
145
200
|
media: content[binary],
|
|
201
|
+
mime: binary,
|
|
146
202
|
mediaType: binary,
|
|
147
203
|
}
|
|
148
204
|
// A wildcard stands for whatever the server sends, and a framework that is not told what it produces
|
|
@@ -155,6 +211,7 @@ function findJsonLike(content: Record<string, any> | undefined) {
|
|
|
155
211
|
schema,
|
|
156
212
|
encoding: file ? 'binary' : 'json',
|
|
157
213
|
media: content[any],
|
|
214
|
+
mime: any,
|
|
158
215
|
...(file ? { mediaType: any } : {}),
|
|
159
216
|
}
|
|
160
217
|
}
|
|
@@ -245,7 +302,8 @@ export interface OperationMeta {
|
|
|
245
302
|
mediaType?: string
|
|
246
303
|
files?: string[]
|
|
247
304
|
formArrays?: Record<string, string>
|
|
248
|
-
security
|
|
305
|
+
/** `false` = none required; otherwise any one of the requirements (see `ManifestOperation.security`) */
|
|
306
|
+
security?: false | Record<string, string[]>[]
|
|
249
307
|
}
|
|
250
308
|
|
|
251
309
|
/** The generator proper, from an OpenAPI 3 document that is already parsed and bundled. */
|
|
@@ -497,6 +555,18 @@ function generateAll(
|
|
|
497
555
|
let defaultSchema: Schema | undefined
|
|
498
556
|
const responseSchemas: Record<string, Schema> = {}
|
|
499
557
|
const mockVariants: NonNullable<ManifestOperation['mockVariants']> = {}
|
|
558
|
+
const responseDocs: Record<string, ResponseDoc> = {}
|
|
559
|
+
let defaultDoc: ResponseDoc | undefined
|
|
560
|
+
const docOf = (res: any, found: { mime?: string } | undefined): ResponseDoc | undefined => {
|
|
561
|
+
const description =
|
|
562
|
+
typeof res?.description === 'string' && res.description.trim()
|
|
563
|
+
? res.description.trim()
|
|
564
|
+
: undefined
|
|
565
|
+
const contentType = found?.mime ?? Object.keys(res?.content ?? {})[0]
|
|
566
|
+
return description || contentType
|
|
567
|
+
? { ...(description ? { description } : {}), ...(contentType ? { contentType } : {}) }
|
|
568
|
+
: undefined
|
|
569
|
+
}
|
|
500
570
|
let defaultVariants: ManifestOperation['mockVariants'] extends infer M
|
|
501
571
|
? M extends Record<string, infer V>
|
|
502
572
|
? V | undefined
|
|
@@ -504,7 +574,9 @@ function generateAll(
|
|
|
504
574
|
: never
|
|
505
575
|
for (const [code, rawRes] of Object.entries<any>(op.responses ?? {})) {
|
|
506
576
|
if (code === 'default') {
|
|
507
|
-
const
|
|
577
|
+
const defaultRes = resolveAny<any>(rawRes)
|
|
578
|
+
const found = findJsonLike(defaultRes?.content)
|
|
579
|
+
defaultDoc = docOf(defaultRes, found)
|
|
508
580
|
if (found?.schema) {
|
|
509
581
|
hasDefault = true
|
|
510
582
|
if (ts) defaultType = tsType(found.schema, ' ', 'T.')
|
|
@@ -522,6 +594,8 @@ function generateAll(
|
|
|
522
594
|
const res = resolveAny<any>(rawRes)
|
|
523
595
|
const found = findJsonLike(res?.content)
|
|
524
596
|
statuses.push(Number(code))
|
|
597
|
+
const responseDoc = docOf(res, found)
|
|
598
|
+
if (responseDoc) responseDocs[code] = responseDoc
|
|
525
599
|
if (found?.schema) responseSchemas[code] = toJsonSchema(found.schema, `${defsId}#/$defs/`)
|
|
526
600
|
if (ts)
|
|
527
601
|
responseTypes.push(
|
|
@@ -540,6 +614,7 @@ function generateAll(
|
|
|
540
614
|
for (const st of DEFAULT_ERROR_STATUSES) {
|
|
541
615
|
if (statuses.includes(st)) continue
|
|
542
616
|
statuses.push(st)
|
|
617
|
+
if (defaultDoc) responseDocs[String(st)] = defaultDoc
|
|
543
618
|
if (defaultSchema) responseSchemas[String(st)] = defaultSchema
|
|
544
619
|
if (defaultVariants) mockVariants[String(st)] = defaultVariants
|
|
545
620
|
if (defaultType) responseTypes.push(`${st}: ${defaultType}`)
|
|
@@ -643,9 +718,10 @@ function generateAll(
|
|
|
643
718
|
|
|
644
719
|
const effective = op.security ?? doc.security
|
|
645
720
|
const secured = hasSchemes && Array.isArray(effective) && effective.length > 0
|
|
721
|
+
const requirements = requirementsFrom(effective)
|
|
646
722
|
const securityLine = !hasSchemes
|
|
647
723
|
? ''
|
|
648
|
-
: `, security: ${
|
|
724
|
+
: `, security: ${requirements.length > 0 ? JSON.stringify(requirements) : 'false'}`
|
|
649
725
|
// a binary body's declared type (`application/octet-stream`) is what the server expects as Content-Type
|
|
650
726
|
const mediaType =
|
|
651
727
|
body &&
|
|
@@ -673,7 +749,9 @@ function generateAll(
|
|
|
673
749
|
...(mediaType ? { mediaType } : {}),
|
|
674
750
|
...(fileFieldPaths.length ? { files: fileFieldPaths } : {}),
|
|
675
751
|
...(Object.keys(arrayStyles).length ? { formArrays: arrayStyles } : {}),
|
|
676
|
-
...(hasSchemes
|
|
752
|
+
...(hasSchemes
|
|
753
|
+
? { security: requirements.length > 0 ? requirements : (false as const) }
|
|
754
|
+
: {}),
|
|
677
755
|
}
|
|
678
756
|
if (ts)
|
|
679
757
|
files.push({
|
|
@@ -696,6 +774,7 @@ ${jsdoc({ description: [...new Set([op.summary, op.description].filter(Boolean))
|
|
|
696
774
|
content: `${JSON.stringify({
|
|
697
775
|
...(op.description && op.description !== summary ? { description: op.description } : {}),
|
|
698
776
|
examples: docs.map((d) => ({ name: d.name, title: d.title, source: d.source })),
|
|
777
|
+
...(Object.keys(responseDocs).length ? { responses: responseDocs } : {}),
|
|
699
778
|
})}\n`,
|
|
700
779
|
})
|
|
701
780
|
operations.push({
|
|
@@ -708,7 +787,7 @@ ${jsdoc({ description: [...new Set([op.summary, op.description].filter(Boolean))
|
|
|
708
787
|
statuses,
|
|
709
788
|
...(Object.keys(mockVariants).length ? { mockVariants } : {}),
|
|
710
789
|
examples: docs.map((d) => ({ name: d.name, title: d.title, source: d.source })),
|
|
711
|
-
...(secured ? { secured: true } : {}),
|
|
790
|
+
...(secured ? { secured: true, security: requirements } : {}),
|
|
712
791
|
file: `operations/${id}.ts`,
|
|
713
792
|
})
|
|
714
793
|
}
|
|
@@ -742,6 +821,7 @@ ${jsdoc({ description: [...new Set([op.summary, op.description].filter(Boolean))
|
|
|
742
821
|
version: doc.info?.version,
|
|
743
822
|
...(logoOf(doc.info) ? { logo: logoOf(doc.info) } : {}),
|
|
744
823
|
servers: (doc.servers ?? []).map((s: any) => s.url),
|
|
824
|
+
...(hasSchemes ? { securitySchemes: securitySchemesOf(doc) } : {}),
|
|
745
825
|
tags: [...byTag.keys()],
|
|
746
826
|
operations,
|
|
747
827
|
},
|