@pikku/openapi-parser 0.12.4 → 0.12.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @pikku/openapi-parser
2
2
 
3
+ ## 0.12.6
4
+
5
+ ### Patch Changes
6
+
7
+ - a57ff11: Add Swagger 2.0 support: extract requestBody from body parameters, responseSchema from direct response schema, and component schemas from definitions. Fix duplicate .describe() on request body properties.
8
+
9
+ ## 0.12.5
10
+
11
+ ### Patch Changes
12
+
13
+ - 8552e18: Don't generate `output: z.void()` for operations without response schemas — omit the field instead
14
+
3
15
  ## 0.12.4
4
16
 
5
17
  ### Patch Changes
package/dist/codegen.js CHANGED
@@ -119,7 +119,10 @@ function generateFunctionFile(named, parsed, vars, ctx, spec, flags) {
119
119
  const outputName = `${pascalName}Output`;
120
120
  // Determine error imports needed
121
121
  const errorClasses = getErrorClassesForResponses(parsed.errorResponses);
122
- lines.push("import { z } from 'zod'");
122
+ const needsZod = hasInput || !!parsed.responseSchema;
123
+ if (needsZod) {
124
+ lines.push("import { z } from 'zod'");
125
+ }
123
126
  lines.push("import { pikkuSessionlessFunc } from '#pikku'");
124
127
  if (errorClasses.length > 0) {
125
128
  lines.push(`import { ${errorClasses.join(', ')} } from '@pikku/core/errors'`);
@@ -148,9 +151,6 @@ function generateFunctionFile(named, parsed, vars, ctx, spec, flags) {
148
151
  if (parsed.responseSchema) {
149
152
  funcConfig.push(` output: ${outputName},`);
150
153
  }
151
- else {
152
- funcConfig.push(' output: z.void(),');
153
- }
154
154
  if (errorClasses.length > 0) {
155
155
  funcConfig.push(` errors: [${errorClasses.join(', ')}],`);
156
156
  }
@@ -186,10 +186,7 @@ function buildInputSchema(parsed, ctx) {
186
186
  continue;
187
187
  const isOptional = !requiredSet.has(key);
188
188
  const zodCode = schemaToZod(propSchema, ctx, { optional: isOptional });
189
- const withDesc = propSchema.description
190
- ? `${zodCode}.describe(${JSON.stringify(propSchema.description)})`
191
- : zodCode;
192
- props.push(` ${safeKey(key)}: ${withDesc},`);
189
+ props.push(` ${safeKey(key)}: ${zodCode},`);
193
190
  }
194
191
  }
195
192
  else {
@@ -38,12 +38,11 @@ export async function parseOpenAPISpec(filePath) {
38
38
  const authType = detectAuthType(doc);
39
39
  const securitySchemes = extractSecuritySchemes(doc);
40
40
  const tagDescriptions = extractTagDescriptions(doc);
41
- // Extract component schemas
41
+ // Extract component schemas (OpenAPI 3.x: components.schemas, Swagger 2.x: definitions)
42
42
  const componentSchemas = {};
43
- if (doc.components?.schemas) {
44
- for (const [name, schema] of Object.entries(doc.components.schemas)) {
45
- componentSchemas[name] = schema;
46
- }
43
+ const rawSchemas = doc.components?.schemas ?? doc.definitions ?? {};
44
+ for (const [name, schema] of Object.entries(rawSchemas)) {
45
+ componentSchemas[name] = schema;
47
46
  }
48
47
  // Extract operations (skip deprecated)
49
48
  const operations = [];
@@ -59,7 +58,9 @@ export async function parseOpenAPISpec(filePath) {
59
58
  if (op.deprecated === true)
60
59
  continue;
61
60
  const allParams = [...sharedParams, ...(op.parameters ?? [])];
62
- const body = op.requestBody;
61
+ // OpenAPI 3.x: op.requestBody, Swagger 2.x: parameters[].in === 'body'
62
+ const body3 = op.requestBody;
63
+ const body2 = allParams.find((p) => p.in === 'body');
63
64
  operations.push({
64
65
  operationId: op.operationId,
65
66
  method,
@@ -71,8 +72,8 @@ export async function parseOpenAPISpec(filePath) {
71
72
  queryParams: extractParams(allParams, 'query'),
72
73
  headerParams: extractParams(allParams, 'header'),
73
74
  requestBody: extractRequestBody(op),
74
- requestBodyDescription: body?.description,
75
- requestBodyRequired: body?.required,
75
+ requestBodyDescription: body3?.description ?? body2?.description,
76
+ requestBodyRequired: body3?.required ?? body2?.required,
76
77
  responseSchema: extractResponseSchema(op),
77
78
  responseDescription: extractResponseDescription(op),
78
79
  errorResponses: extractErrorResponses(op),
@@ -184,20 +185,25 @@ function extractParams(params, location) {
184
185
  }));
185
186
  }
186
187
  function extractRequestBody(op) {
188
+ // OpenAPI 3.x: op.requestBody.content['application/json'].schema
187
189
  const body = op.requestBody;
188
- if (!body)
189
- return undefined;
190
- const content = body.content;
191
- if (!content)
192
- return undefined;
193
- // Prefer JSON
194
- const jsonContent = content['application/json'];
195
- if (jsonContent?.schema)
196
- return jsonContent.schema;
197
- // Fallback to first content type
198
- const firstKey = Object.keys(content)[0];
199
- if (firstKey && content[firstKey]?.schema) {
200
- return content[firstKey].schema;
190
+ if (body) {
191
+ const content = body.content;
192
+ if (content) {
193
+ const jsonContent = content['application/json'];
194
+ if (jsonContent?.schema)
195
+ return jsonContent.schema;
196
+ const firstKey = Object.keys(content)[0];
197
+ if (firstKey && content[firstKey]?.schema) {
198
+ return content[firstKey].schema;
199
+ }
200
+ }
201
+ }
202
+ // Swagger 2.x: parameters[].in === 'body' → schema
203
+ if (op.parameters) {
204
+ const bodyParam = op.parameters.find((p) => p.in === 'body');
205
+ if (bodyParam?.schema)
206
+ return bodyParam.schema;
201
207
  }
202
208
  return undefined;
203
209
  }
@@ -221,24 +227,32 @@ function extractResponseSchema(op) {
221
227
  const resp = responses[code];
222
228
  if (!resp)
223
229
  continue;
230
+ // OpenAPI 3.x: resp.content['application/json'].schema
224
231
  const content = resp.content;
225
- if (!content)
226
- continue;
227
- const jsonContent = content['application/json'];
228
- if (jsonContent?.schema)
229
- return jsonContent.schema;
230
- const firstKey = Object.keys(content)[0];
231
- if (firstKey && content[firstKey]?.schema) {
232
- return content[firstKey].schema;
232
+ if (content) {
233
+ const jsonContent = content['application/json'];
234
+ if (jsonContent?.schema)
235
+ return jsonContent.schema;
236
+ const firstKey = Object.keys(content)[0];
237
+ if (firstKey && content[firstKey]?.schema) {
238
+ return content[firstKey].schema;
239
+ }
233
240
  }
241
+ // Swagger 2.x: resp.schema directly
242
+ if (resp.schema)
243
+ return resp.schema;
234
244
  }
235
245
  // Fallback: any 2xx
236
246
  for (const [code, resp] of Object.entries(responses)) {
237
- if (code.startsWith('2') && resp.content) {
247
+ if (!code.startsWith('2'))
248
+ continue;
249
+ if (resp.content) {
238
250
  const jsonContent = resp.content['application/json'];
239
251
  if (jsonContent?.schema)
240
252
  return jsonContent.schema;
241
253
  }
254
+ if (resp.schema)
255
+ return resp.schema;
242
256
  }
243
257
  return undefined;
244
258
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/openapi-parser",
3
- "version": "0.12.4",
3
+ "version": "0.12.6",
4
4
  "description": "OpenAPI spec parser and code generator for Pikku",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "MIT",
package/src/codegen.ts CHANGED
@@ -211,7 +211,10 @@ function generateFunctionFile(
211
211
  // Determine error imports needed
212
212
  const errorClasses = getErrorClassesForResponses(parsed.errorResponses)
213
213
 
214
- lines.push("import { z } from 'zod'")
214
+ const needsZod = hasInput || !!parsed.responseSchema
215
+ if (needsZod) {
216
+ lines.push("import { z } from 'zod'")
217
+ }
215
218
  lines.push("import { pikkuSessionlessFunc } from '#pikku'")
216
219
 
217
220
  if (errorClasses.length > 0) {
@@ -246,8 +249,6 @@ function generateFunctionFile(
246
249
  if (hasInput) funcConfig.push(` input: ${inputName},`)
247
250
  if (parsed.responseSchema) {
248
251
  funcConfig.push(` output: ${outputName},`)
249
- } else {
250
- funcConfig.push(' output: z.void(),')
251
252
  }
252
253
 
253
254
  if (errorClasses.length > 0) {
@@ -303,10 +304,7 @@ function buildInputSchema(
303
304
  if (propSchema.readOnly) continue
304
305
  const isOptional = !requiredSet.has(key)
305
306
  const zodCode = schemaToZod(propSchema, ctx, { optional: isOptional })
306
- const withDesc = propSchema.description
307
- ? `${zodCode}.describe(${JSON.stringify(propSchema.description)})`
308
- : zodCode
309
- props.push(` ${safeKey(key)}: ${withDesc},`)
307
+ props.push(` ${safeKey(key)}: ${zodCode},`)
310
308
  }
311
309
  } else {
312
310
  const bodyZod = schemaToZod(parsed.requestBody, ctx)
@@ -1,6 +1,9 @@
1
1
  import { strict as assert } from 'assert'
2
2
  import { describe, test } from 'node:test'
3
- import { computeContractHash } from './parse-openapi.js'
3
+ import { writeFile, rm, mkdtemp } from 'fs/promises'
4
+ import { join } from 'path'
5
+ import { tmpdir } from 'os'
6
+ import { computeContractHash, parseOpenAPISpec } from './parse-openapi.js'
4
7
  import type { ParsedSpec } from './parse-openapi.js'
5
8
 
6
9
  function makeSpec(overrides: Partial<ParsedSpec> = {}): ParsedSpec {
@@ -148,3 +151,151 @@ describe('computeContractHash', () => {
148
151
  )
149
152
  })
150
153
  })
154
+
155
+ describe('parseOpenAPISpec — Swagger 2.0', () => {
156
+ const swagger2Spec = {
157
+ swagger: '2.0',
158
+ info: { title: 'Test API', version: '1.0.0' },
159
+ host: 'api.example.com',
160
+ basePath: '/v1',
161
+ schemes: ['https'],
162
+ definitions: {
163
+ Pet: {
164
+ type: 'object',
165
+ required: ['name'],
166
+ properties: {
167
+ id: { type: 'integer', format: 'int64' },
168
+ name: { type: 'string' },
169
+ status: { type: 'string', enum: ['available', 'sold'], description: 'pet status' },
170
+ },
171
+ },
172
+ },
173
+ securityDefinitions: {
174
+ api_key: { type: 'apiKey', name: 'api_key', in: 'header' },
175
+ },
176
+ paths: {
177
+ '/pets': {
178
+ get: {
179
+ operationId: 'listPets',
180
+ summary: 'List all pets',
181
+ parameters: [
182
+ { in: 'query', name: 'status', type: 'string', description: 'Filter by status' },
183
+ ],
184
+ responses: {
185
+ '200': {
186
+ description: 'A list of pets',
187
+ schema: { type: 'array', items: { $ref: '#/definitions/Pet' } },
188
+ },
189
+ },
190
+ },
191
+ post: {
192
+ operationId: 'createPet',
193
+ summary: 'Create a pet',
194
+ parameters: [
195
+ {
196
+ in: 'body',
197
+ name: 'body',
198
+ description: 'Pet to create',
199
+ required: true,
200
+ schema: { $ref: '#/definitions/Pet' },
201
+ },
202
+ ],
203
+ responses: {
204
+ '201': {
205
+ description: 'Created',
206
+ schema: { $ref: '#/definitions/Pet' },
207
+ },
208
+ '400': { description: 'Bad request' },
209
+ },
210
+ },
211
+ },
212
+ '/pets/{petId}': {
213
+ get: {
214
+ operationId: 'getPet',
215
+ parameters: [
216
+ { in: 'path', name: 'petId', type: 'integer', required: true },
217
+ ],
218
+ responses: {
219
+ '200': {
220
+ description: 'A pet',
221
+ schema: { $ref: '#/definitions/Pet' },
222
+ },
223
+ },
224
+ },
225
+ },
226
+ },
227
+ }
228
+
229
+ let tmpDir: string
230
+ let specPath: string
231
+
232
+ test('setup', async () => {
233
+ tmpDir = await mkdtemp(join(tmpdir(), 'openapi-test-'))
234
+ specPath = join(tmpDir, 'spec.json')
235
+ await writeFile(specPath, JSON.stringify(swagger2Spec))
236
+ })
237
+
238
+ test('extracts component schemas from definitions', async () => {
239
+ const spec = await parseOpenAPISpec(specPath)
240
+ assert.ok(spec.componentSchemas.Pet)
241
+ assert.equal(spec.componentSchemas.Pet.type, 'object')
242
+ assert.ok(spec.componentSchemas.Pet.properties?.name)
243
+ })
244
+
245
+ test('extracts server URLs from host/basePath/schemes', async () => {
246
+ const spec = await parseOpenAPISpec(specPath)
247
+ assert.deepEqual(spec.serverUrls, ['https://api.example.com/v1'])
248
+ })
249
+
250
+ test('detects apiKey auth from securityDefinitions', async () => {
251
+ const spec = await parseOpenAPISpec(specPath)
252
+ assert.equal(spec.authType, 'apiKey')
253
+ })
254
+
255
+ test('extracts requestBody from body parameter', async () => {
256
+ const spec = await parseOpenAPISpec(specPath)
257
+ const createPet = spec.operations.find(op => op.operationId === 'createPet')
258
+ assert.ok(createPet?.requestBody)
259
+ assert.equal(createPet.requestBody.type, 'object')
260
+ assert.ok(createPet.requestBody.properties?.name)
261
+ assert.equal(createPet.requestBodyRequired, true)
262
+ assert.equal(createPet.requestBodyDescription, 'Pet to create')
263
+ })
264
+
265
+ test('extracts responseSchema from Swagger 2.0 response', async () => {
266
+ const spec = await parseOpenAPISpec(specPath)
267
+ const createPet = spec.operations.find(op => op.operationId === 'createPet')
268
+ assert.ok(createPet?.responseSchema)
269
+ assert.equal(createPet.responseSchema.type, 'object')
270
+
271
+ const listPets = spec.operations.find(op => op.operationId === 'listPets')
272
+ assert.ok(listPets?.responseSchema)
273
+ assert.equal(listPets.responseSchema.type, 'array')
274
+ })
275
+
276
+ test('extracts query params', async () => {
277
+ const spec = await parseOpenAPISpec(specPath)
278
+ const listPets = spec.operations.find(op => op.operationId === 'listPets')
279
+ assert.equal(listPets?.queryParams.length, 1)
280
+ assert.equal(listPets?.queryParams[0].name, 'status')
281
+ })
282
+
283
+ test('extracts path params', async () => {
284
+ const spec = await parseOpenAPISpec(specPath)
285
+ const getPet = spec.operations.find(op => op.operationId === 'getPet')
286
+ assert.equal(getPet?.pathParams.length, 1)
287
+ assert.equal(getPet?.pathParams[0].name, 'petId')
288
+ assert.equal(getPet?.pathParams[0].required, true)
289
+ })
290
+
291
+ test('extracts error responses', async () => {
292
+ const spec = await parseOpenAPISpec(specPath)
293
+ const createPet = spec.operations.find(op => op.operationId === 'createPet')
294
+ assert.equal(createPet?.errorResponses.length, 1)
295
+ assert.equal(createPet?.errorResponses[0].statusCode, 400)
296
+ })
297
+
298
+ test('cleanup', async () => {
299
+ await rm(tmpDir, { recursive: true, force: true })
300
+ })
301
+ })
@@ -106,12 +106,11 @@ export async function parseOpenAPISpec(filePath: string): Promise<ParsedSpec> {
106
106
  const securitySchemes = extractSecuritySchemes(doc)
107
107
  const tagDescriptions = extractTagDescriptions(doc)
108
108
 
109
- // Extract component schemas
109
+ // Extract component schemas (OpenAPI 3.x: components.schemas, Swagger 2.x: definitions)
110
110
  const componentSchemas: Record<string, OpenAPISchema> = {}
111
- if (doc.components?.schemas) {
112
- for (const [name, schema] of Object.entries(doc.components.schemas)) {
113
- componentSchemas[name] = schema as OpenAPISchema
114
- }
111
+ const rawSchemas = doc.components?.schemas ?? doc.definitions ?? {}
112
+ for (const [name, schema] of Object.entries(rawSchemas)) {
113
+ componentSchemas[name] = schema as OpenAPISchema
115
114
  }
116
115
 
117
116
  // Extract operations (skip deprecated)
@@ -132,7 +131,9 @@ export async function parseOpenAPISpec(filePath: string): Promise<ParsedSpec> {
132
131
 
133
132
  const allParams: any[] = [...sharedParams, ...(op.parameters ?? [])]
134
133
 
135
- const body = op.requestBody
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')
136
137
 
137
138
  operations.push({
138
139
  operationId: op.operationId,
@@ -145,8 +146,8 @@ export async function parseOpenAPISpec(filePath: string): Promise<ParsedSpec> {
145
146
  queryParams: extractParams(allParams, 'query'),
146
147
  headerParams: extractParams(allParams, 'header'),
147
148
  requestBody: extractRequestBody(op),
148
- requestBodyDescription: body?.description,
149
- requestBodyRequired: body?.required,
149
+ requestBodyDescription: body3?.description ?? body2?.description,
150
+ requestBodyRequired: body3?.required ?? body2?.required,
150
151
  responseSchema: extractResponseSchema(op),
151
152
  responseDescription: extractResponseDescription(op),
152
153
  errorResponses: extractErrorResponses(op),
@@ -266,20 +267,25 @@ function extractParams(
266
267
  }
267
268
 
268
269
  function extractRequestBody(op: any): OpenAPISchema | undefined {
270
+ // OpenAPI 3.x: op.requestBody.content['application/json'].schema
269
271
  const body = op.requestBody
270
- if (!body) return undefined
271
-
272
- const content = body.content
273
- if (!content) return undefined
272
+ if (body) {
273
+ const content = body.content
274
+ if (content) {
275
+ const jsonContent = content['application/json']
276
+ if (jsonContent?.schema) return jsonContent.schema as OpenAPISchema
274
277
 
275
- // Prefer JSON
276
- const jsonContent = content['application/json']
277
- if (jsonContent?.schema) return jsonContent.schema as OpenAPISchema
278
+ const firstKey = Object.keys(content)[0]
279
+ if (firstKey && content[firstKey]?.schema) {
280
+ return content[firstKey].schema as OpenAPISchema
281
+ }
282
+ }
283
+ }
278
284
 
279
- // Fallback to first content type
280
- const firstKey = Object.keys(content)[0]
281
- if (firstKey && content[firstKey]?.schema) {
282
- return content[firstKey].schema as OpenAPISchema
285
+ // Swagger 2.x: parameters[].in === 'body' → schema
286
+ if (op.parameters) {
287
+ const bodyParam = op.parameters.find((p: any) => p.in === 'body')
288
+ if (bodyParam?.schema) return bodyParam.schema as OpenAPISchema
283
289
  }
284
290
 
285
291
  return undefined
@@ -304,24 +310,30 @@ function extractResponseSchema(op: any): OpenAPISchema | undefined {
304
310
  const resp = responses[code]
305
311
  if (!resp) continue
306
312
 
313
+ // OpenAPI 3.x: resp.content['application/json'].schema
307
314
  const content = resp.content
308
- if (!content) continue
309
-
310
- const jsonContent = content['application/json']
311
- if (jsonContent?.schema) return jsonContent.schema as OpenAPISchema
315
+ if (content) {
316
+ const jsonContent = content['application/json']
317
+ if (jsonContent?.schema) return jsonContent.schema as OpenAPISchema
312
318
 
313
- const firstKey = Object.keys(content)[0]
314
- if (firstKey && content[firstKey]?.schema) {
315
- return content[firstKey].schema as OpenAPISchema
319
+ const firstKey = Object.keys(content)[0]
320
+ if (firstKey && content[firstKey]?.schema) {
321
+ return content[firstKey].schema as OpenAPISchema
322
+ }
316
323
  }
324
+
325
+ // Swagger 2.x: resp.schema directly
326
+ if (resp.schema) return resp.schema as OpenAPISchema
317
327
  }
318
328
 
319
329
  // Fallback: any 2xx
320
330
  for (const [code, resp] of Object.entries(responses) as [string, any][]) {
321
- if (code.startsWith('2') && resp.content) {
331
+ if (!code.startsWith('2')) continue
332
+ if (resp.content) {
322
333
  const jsonContent = resp.content['application/json']
323
334
  if (jsonContent?.schema) return jsonContent.schema as OpenAPISchema
324
335
  }
336
+ if (resp.schema) return resp.schema as OpenAPISchema
325
337
  }
326
338
 
327
339
  return undefined