@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specbook/codegen",
3
- "version": "0.9.1",
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.9.1",
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
@@ -13,7 +13,9 @@ export type {
13
13
  GenerateFlags,
14
14
  GenerateOptions,
15
15
  ManifestOperation,
16
+ ManifestSecurityScheme,
16
17
  OperationMeta,
18
+ ResponseDoc,
17
19
  } from './build'
18
20
  export { generateFromDoc, logoOf } from './build'
19
21
 
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?: boolean
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 found = findJsonLike(resolveAny<any>(rawRes)?.content)
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: ${Array.isArray(effective) && effective.length > 0 ? 'true' : 'false'}`
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 ? { security: Array.isArray(effective) && effective.length > 0 } : {}),
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
  },
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export type {
3
3
  GenerateFlags,
4
4
  GenerateOptions,
5
5
  ManifestOperation,
6
+ ManifestSecurityScheme,
6
7
  OperationMeta,
7
8
  } from './build'
8
9
  export { generate, generateFromDoc, withPlainSpec } from './generate'