Skip to content

Add Sinatra integration (OpenapiFirst::Sinatra) - #483

Merged
ahx merged 2 commits into
mainfrom
sinatra-extension
Aug 12, 2026
Merged

Add Sinatra integration (OpenapiFirst::Sinatra)#483
ahx merged 2 commits into
mainfrom
sinatra-extension

Conversation

@ahx

@ahx ahx commented Jun 3, 2026

Copy link
Copy Markdown
Owner

A Sinatra extension to define routes by referencing OpenAPI operations, so URLs and HTTP methods live only in the API description:

require 'openapi_first/sinatra'

class PetsApp < Sinatra::Base
  register OpenapiFirst::Sinatra
  openapi 'openapi.yaml'

  operation(:list_pets) do |params|
    json list_pets(params[:filter])
  end
end

The HTTP method and path for each route come from the operationId. Request validation is called automatically for these operations.

Why?

  • Performance: Use only Sinatra's routing instead of routing in the middleware once, then again in Sinatra
  • No redundant path definition in OAD and code
  • This does not add a middleware. It allows us to run request validation for some and not for other routes (good? bad?)

Related to #365

@ahx
ahx force-pushed the sinatra-extension branch 2 times, most recently from 640794f to de8bce8 Compare June 3, 2026 13:23

@jzobel jzobel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nicely done — reads clean and is unusually well-tested and documented for an experimental extension. Inline notes below; two more worth flagging beyond those:

1 (design) — routing is delegated to Sinatra/Mustermann, but validation uses the OAD template. The two matchers can diverge at the edges (trailing slash, dot-in-segment, encoding, greediness): a request Mustermann matches is validated against the template it was defined with, using the params Mustermann extracted — not openapi_first's own path matching. Fine for the covered cases; worth a docs note that the two routers' path semantics should be kept aligned. (Mustermann itself is healthy — 4.0.0, Apr 2026 — so this is about semantic divergence, not dependency risk.)

5 (minor) — build_operation_index is first-wins on duplicate operationIds (index[operation_id] ||= …): a duplicate is silently dropped. Invalid OAD, but a warn/raise would beat silent.

🤖 Generated with Claude Code

Comment thread lib/openapi_first/sinatra.rb Outdated
# The parsed request body
# @return [Sinatra::IndifferentHash]
def parsed_body
::Sinatra::IndifferentHash[openapi_request.parsed_body]

@jzobel jzobel Jul 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parsed_body assumes an object body: Sinatra::IndifferentHash[openapi_request.parsed_body] raises for a top-level JSON array/scalar body (and for a bodyless request, where parsed_body is nil). Consider guarding for non-Hash bodies, or documenting it as object-only and pointing to openapi_request.parsed_body for arrays/scalars.
🤖 Generated with Claude Code

Comment thread lib/openapi_first/request.rb Outdated

def validate(request, route_params:)
parsed_request, error = parse_request(request, route_params:)
def validate(request, path_params:)

@jzobel jzobel Jul 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming flag: route_params:path_params: is clearer (they are path params). If Request#validate / parse_request count as public / manual-use API, this is a breaking keyword rename — worth a CHANGELOG line; ignore if they're internal.
🤖 Generated with Claude Code

Comment thread CHANGELOG.md Outdated
```

The HTTP method and path for each route come from the operationId.
Request validation is called automatically for these operations

@jzobel jzobel Jul 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting nits: add a blank line before ## 3.4.3, give this line a trailing period, and drop the trailing whitespace on the closing ``` fence a few lines up. (README also has trailing whitespace on the blank line inside the create_pet example, ~L442.)
🤖 Generated with Claude Code

@ahx
ahx force-pushed the sinatra-extension branch from de8bce8 to 49f9d9c Compare July 6, 2026 08:36
@ahx
ahx force-pushed the sinatra-extension branch from 49f9d9c to 0007b0c Compare July 27, 2026 10:24
@jzobel

jzobel commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Reviewed as a downstream consumer of the gem — really solid, well-documented, and thoroughly tested. Three points worth raising (all advisory):

Classic (top-level) app auto-registration is untested

The file ends with Sinatra.register(OpenapiFirst::Sinatra), and the README/YARD both ship "in a classic app the extension registers itself, so requiring the file is enough." But every spec uses a modular Class.new(Sinatra::Base) with an explicit register, so the classic path is never exercised functionally. Because line 217 executes on require, SimpleCov reports it as covered — which makes the behavioral gap easy to miss. A shipped, documented feature with no behavioral test.

Falsy openapi_error_response falls through to the block

The handler guards with:

if (failure = validated.error) && (error_response = settings.openapi_error_response)
  halt(*error_response.new(failure:).render)
end

openapi sets that setting from OpenapiFirst.configuration.request_validation_error_response, which is user-settable to nil/false. When it's falsy, a contract-violating request skips halt and runs the block anyway, with a failed validated sitting in env[REQUEST]. This mirrors the middleware's error_response: false "disable" semantics, so it's likely intended — but it's neither tested nor documented in the Sinatra path. Worth a test that pins the intended behavior (block runs vs. request rejected).

No first-class way to override/disable the error response

The request-validation middleware exposes an explicit error_response: option (including false to disable). The Sinatra openapi method takes only spec and offers no equivalent — the only override is the undocumented set :openapi_error_response, MyClass after openapi. So there's currently no first-class way to get, say, JSON:API errors on a Sinatra app. Feature gap rather than a bug (and note the error-response class is sourced from the global config, same as the middleware — nothing consumes a per-definition request_validation_error_response, so setting it in the OpenapiFirst.load(spec) { |c| … } block is silently ignored in both paths).

🤖 Generated with Claude Code

@ahx

ahx commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

@jzobel Is that you? Please mark LLM generated communication as such so people can tell if these opinions are coming from a real engineer. About the feature gap: You should still be able to configure the error response format globally. Do you think we need an additional way to configure that?

@jzobel

jzobel commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Sorry for the sloppyness, I updated "my" comments.

You should still be able to configure the error response format globally.

Absolutely right, Claude and I got that wrong. Sorry for the noise!

ahx and others added 2 commits August 11, 2026 10:03
Resolve the path for an operation by operationId, filling in path
parameters. Extracted from the Sinatra integration, where it backs the
operation_url helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A Sinatra extension to define routes by referencing OpenAPI operations,
so URLs and HTTP methods live only in the description:

    require 'openapi_first/sinatra'

    class PetsApi < Sinatra::Base
    register OpenapiFirst::Sinatra
    openapi 'openapi.yaml'

    operation :index_pets do |params|
    json index_pets(params[:filter])
    end
    end

The HTTP method and path for each route come from the operationId.
Request validation is called automatically for these operations.
@ahx
ahx force-pushed the sinatra-extension branch from 162b3bc to 22e9855 Compare August 11, 2026 17:42
@ahx
ahx marked this pull request as ready for review August 12, 2026 10:25
@ahx
ahx merged commit ab1bdac into main Aug 12, 2026
32 checks passed
@ahx
ahx deleted the sinatra-extension branch August 12, 2026 13:32
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.

2 participants