fix(app): openapi importer - resolve $ref'd path items and form bodies, honor consumes - #208
Conversation
…s, honor consumes
Seven gaps in the OpenAPI 3 / Swagger 2 importers, the two silent-loss ones
first:
- A path item written as `{"$ref": ...}` has no method keys, so every operation
under it was skipped with no request built, no `requestCount`, and nothing in
`meta.skipped`. Both parsers now resolve it through `resolvePathItem`.
- An urlencoded/multipart body schema behind `$ref` or `allOf` has no literal
`properties`, so the form branch produced the right body mode with an empty
field list. Field names now come from `schemaFieldNames`, which samples the
schema - the same `$ref`/`allOf` resolution JSON bodies already got.
- Swagger 2 `formData` was unconditionally multipart; it now picks
`x-www-form-urlencoded` vs `form-data` from the operation's (else the spec's)
`consumes`, matching what the endpoint actually accepts.
- The detector claims 3.1, so the sampler now reads the 3.1 keywords it was
ignoring: type arrays (`["string", "null"]` samples the first non-null
member), `const` (outranking `example`), and the plural `examples`.
- A `trace` operation cannot be represented (`HttpMethod` has no `"TRACE"`) and
is now counted as an `unsupported_method` SkippedItem instead of vanishing
from `requestCount`.
- A non-array `parameters` (the missing-`-` YAML mistake) threw `is not
iterable` and aborted the whole file. It is stepped over, counted as
`malformed_spec`, and every other path still imports.
`openapi-shared.ts` holds the three helpers both parsers needed - they are
structural clones, which is how both grew the same unguarded spread and the
same hardcoded `skipped: []`.
Closes #193
|
Fix the merge conflict |
Resolves the SkippedItem.kind conflict with #207: both sides widened the union (malformed_item there, unsupported_method + malformed_spec here), so the resolution keeps all three and the doc comment now separates the kinds that mean "Vayu cannot represent this" from the three that do not. The README's SkippedItem line is updated to match - master documented malformed_item in postman.md rather than there, so listing only this branch's two new kinds would have left the union under-documented.
|
Merged One conflict, in I also updated the Everything else auto-merged. Worth noting the two semantic overlaps that a clean auto-merge does not prove correct, both checked: #207's Re-verified on the merged tree ( Generated by Claude Code |
Description
Part of #192. All seven findings in #193 were re-verified against current master (
9fc4b9d) before editing - the line anchors had shifted slightly, but every defect is still present exactly as described.Plan (root cause, approach, rejected alternative). The root cause of the two silent-loss items is the same one twice: the parsers resolve
$refat some levels and not others, and the unresolved level then reads as "nothing here". A path item is only checked for method keys, so a{"$ref": ...}item contributes none and its whole path disappears; the form-body branch readscontent[ct].schema?.propertiesdirectly, so a schema behind$ref/allOfyields zero fields while the JSON branch two lines up resolves the same shape fine. So the approach in both cases is to route the read through the resolution the parser already owns:resolvePathItembefore the method loop, andschemaFieldNames- which samples the schema and takes the stub's own keys - for the form branch. For the form fields I rejected writing a second properties-walker that mergesallOf: it would resolve further than the JSON body next to it, so the same schema would import differently depending on its media type. Going throughsampleSchemagives form and JSON bodies provably the same reach (first branch only), which is the parity the issue asks for.The helpers live in a new
openapi-shared.ts, imported by both parsers, rather than as a copy in each. The two parsers are structural clones - which is precisely how both grew the same unguardedparametersspread and the same hardcodedskipped: []- and three of these fixes had to land in both. For item 4 I took route (a), support the keywords, over (b) narrowing the detector:detect()matching3.1.xis right (a 3.1 spec imports fine), the gap was only in the sampler, and telling users "3.1 detected but not really sampled" is a worse contract than reading three keywords.Findings, and what changed
$refnever resolved - a whole path silently vanishesresolvePathItem(openapi-shared.ts) resolves a{"$ref": ...}path item before theHTTP_METHODSloop, in both parsers. One hop only, matching the parameter /requestBodyrefs (a ref-to-a-ref would need a cycle guard and is not a shape generators emit). A path item that is not an object, or whose$refdoes not resolve to one, is now counted rather than skipped in silence.$reforallOfyields zero form fieldsschemaFieldNames(schema, resolveRef)(schema-sampler.ts), which samples the schema and returnsObject.keys()of the stub.$refandallOfnow resolve for form bodies exactly as far as they already did for JSON bodies - first branch only, deliberately, so the two cannot disagree. A schema that samples to a non-object contributes no names.formDataalways multipart, ignoringconsumesbuildSwaggerOpderivesformModefrom the operation's (else the spec's)consumes: urlencoded when it namesapplication/x-www-form-urlencodedand not multipart;form-datawhen it names multipart, names both (only multipart can carry atype: filefield), or names neither - so today's default is preserved for a spec with noconsumes. Compared on the media type alone, so; charset=utf-8still matches.schema-sampler.tshandlesArray.isArray(schema.type)(samples the first non-"null"member; an only-"null"type and the scalartype: "null"sample asnull) and readsconst.constis placed aboveexample- JSON Schema makes it the only permitted value, whereexampleis an annotation.traceoperation is counted as a newunsupported_methodSkippedItemand stays out ofrequestCount.UNSUPPORTED_METHODSis the list andtraceis its only member (a path item defines exactly eight methods).HttpMethodis not touched, per the issue.parameterscrashes the whole parseSkipTally.params, which returns[]for a non-array and counts amalformed_spec. Absent stays uncounted - only a present-but-wrong shape is a defect. Every other path in the file still imports.Beyond the issue's literal list (both noted in the docs): the sampler also reads 3.1's plural
examples(first entry, when there is no singularexample). The issue's Problem section names it as never read and route (a) is "support the keywords"; leaving it out would have meant a 3.1 spec's request-body example still being ignored, and the doc caveating it. Second, item 6's "ideally, record it" is implemented rather than skipped - silently treating malformed input as empty is the same silent-loss shape as the findings being fixed here.meta.skippedgains its first producer in these parsers, so the two new kinds have a reader before they have a writer:ImportModal.tsx:506-514already renders${count} ${kind}for everymeta.skippedentry, which is where1 unsupported_method/2 malformed_specsurface in the preview. No new UI was needed and none was added.Blast radius traced.
SkippedItem.kindis read in exactly two places -ImportModal's preview line andinsomnia-v4.ts's cast - and neither switches exhaustively on the union, so widening it changes no existing behaviour (tscconfirms).sampleSchemahas three call sites (both OpenAPI parsers' body paths); the addedconst/examples/type-array handling only fires on schemas that previously fell through to{}or ignored a pinned value, so existing fixtures are unaffected - the two parser suites andorchestrator.fixture-parity.test.tspass unchanged.schemaFieldNamesis v3-only (Swagger 2 form fields come fromformDataparameters, not a schema). No engine file, noPOST /import/applypayload shape, and no MCP surface is touched -rg -n "openapi-v3|openapi-v2|schema-sampler" app/electron engineis still empty, matching the issue's "App changes: none".Type of Change
Testing
All run on this branch at
eeee26d, on Linux.cd app && pnpm test- 1947 passed, 230 files (1926 on the base commit; 21 tests added).cd app && pnpm type-check- clean.mkdocs build --strict -f .github/mkdocs.yml- clean (the new#consumes--form-encodingand cross-doc anchors resolve).npx eslinton the eight touched app files - 29 problems, down from 31 on the base commit; every one is a pre-existingno-explicit-anyin these parsers belonging to app: burn down the ESLint debt (100 errors) and enforce lint in the PR CI matrix #189's lint burn-down (two went away because thepathParams: any[] = []defaults did). No new diagnostics.npx prettier --checkon the eight touched app files - clean (all seven pre-existing ones were prettier-clean before, so formatting them was in scope). The threedocs/app/import-collections/*.mdfiles are not prettier-clean and were not clean on the base commit either, so they were deliberately left unformatted per CLAUDE.md.ctestwere not run: no C++ changed (the issue's own acceptance criteria include "no change to any engine file"), so no engine test could go from green to red.New coverage:
openapi-v3.test.ts(acomponents.pathItems$refpath item incl. its shared parameters, an unresolvable one, a$ref'd urlencoded body, anallOfmultipart body, TRACE, a non-arrayparameterson both a path item and an operation, and a clean-fixture assertion thatskippedstays[]),openapi-v2.test.ts(the sixconsumes→ form-encoding cases, spec-levelconsumesfallback, field rows preserved, path-item$ref, theparametersguard), andschema-sampler.test.ts(type arrays in three shapes, only-null,constincl. beatingexampleandconst: null,examples[0], plus aschemaFieldNamesblock).Every fix was mutation-checked by script - reverted in place, its covering tests run, then restored from a copy. All fourteen reverts fail, and only the intended tests fail:
resolvePathItemin v3 → raw path item$ref'd item, unresolvable item)schemaFieldNames→schema?.properties$refform body,allOfform body)UNSUPPORTED_METHODStallytally.params(op.parameters)in v3tally.params(pathItem.parameters)in v3mode: formMode→mode: "form-data"consumes, field rows, spec-level fallback)resolvePathItemin v2tally.paramsin v2constbranchexamples[0]branchcase "null"schemaFieldNames' non-object guardSkipTally.params' malformed countThe two
skippedassertions on the committed fixtures are what stop the tally from becoming noise: a producer that fires on a healthy spec would fail them, not merely add a warning nobody notices.Checklist
Related Issues
Closes #193
Docs updated in the same commit:
docs/app/import-collections/openapi-v3.md(path-item$refresolution and themalformed_speccount in Tree structure, a newtraceparagraph, the form-body table row plus a paragraph on why field names go through the sampler, the rewrittenconst/example/examplesprecedence and type-array bullets, thenullrow, the parameter-guard paragraph, and ameta.skippedkind table replacing "this parser never emitsSkippedItems"),docs/app/import-collections/openapi-v2.md(a newconsumes→ form encoding section with the four-case table, theformDatamapping-table row, path-item$refand parameter-guard paragraphs, the sampler bullets, the corrected "Dropped / not represented" andmetalines, and two rows in the v2-vs-v3 contrast table), anddocs/app/import-collections/README.md(the widenedSkippedItemunion with what each new kind means, and thesampleSchema/schemaFieldNamesbullets).Generated by Claude Code