@pikku/openapi-parser 0.12.10 → 0.12.12
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 +14 -1
- package/dist/codegen.js +30 -1
- package/package.json +2 -2
- package/src/codegen.test.ts +65 -0
- package/src/codegen.ts +30 -2
- package/src/parse-openapi.test.ts +23 -8
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
# @pikku/openapi-parser
|
|
2
2
|
|
|
3
|
+
## 0.12.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d97f2a1: Always emit a description for generated addon functions (and their MCP tools). When an OpenAPI operation omits both `description` and `summary` (common), the generator now synthesizes one — a humanized `operationId` (with the `Controller` segment stripped), else `METHOD /path` — instead of emitting none. This removes the "MCP tool is missing a description" warnings and makes `--mcp`-exposed tools usable.
|
|
8
|
+
|
|
9
|
+
## 0.12.11
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 41ce2cb: Upgrade to TypeScript 6 and raise the minimum Node.js version to 22.
|
|
14
|
+
|
|
15
|
+
All packages now build against `typescript@^6.0.3` and declare `engines.node >= 22`. Internal tooling (`ts-json-schema-generator`, `zod-to-ts`) was bumped to TypeScript 6-compatible releases.
|
|
16
|
+
|
|
3
17
|
## 0.12.10
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
|
6
20
|
|
|
7
21
|
- 02fca80: Fix OpenAPI-to-addon codegen robustness across 2521 real-world specs (77% → 97.8% pass rate).
|
|
8
|
-
|
|
9
22
|
- Cycle detection and depth limits in schema generation to prevent stack overflows
|
|
10
23
|
- Schema partitioning: shared types file for multi-referenced schemas, inline for single-use (fixes crash on large specs)
|
|
11
24
|
- Topological sort with z.lazy() for circular schema references
|
package/dist/codegen.js
CHANGED
|
@@ -27,12 +27,41 @@ const STATUS_TO_ERROR = {
|
|
|
27
27
|
function capitalize(str) {
|
|
28
28
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Turn an operationId like `contactsControllerGetContacts` (or snake/kebab
|
|
32
|
+
* variants) into a readable phrase — split camel/snake/kebab into words, drop
|
|
33
|
+
* noise segments ("controller"), and capitalize. Returns undefined if nothing
|
|
34
|
+
* usable remains.
|
|
35
|
+
*/
|
|
36
|
+
function humanizeOperationId(operationId) {
|
|
37
|
+
if (!operationId)
|
|
38
|
+
return undefined;
|
|
39
|
+
const words = operationId
|
|
40
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
41
|
+
.replace(/[_-]+/g, ' ')
|
|
42
|
+
.split(/\s+/)
|
|
43
|
+
.map((w) => w.trim())
|
|
44
|
+
.filter((w) => w && w.toLowerCase() !== 'controller');
|
|
45
|
+
if (words.length === 0)
|
|
46
|
+
return undefined;
|
|
47
|
+
return capitalize(words.join(' ').toLowerCase());
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A description for the generated function (and its MCP tool). Prefers the
|
|
51
|
+
* spec's `description`, then `summary`; when a spec omits both (common), a
|
|
52
|
+
* synthesized description is still emitted so no function — and no MCP tool —
|
|
53
|
+
* ships description-less: a humanized operationId, else "METHOD /path".
|
|
54
|
+
*/
|
|
30
55
|
function humanDescription(parsed) {
|
|
31
56
|
const description = parsed.description?.trim();
|
|
32
57
|
if (description && !GENERIC_SUMMARIES.has(description.toLowerCase())) {
|
|
33
58
|
return capitalize(description);
|
|
34
59
|
}
|
|
35
|
-
|
|
60
|
+
const summary = parsed.summary?.trim();
|
|
61
|
+
if (summary && !GENERIC_SUMMARIES.has(summary.toLowerCase())) {
|
|
62
|
+
return capitalize(summary);
|
|
63
|
+
}
|
|
64
|
+
return humanizeOperationId(parsed.operationId) ?? `${parsed.method.toUpperCase()} ${parsed.path}`;
|
|
36
65
|
}
|
|
37
66
|
function getErrorClassesForResponses(errorResponses) {
|
|
38
67
|
const classes = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/openapi-parser",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.12",
|
|
4
4
|
"description": "OpenAPI spec parser and code generator for Pikku",
|
|
5
5
|
"author": "yasser.fadl@gmail.com",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,6 +21,6 @@
|
|
|
21
21
|
"yaml": "^2.7.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"typescript": "^
|
|
24
|
+
"typescript": "^6.0.3"
|
|
25
25
|
}
|
|
26
26
|
}
|
package/src/codegen.test.ts
CHANGED
|
@@ -780,3 +780,68 @@ describe('camelCase flag', () => {
|
|
|
780
780
|
)
|
|
781
781
|
})
|
|
782
782
|
})
|
|
783
|
+
|
|
784
|
+
// ---------------------------------------------------------------------------
|
|
785
|
+
// Function/MCP-tool description synthesis (specs that omit descriptions)
|
|
786
|
+
// ---------------------------------------------------------------------------
|
|
787
|
+
describe('function description synthesis', () => {
|
|
788
|
+
test('synthesizes a description from operationId when the spec omits one', () => {
|
|
789
|
+
const spec = makeSpec({
|
|
790
|
+
operations: [
|
|
791
|
+
makeOp({
|
|
792
|
+
operationId: 'contactsControllerGetContacts',
|
|
793
|
+
method: 'get',
|
|
794
|
+
path: '/contacts',
|
|
795
|
+
}),
|
|
796
|
+
],
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
const files = generateAddonFromOpenAPI(spec, makeVars(), {
|
|
800
|
+
oauth: false,
|
|
801
|
+
secret: false,
|
|
802
|
+
mcp: true,
|
|
803
|
+
})
|
|
804
|
+
const funcFile = files['src/functions/contactsControllerGetContacts.function.ts']
|
|
805
|
+
assert.ok(funcFile, 'function file should exist')
|
|
806
|
+
assert.ok(
|
|
807
|
+
funcFile.includes('description: "Contacts get contacts"'),
|
|
808
|
+
'humanized operationId (Controller stripped) should be emitted as the description'
|
|
809
|
+
)
|
|
810
|
+
assert.ok(funcFile.includes('mcp: true'), 'mcp tool flag should be present')
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
test('falls back to "METHOD /path" when operationId is absent', () => {
|
|
814
|
+
const spec = makeSpec({
|
|
815
|
+
operations: [makeOp({ operationId: undefined, method: 'get', path: '/health' })],
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
const files = generateAddonFromOpenAPI(spec, makeVars(), {
|
|
819
|
+
oauth: false,
|
|
820
|
+
secret: false,
|
|
821
|
+
})
|
|
822
|
+
const key = Object.keys(files).find((k) => k.startsWith('src/functions/'))
|
|
823
|
+
assert.ok(key, 'a function file should be generated')
|
|
824
|
+
assert.ok(
|
|
825
|
+
files[key].includes('description: "GET /health"'),
|
|
826
|
+
'should fall back to METHOD path when nothing else is available'
|
|
827
|
+
)
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
test('prefers the spec description over synthesis', () => {
|
|
831
|
+
const spec = makeSpec({
|
|
832
|
+
operations: [
|
|
833
|
+
makeOp({ operationId: 'getPet', description: 'Fetch a single pet by id' }),
|
|
834
|
+
],
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
const files = generateAddonFromOpenAPI(spec, makeVars(), {
|
|
838
|
+
oauth: false,
|
|
839
|
+
secret: false,
|
|
840
|
+
})
|
|
841
|
+
const funcFile = files['src/functions/getPet.function.ts']
|
|
842
|
+
assert.ok(
|
|
843
|
+
funcFile.includes('description: "Fetch a single pet by id"'),
|
|
844
|
+
'the real spec description should win over the synthesized one'
|
|
845
|
+
)
|
|
846
|
+
})
|
|
847
|
+
})
|
package/src/codegen.ts
CHANGED
|
@@ -69,12 +69,40 @@ function capitalize(str: string): string {
|
|
|
69
69
|
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Turn an operationId like `contactsControllerGetContacts` (or snake/kebab
|
|
74
|
+
* variants) into a readable phrase — split camel/snake/kebab into words, drop
|
|
75
|
+
* noise segments ("controller"), and capitalize. Returns undefined if nothing
|
|
76
|
+
* usable remains.
|
|
77
|
+
*/
|
|
78
|
+
function humanizeOperationId(operationId?: string): string | undefined {
|
|
79
|
+
if (!operationId) return undefined
|
|
80
|
+
const words = operationId
|
|
81
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
82
|
+
.replace(/[_-]+/g, ' ')
|
|
83
|
+
.split(/\s+/)
|
|
84
|
+
.map((w) => w.trim())
|
|
85
|
+
.filter((w) => w && w.toLowerCase() !== 'controller')
|
|
86
|
+
if (words.length === 0) return undefined
|
|
87
|
+
return capitalize(words.join(' ').toLowerCase())
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A description for the generated function (and its MCP tool). Prefers the
|
|
92
|
+
* spec's `description`, then `summary`; when a spec omits both (common), a
|
|
93
|
+
* synthesized description is still emitted so no function — and no MCP tool —
|
|
94
|
+
* ships description-less: a humanized operationId, else "METHOD /path".
|
|
95
|
+
*/
|
|
96
|
+
function humanDescription(parsed: ParsedOperation): string {
|
|
73
97
|
const description = parsed.description?.trim()
|
|
74
98
|
if (description && !GENERIC_SUMMARIES.has(description.toLowerCase())) {
|
|
75
99
|
return capitalize(description)
|
|
76
100
|
}
|
|
77
|
-
|
|
101
|
+
const summary = parsed.summary?.trim()
|
|
102
|
+
if (summary && !GENERIC_SUMMARIES.has(summary.toLowerCase())) {
|
|
103
|
+
return capitalize(summary)
|
|
104
|
+
}
|
|
105
|
+
return humanizeOperationId(parsed.operationId) ?? `${parsed.method.toUpperCase()} ${parsed.path}`
|
|
78
106
|
}
|
|
79
107
|
|
|
80
108
|
function getErrorClassesForResponses(
|
|
@@ -166,7 +166,11 @@ describe('parseOpenAPISpec — Swagger 2.0', () => {
|
|
|
166
166
|
properties: {
|
|
167
167
|
id: { type: 'integer', format: 'int64' },
|
|
168
168
|
name: { type: 'string' },
|
|
169
|
-
status: {
|
|
169
|
+
status: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
enum: ['available', 'sold'],
|
|
172
|
+
description: 'pet status',
|
|
173
|
+
},
|
|
170
174
|
},
|
|
171
175
|
},
|
|
172
176
|
},
|
|
@@ -179,7 +183,12 @@ describe('parseOpenAPISpec — Swagger 2.0', () => {
|
|
|
179
183
|
operationId: 'listPets',
|
|
180
184
|
summary: 'List all pets',
|
|
181
185
|
parameters: [
|
|
182
|
-
{
|
|
186
|
+
{
|
|
187
|
+
in: 'query',
|
|
188
|
+
name: 'status',
|
|
189
|
+
type: 'string',
|
|
190
|
+
description: 'Filter by status',
|
|
191
|
+
},
|
|
183
192
|
],
|
|
184
193
|
responses: {
|
|
185
194
|
'200': {
|
|
@@ -254,7 +263,9 @@ describe('parseOpenAPISpec — Swagger 2.0', () => {
|
|
|
254
263
|
|
|
255
264
|
test('extracts requestBody from body parameter', async () => {
|
|
256
265
|
const spec = await parseOpenAPISpec(specPath)
|
|
257
|
-
const createPet = spec.operations.find(
|
|
266
|
+
const createPet = spec.operations.find(
|
|
267
|
+
(op) => op.operationId === 'createPet'
|
|
268
|
+
)
|
|
258
269
|
assert.ok(createPet?.requestBody)
|
|
259
270
|
assert.equal(createPet.requestBody.type, 'object')
|
|
260
271
|
assert.ok(createPet.requestBody.properties?.name)
|
|
@@ -264,25 +275,27 @@ describe('parseOpenAPISpec — Swagger 2.0', () => {
|
|
|
264
275
|
|
|
265
276
|
test('extracts responseSchema from Swagger 2.0 response', async () => {
|
|
266
277
|
const spec = await parseOpenAPISpec(specPath)
|
|
267
|
-
const createPet = spec.operations.find(
|
|
278
|
+
const createPet = spec.operations.find(
|
|
279
|
+
(op) => op.operationId === 'createPet'
|
|
280
|
+
)
|
|
268
281
|
assert.ok(createPet?.responseSchema)
|
|
269
282
|
assert.equal(createPet.responseSchema.type, 'object')
|
|
270
283
|
|
|
271
|
-
const listPets = spec.operations.find(op => op.operationId === 'listPets')
|
|
284
|
+
const listPets = spec.operations.find((op) => op.operationId === 'listPets')
|
|
272
285
|
assert.ok(listPets?.responseSchema)
|
|
273
286
|
assert.equal(listPets.responseSchema.type, 'array')
|
|
274
287
|
})
|
|
275
288
|
|
|
276
289
|
test('extracts query params', async () => {
|
|
277
290
|
const spec = await parseOpenAPISpec(specPath)
|
|
278
|
-
const listPets = spec.operations.find(op => op.operationId === 'listPets')
|
|
291
|
+
const listPets = spec.operations.find((op) => op.operationId === 'listPets')
|
|
279
292
|
assert.equal(listPets?.queryParams.length, 1)
|
|
280
293
|
assert.equal(listPets?.queryParams[0].name, 'status')
|
|
281
294
|
})
|
|
282
295
|
|
|
283
296
|
test('extracts path params', async () => {
|
|
284
297
|
const spec = await parseOpenAPISpec(specPath)
|
|
285
|
-
const getPet = spec.operations.find(op => op.operationId === 'getPet')
|
|
298
|
+
const getPet = spec.operations.find((op) => op.operationId === 'getPet')
|
|
286
299
|
assert.equal(getPet?.pathParams.length, 1)
|
|
287
300
|
assert.equal(getPet?.pathParams[0].name, 'petId')
|
|
288
301
|
assert.equal(getPet?.pathParams[0].required, true)
|
|
@@ -290,7 +303,9 @@ describe('parseOpenAPISpec — Swagger 2.0', () => {
|
|
|
290
303
|
|
|
291
304
|
test('extracts error responses', async () => {
|
|
292
305
|
const spec = await parseOpenAPISpec(specPath)
|
|
293
|
-
const createPet = spec.operations.find(
|
|
306
|
+
const createPet = spec.operations.find(
|
|
307
|
+
(op) => op.operationId === 'createPet'
|
|
308
|
+
)
|
|
294
309
|
assert.equal(createPet?.errorResponses.length, 1)
|
|
295
310
|
assert.equal(createPet?.errorResponses[0].statusCode, 400)
|
|
296
311
|
})
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/codegen.ts","./src/index.ts","./src/naming.ts","./src/openapi-to-zod-schema.ts","./src/parse-openapi.ts","./src/zod-codegen.ts"],"version":"
|
|
1
|
+
{"root":["./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"}
|