Skip to content

Declare the path parameters the exporter templates - #40

Open
VSN2015 wants to merge 1 commit into
fix/openapi-glob-routesfrom
fix/openapi-accuracy
Open

Declare the path parameters the exporter templates#40
VSN2015 wants to merge 1 commit into
fix/openapi-glob-routesfrom
fix/openapi-accuracy

Conversation

@VSN2015

@VSN2015 VSN2015 commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Stacked on #29 — both edit rails_routes, so this builds on it rather than conflicting. Merge #29 first and the base retargets to master.

Four accuracy bugs in the exporter, from the same audit as #39. The headline claim is that an exported document cannot drift from what the server enforces; these are four ways it did.

1. A templated path variable was never declared as a parameter

OpenAPI 3.1 requires every {variable} in a path template to have a matching path parameter. The exporter templated /users/:id/users/{id} and then emitted no parameters at all, so every document containing a member route was invalid — including the committed golden fixture, which has a /users/{id} PATCH route and has been wrong since it was generated.

"/users/{id}": { "patch": {
  "operationId": "users_update",
  "parameters": [
    { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
  ],
  "requestBody": { ... }
} }

Typed string and nothing cleverer: a route set doesn't say what an :id is, and this exporter documents what arrives rather than inventing a type. Derived from the templated path rather than from Journey internals, so it also works for hosts passing their own routes: descriptors — including the {path} wildcards #29 adds.

Guarded as an invariant over the whole document rather than one assertion per operation, so an operation added later cannot quietly reintroduce it:

fixture["paths"].each do |path, operations|
  variables = path.scan(/\{(\w+)\}/).flatten
  operations.each_value do |operation|
    declared = operation.fetch("parameters", []).select { |p| p["in"] == "path" }
    expect(declared.map { |p| p["name"] }).to match_array(variables)
    expect(declared).to all(include("required" => true))
  end
end

2. A route answering several verbs was documented for one

rails_routes kept verb.split("|").first, so the PATCH|PUT pair resources generates — and any match via: [:patch, :put] — exported the PATCH operation and silently dropped PUT. Every verb a route answers now gets its own descriptor.

3. ERROR_SCHEMA had drifted from what the server renders

It is the one hand-written part of the export, and it had fallen behind twice:

  • A violation on a field with message: (or with app I18n copy) carries {param:, code:, message:}. The schema documented only param and code, so a client generating types from it dropped the human-readable copy.
  • The code enumeration never gained depth, which a :json field's max_depth: bound emits.

message is documented as an optional property — required stays %w[param code], since a violation without one keeps the bare shape — and depth is listed. #27 guards the request-body half of the drift claim; the response half was unguarded, so there is now a spec that renders a real violation and holds the schema to the envelope:

body[:error][:details].each do |entry|
  expect(entry.keys.map(&:to_s)).to all(be_in(detail_schema["properties"].keys))
  expect(detail_schema["required"]).to all(be_in(entry.keys.map(&:to_s)))
end

4. Two controllers on one path+verb overwrote each other

A document cannot carry two operations in one slot, and the second claim replaced the first with nothing to show it. The loser now lands in x-permittable-controllers — which is exactly where the exporter already puts an operation it cannot place, so this needed no new vocabulary.


Verification

  • 264 examples, 0 failures; rubocop clean; coverage 96.97%.
  • All six new specs fail on Template wildcard route segments in exported OpenAPI #29's HEAD for the right reasons, including the two golden-document ones.
  • No new dependency. The conformance specs are property assertions over the exported document, so nothing was added to the Gemfile or to the six compatibility gemfiles.
  • The golden fixture was regenerated from the exporter, and its diff is exactly the three intended additions — parameters on the one templated path, depth in the code description, and the message property. Nothing else moved.

Note on the CHANGELOG

This adds to the ## Unreleased section #29 opens. #39 opens one too, so those two will want a trivial merge resolution whichever lands second.

🤖 Generated with Claude Code

Four accuracy bugs in the OpenAPI exporter, found in the same audit as
the wildcard templating below it.

OpenAPI 3.1 requires every {variable} in a path template to have a
matching path parameter, and the exporter emitted none — so every
document containing a member route was invalid, the committed golden
fixture included. Each operation now carries one path parameter per
variable in the path it was placed at, typed string, because a route
set does not say what an :id is and this exporter documents what
arrives rather than guessing. The invariant is asserted over the whole
document, so an operation added later cannot reintroduce it.

`rails_routes` kept only the first of a route's verbs, so the PATCH|PUT
pair `resources` generates exported PATCH and silently dropped PUT.
Every verb a route answers now gets its own descriptor.

ERROR_SCHEMA is the one hand-written part of the export, and it had
drifted twice: a violation on a field with `message:` carries a third
key it never mentioned, and the code enumeration never gained `depth`
from a :json field's max_depth: bound. Both are documented now, with
`message` optional since a violation without one keeps the bare shape,
and a spec renders a real violation and holds the schema to it — the
response half of "docs cannot drift" was the unguarded half.

Two controllers claiming one path and verb overwrote each other with no
indication anything was lost. The loser now lands in
x-permittable-controllers, where the exporter already puts an operation
it cannot place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant