@pikku/openapi-parser 0.12.5 → 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,11 @@
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
+
3
9
  ## 0.12.5
4
10
 
5
11
  ### 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'`);
@@ -183,10 +186,7 @@ function buildInputSchema(parsed, ctx) {
183
186
  continue;
184
187
  const isOptional = !requiredSet.has(key);
185
188
  const zodCode = schemaToZod(propSchema, ctx, { optional: isOptional });
186
- const withDesc = propSchema.description
187
- ? `${zodCode}.describe(${JSON.stringify(propSchema.description)})`
188
- : zodCode;
189
- props.push(` ${safeKey(key)}: ${withDesc},`);
189
+ props.push(` ${safeKey(key)}: ${zodCode},`);
190
190
  }
191
191
  }
192
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.5",
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) {
@@ -301,10 +304,7 @@ function buildInputSchema(
301
304
  if (propSchema.readOnly) continue
302
305
  const isOptional = !requiredSet.has(key)
303
306
  const zodCode = schemaToZod(propSchema, ctx, { optional: isOptional })
304
- const withDesc = propSchema.description
305
- ? `${zodCode}.describe(${JSON.stringify(propSchema.description)})`
306
- : zodCode
307
- props.push(` ${safeKey(key)}: ${withDesc},`)
307
+ props.push(` ${safeKey(key)}: ${zodCode},`)
308
308
  }
309
309
  } else {
310
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