@pikku/openapi-parser 0.12.20 → 0.12.22

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.
@@ -1,467 +0,0 @@
1
- /**
2
- * Parses OpenAPI YAML/JSON specs, resolves $ref pointers, and produces a normalized IR.
3
- */
4
- import { createHash } from 'crypto'
5
- import { readFile } from 'fs/promises'
6
- import { parse as parseYAML } from 'yaml'
7
-
8
- import type { OpenAPISchema } from './zod-codegen.js'
9
-
10
- export interface ErrorResponse {
11
- statusCode: number
12
- description: string
13
- }
14
-
15
- export interface SecuritySchemeInfo {
16
- type: 'oauth2' | 'http' | 'apiKey'
17
- scheme?: string
18
- bearerFormat?: string
19
- name?: string
20
- in?: string
21
- flows?: {
22
- authorizationUrl?: string
23
- tokenUrl?: string
24
- scopes?: Record<string, string>
25
- }
26
- }
27
-
28
- export interface ParsedSpec {
29
- info: { title: string; version: string; description?: string }
30
- baseUrl: string
31
- serverUrls: string[]
32
- authType: 'bearer' | 'oauth2' | 'apiKey' | 'none'
33
- operations: ParsedOperation[]
34
- componentSchemas: Record<string, OpenAPISchema>
35
- securitySchemes: Record<string, SecuritySchemeInfo>
36
- tagDescriptions: Record<string, string>
37
- }
38
-
39
- export interface ParsedOperation {
40
- operationId?: string
41
- method: string
42
- path: string
43
- summary?: string
44
- description?: string
45
- tags: string[]
46
- pathParams: ParsedParam[]
47
- queryParams: ParsedParam[]
48
- headerParams: ParsedParam[]
49
- requestBody?: OpenAPISchema
50
- requestBodyDescription?: string
51
- requestBodyRequired?: boolean
52
- responseSchema?: OpenAPISchema
53
- responseDescription?: string
54
- errorResponses: ErrorResponse[]
55
- deprecated: boolean
56
- }
57
-
58
- export interface ParsedParam {
59
- name: string
60
- required: boolean
61
- schema: OpenAPISchema
62
- description?: string
63
- example?: unknown
64
- }
65
-
66
- /**
67
- * Read and parse an OpenAPI spec from a file path.
68
- * Supports both YAML (.yaml, .yml) and JSON (.json) files.
69
- */
70
- export async function parseOpenAPISpec(filePath: string): Promise<ParsedSpec> {
71
- const content = await readFile(filePath, 'utf-8')
72
-
73
- let doc: any
74
- if (filePath.endsWith('.json')) {
75
- doc = JSON.parse(content)
76
- } else {
77
- doc = parseYAML(content)
78
- }
79
-
80
- // Validate spec version
81
- const specVersion = doc.openapi ?? doc.swagger
82
- if (!specVersion) {
83
- throw new Error(
84
- 'Not a valid OpenAPI/Swagger spec: missing "openapi" or "swagger" version field.'
85
- )
86
- }
87
- const major = String(specVersion).split('.')[0]
88
- if (major !== '2' && major !== '3') {
89
- throw new Error(
90
- `Unsupported spec version "${specVersion}". Only OpenAPI 3.x and Swagger 2.x are supported.`
91
- )
92
- }
93
-
94
- // Resolve all $ref pointers in-place
95
- resolveRefs(doc, doc)
96
-
97
- const info = {
98
- title: doc.info?.title ?? 'Unknown API',
99
- version: doc.info?.version ?? '1.0.0',
100
- description: doc.info?.description,
101
- }
102
-
103
- const serverUrls = extractServerUrls(doc)
104
- const baseUrl = serverUrls[0] ?? ''
105
- const authType = detectAuthType(doc)
106
- const securitySchemes = extractSecuritySchemes(doc)
107
- const tagDescriptions = extractTagDescriptions(doc)
108
-
109
- // Extract component schemas (OpenAPI 3.x: components.schemas, Swagger 2.x: definitions)
110
- const componentSchemas: Record<string, OpenAPISchema> = {}
111
- const rawSchemas = doc.components?.schemas ?? doc.definitions ?? {}
112
- for (const [name, schema] of Object.entries(rawSchemas)) {
113
- componentSchemas[name] = schema as OpenAPISchema
114
- }
115
-
116
- // Extract operations (skip deprecated)
117
- const operations: ParsedOperation[] = []
118
- if (doc.paths) {
119
- for (const [path, pathItem] of Object.entries(
120
- doc.paths as Record<string, any>
121
- )) {
122
- // Shared parameters at the path level
123
- const sharedParams: any[] = pathItem.parameters ?? []
124
-
125
- for (const method of ['get', 'post', 'put', 'patch', 'delete'] as const) {
126
- const op = pathItem[method]
127
- if (!op) continue
128
-
129
- // Skip deprecated operations
130
- if (op.deprecated === true) continue
131
-
132
- const allParams: any[] = [...sharedParams, ...(op.parameters ?? [])]
133
-
134
- // OpenAPI 3.x: op.requestBody, Swagger 2.x: parameters[].in === 'body'
135
- const body3 = op.requestBody
136
- const body2 = allParams.find((p: any) => p.in === 'body')
137
-
138
- operations.push({
139
- operationId: op.operationId,
140
- method,
141
- path,
142
- summary: op.summary,
143
- description: op.description,
144
- tags: op.tags ?? [],
145
- pathParams: extractParams(allParams, 'path'),
146
- queryParams: extractParams(allParams, 'query'),
147
- headerParams: extractParams(allParams, 'header'),
148
- requestBody: extractRequestBody(op),
149
- requestBodyDescription: body3?.description ?? body2?.description,
150
- requestBodyRequired: body3?.required ?? body2?.required,
151
- responseSchema: extractResponseSchema(op),
152
- responseDescription: extractResponseDescription(op),
153
- errorResponses: extractErrorResponses(op),
154
- deprecated: false,
155
- })
156
- }
157
- }
158
- }
159
-
160
- return {
161
- info,
162
- baseUrl,
163
- serverUrls,
164
- authType,
165
- operations,
166
- componentSchemas,
167
- securitySchemes,
168
- tagDescriptions,
169
- }
170
- }
171
-
172
- /** Recursively resolve $ref pointers in-place, with cycle detection */
173
- function resolveRefs(node: any, root: any, visited?: Set<any>): any {
174
- if (node === null || typeof node !== 'object') return node
175
-
176
- const seen = visited ?? new Set<any>()
177
- if (seen.has(node)) return node
178
- seen.add(node)
179
-
180
- if (Array.isArray(node)) {
181
- for (let i = 0; i < node.length; i++) {
182
- node[i] = resolveRefs(node[i], root, seen)
183
- }
184
- return node
185
- }
186
-
187
- if (typeof node.$ref === 'string') {
188
- const resolved = resolveRefPath(node.$ref, root)
189
- if (resolved && typeof resolved === 'object') {
190
- // Merge any sibling properties (like description overrides)
191
- const { $ref, ...siblings } = node
192
- const result = { ...resolved, ...siblings }
193
- // Don't recurse into the merged result — it shares structure with
194
- // the original resolved object and may be circular
195
- return result
196
- }
197
- return node
198
- }
199
-
200
- for (const key of Object.keys(node)) {
201
- node[key] = resolveRefs(node[key], root, seen)
202
- }
203
-
204
- return node
205
- }
206
-
207
- function resolveRefPath(ref: string, root: any): any {
208
- if (!ref.startsWith('#/')) return undefined
209
- const parts = ref.slice(2).split('/')
210
- let current = root
211
- for (const part of parts) {
212
- const decoded = part.replace(/~1/g, '/').replace(/~0/g, '~')
213
- if (current == null || typeof current !== 'object') return undefined
214
- current = current[decoded]
215
- }
216
- return current
217
- }
218
-
219
- function extractServerUrls(doc: any): string[] {
220
- // OpenAPI 3.x
221
- if (doc.servers && doc.servers.length > 0) {
222
- return doc.servers.map((s: any) => s.url).filter((url: string) => !!url)
223
- }
224
- // Swagger 2.x
225
- if (doc.host) {
226
- const schemes = doc.schemes?.length ? doc.schemes : ['https']
227
- const basePath = doc.basePath ?? ''
228
- return schemes.map((scheme: string) => `${scheme}://${doc.host}${basePath}`)
229
- }
230
- return []
231
- }
232
-
233
- function detectAuthType(doc: any): 'bearer' | 'oauth2' | 'apiKey' | 'none' {
234
- const securitySchemes =
235
- doc.components?.securitySchemes ?? doc.securityDefinitions ?? {}
236
-
237
- for (const scheme of Object.values(securitySchemes) as any[]) {
238
- if (scheme.type === 'oauth2') return 'oauth2'
239
- if (scheme.type === 'http' && scheme.scheme === 'bearer') return 'bearer'
240
- if (scheme.type === 'apiKey') return 'apiKey'
241
- }
242
-
243
- return 'none'
244
- }
245
-
246
- /**
247
- * Build an OpenAPI schema object from v2-style parameter-level properties
248
- * (type, enum, items, format, default) that aren't nested under `schema`.
249
- */
250
- function paramToSchema(p: any): OpenAPISchema {
251
- const schema: any = { type: p.type ?? 'string' }
252
- if (p.enum) schema.enum = p.enum
253
- if (p.items) schema.items = p.items
254
- if (p.format) schema.format = p.format
255
- if (p.default !== undefined) schema.default = p.default
256
- return schema
257
- }
258
-
259
- function extractParams(
260
- params: any[],
261
- location: 'path' | 'query' | 'header'
262
- ): ParsedParam[] {
263
- return params
264
- .filter((p) => p.in === location)
265
- .map((p) => ({
266
- name: p.name,
267
- required: p.required ?? location === 'path',
268
- schema: (p.schema ?? paramToSchema(p)) as OpenAPISchema,
269
- description: p.description,
270
- example: p.example,
271
- }))
272
- }
273
-
274
- function extractRequestBody(op: any): OpenAPISchema | undefined {
275
- // OpenAPI 3.x: op.requestBody.content['application/json'].schema
276
- const body = op.requestBody
277
- if (body) {
278
- const content = body.content
279
- if (content) {
280
- const jsonContent = content['application/json']
281
- if (jsonContent?.schema) return jsonContent.schema as OpenAPISchema
282
-
283
- const firstKey = Object.keys(content)[0]
284
- if (firstKey && content[firstKey]?.schema) {
285
- return content[firstKey].schema as OpenAPISchema
286
- }
287
- }
288
- }
289
-
290
- // Swagger 2.x: parameters[].in === 'body' → schema
291
- if (op.parameters) {
292
- const bodyParam = op.parameters.find((p: any) => p.in === 'body')
293
- if (bodyParam?.schema) return bodyParam.schema as OpenAPISchema
294
- }
295
-
296
- return undefined
297
- }
298
-
299
- function extractResponseDescription(op: any): string | undefined {
300
- const responses = op.responses
301
- if (!responses) return undefined
302
- for (const code of ['200', '201', '202']) {
303
- const resp = responses[code]
304
- if (resp?.description) return resp.description
305
- }
306
- return undefined
307
- }
308
-
309
- function extractResponseSchema(op: any): OpenAPISchema | undefined {
310
- const responses = op.responses
311
- if (!responses) return undefined
312
-
313
- // Look for 2xx responses in order of preference
314
- for (const code of ['200', '201', '202', '204']) {
315
- const resp = responses[code]
316
- if (!resp) continue
317
-
318
- // OpenAPI 3.x: resp.content['application/json'].schema
319
- const content = resp.content
320
- if (content) {
321
- const jsonContent = content['application/json']
322
- if (jsonContent?.schema) return jsonContent.schema as OpenAPISchema
323
-
324
- const firstKey = Object.keys(content)[0]
325
- if (firstKey && content[firstKey]?.schema) {
326
- return content[firstKey].schema as OpenAPISchema
327
- }
328
- }
329
-
330
- // Swagger 2.x: resp.schema directly
331
- if (resp.schema) return resp.schema as OpenAPISchema
332
- }
333
-
334
- // Fallback: any 2xx
335
- for (const [code, resp] of Object.entries(responses) as [string, any][]) {
336
- if (!code.startsWith('2')) continue
337
- if (resp.content) {
338
- const jsonContent = resp.content['application/json']
339
- if (jsonContent?.schema) return jsonContent.schema as OpenAPISchema
340
- }
341
- if (resp.schema) return resp.schema as OpenAPISchema
342
- }
343
-
344
- return undefined
345
- }
346
-
347
- function extractErrorResponses(op: any): ErrorResponse[] {
348
- const responses = op.responses
349
- if (!responses) return []
350
-
351
- const errors: ErrorResponse[] = []
352
- for (const [code, resp] of Object.entries(responses) as [string, any][]) {
353
- const statusCode = parseInt(code, 10)
354
- if (isNaN(statusCode)) continue
355
- if (statusCode >= 400) {
356
- errors.push({
357
- statusCode,
358
- description: resp.description ?? `Error ${statusCode}`,
359
- })
360
- }
361
- }
362
- return errors
363
- }
364
-
365
- function extractSecuritySchemes(doc: any): Record<string, SecuritySchemeInfo> {
366
- const raw = doc.components?.securitySchemes ?? doc.securityDefinitions ?? {}
367
- const result: Record<string, SecuritySchemeInfo> = {}
368
-
369
- for (const [name, scheme] of Object.entries(raw) as [string, any][]) {
370
- const info: SecuritySchemeInfo = {
371
- type: scheme.type === 'http' ? 'http' : scheme.type,
372
- }
373
-
374
- if (scheme.type === 'http') {
375
- info.scheme = scheme.scheme
376
- info.bearerFormat = scheme.bearerFormat
377
- }
378
-
379
- if (scheme.type === 'apiKey') {
380
- info.name = scheme.name
381
- info.in = scheme.in
382
- }
383
-
384
- if (scheme.type === 'oauth2') {
385
- // Extract flows — prefer authorizationCode, then implicit, then clientCredentials
386
- const flows = scheme.flows ?? {}
387
- const flow =
388
- flows.authorizationCode ??
389
- flows.implicit ??
390
- flows.clientCredentials ??
391
- flows.password
392
-
393
- if (flow) {
394
- info.flows = {
395
- authorizationUrl: flow.authorizationUrl,
396
- tokenUrl: flow.tokenUrl,
397
- scopes: flow.scopes,
398
- }
399
- }
400
- }
401
-
402
- result[name] = info
403
- }
404
-
405
- return result
406
- }
407
-
408
- /**
409
- * Compute a SHA-256 hash of the contract-relevant parts of a parsed spec.
410
- * Excludes presentation fields (summary, description, tags, operationId)
411
- * so that only structural API changes produce a different hash.
412
- */
413
- export function computeContractHash(spec: ParsedSpec): string {
414
- const contract = {
415
- operations: spec.operations.map((op) => ({
416
- method: op.method,
417
- path: op.path,
418
- pathParams: op.pathParams.map((p) => ({
419
- name: p.name,
420
- required: p.required,
421
- schema: p.schema,
422
- })),
423
- queryParams: op.queryParams.map((p) => ({
424
- name: p.name,
425
- required: p.required,
426
- schema: p.schema,
427
- })),
428
- headerParams: op.headerParams.map((p) => ({
429
- name: p.name,
430
- required: p.required,
431
- schema: p.schema,
432
- })),
433
- requestBody: op.requestBody,
434
- requestBodyRequired: op.requestBodyRequired,
435
- responseSchema: op.responseSchema,
436
- errorResponses: op.errorResponses,
437
- })),
438
- componentSchemas: spec.componentSchemas,
439
- securitySchemes: spec.securitySchemes,
440
- serverUrls: spec.serverUrls,
441
- }
442
-
443
- // Use a cycle-safe JSON serializer since resolved $ref schemas
444
- // can contain circular object references
445
- const seen = new WeakSet()
446
- const json = JSON.stringify(contract, (_key, value) => {
447
- if (typeof value === 'object' && value !== null) {
448
- if (seen.has(value)) return '[Circular]'
449
- seen.add(value)
450
- }
451
- return value
452
- })
453
-
454
- return createHash('sha256').update(json).digest('hex').slice(0, 16)
455
- }
456
-
457
- function extractTagDescriptions(doc: any): Record<string, string> {
458
- const result: Record<string, string> = {}
459
- if (Array.isArray(doc.tags)) {
460
- for (const tag of doc.tags) {
461
- if (tag.name && tag.description) {
462
- result[tag.name] = tag.description
463
- }
464
- }
465
- }
466
- return result
467
- }
@@ -1,9 +0,0 @@
1
- export {
2
- type OpenAPISchema,
3
- type ZodCodegenContext,
4
- createContext,
5
- schemaToZod,
6
- schemaVarName,
7
- sanitizeTypeName,
8
- snakeToCamel,
9
- } from './openapi-to-zod-schema.js'
package/tsconfig.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "extends": "../tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "module": "Node18",
6
- "outDir": "dist",
7
- "target": "esnext",
8
- "declaration": true
9
- },
10
- "include": ["src/**/*.ts"],
11
- "exclude": ["**/*.test.ts", "node_modules"]
12
- }
@@ -1 +0,0 @@
1
- {"root":["./src/auth-config.ts","./src/codegen.ts","./src/index.ts","./src/naming.ts","./src/openapi-to-zod-schema.ts","./src/parse-openapi.ts","./src/zod-codegen.ts"],"version":"6.0.3"}