Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 28 additions & 26 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

## Unreleased

No stricter / less strict request validation. Mostly internal stuff. Plus a Sinatra integration.
## 4.0.0

This release has no stricter or less strict request validation. It changes mostly internal stuff and adds a Sinatra integration. It's a major version, but it should be safe to upgrade.

#### Breaking changes
- Uploaded files are no longer read during request validation. Before, the whole content of every `multipart/form-data` part that was sent as a file was read into memory, which allowed a single large upload to any documented multipart route to exhaust the memory of the server process. Such a field is now passed through as Rack parsed it (`{ filename:, type:, name:, tempfile:, head: }`), which is the same shape that Sinatra and Hanami hand to your application. Use `parsed_body['file'][:tempfile]` to read or stream the file.
Expand All @@ -11,6 +13,7 @@ No stricter / less strict request validation. Mostly internal stuff. Plus a Sina
- Fields that were not sent as a file, and fields with a JSON `contentType` in the `encoding` map, are read and validated as before.
- The `openapi_parameters` gem was merged into openapi_first and is not a dependency anymore. Parameter parsing now lives in openapi_first itself. If you registered a parser for parameters that use a `content` field, use `OpenapiFirst::ParameterContentParsers.register` instead of `OpenapiParameters::ContentParsers.register`.
- Changed: `OpenapiFirst::ResponseHeader` (returned by `Response#headers`, renamed from `OpenapiFirst::Header`) exposes `parameter`, an `OpenapiFirst::Parameter`, instead of `node`.
- Changed: `OpenapiFirst::Request#parameters` returns the parameters that are defined for a request as `OpenapiFirst::Parameter` objects, which expose `name`, `location`, `schema`, `required?`, `deprecated?`, `style`, `explode?` and `media_type`.

#### Removed deprecations
- Removed: `OpenapiFirst::Configuration#request_validation_raise_error` and `#response_validation_raise_error` (both reader and writer), deprecated since 3.0.0. Pass `raise_error:` to middlewares instead.
Expand All @@ -20,42 +23,41 @@ No stricter / less strict request validation. Mostly internal stuff. Plus a Sina
- Removed: `OpenapiFirst::Test::Coverage::TerminalReporter#format`, deprecated since 3.4.0. Use `#report` instead.

#### Added
- API coverage now reports skipped requests and responses.
- Added: OpenAPI 3.2 documents are accepted, but not fully supported yet. They are handled using the OpenAPI 3.1 rules, so features introduced in 3.2 may be ignored. Loading such a document prints a warning. Operations defined under `additionalOperations` are routed. See #469.
- Added: `OpenapiFirst::Request#parameters` returns the parameters that are defined for a request as `OpenapiFirst::Parameter` objects, which expose `name`, `location`, `schema`, `required?`, `deprecated?`, `style`, `explode?` and `media_type`. It used to return an internal object with a different interface.
- Added: Show all covered endpoints in HTML coverage reporter and filter covered/uncovered endpoints
- Added: API coverage now reports skipped requests and responses.
- Added: OpenAPI 3.2 documents are accepted, but not fully supported yet. They are handled using the OpenAPI 3.1 rules, so features introduced in 3.2 may be ignored. Loading such a document prints a warning. Operations defined under `additionalOperations` are routed. See [#469](https://github.com/ahx/openapi_first/issues/469).
- Added: Show all covered endpoints in HTML coverage reporter and filter covered/uncovered endpoints.
- Added: Sinatra integration (OpenapiFirst::Sinatra)
A Sinatra extension to define routes by referencing OpenAPI operations:

```ruby
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

operation :create_pet do
pet = create_pet(parsed_body)
headers['Location'] = operation_url(:show_pet, petId: pet.id)
status :created
json pet
end

operation :show_pet do |params|
json show_pet(params[:petId])
end
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

operation :create_pet do
pet = create_pet(parsed_body)
headers['Location'] = operation_url(:show_pet, petId: pet.id)
status :created
json pet
end

operation :show_pet do |params|
json show_pet(params[:petId])
end
end
```

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

#### Fixed
- Fixed: Validating against a schema from a referenced file raised `ArgumentError` in OpenAPI 3.0 documents when a top-level key of that file collides with a JSON Schema keyword, such as `$ref: 'parameters.yaml#/id'`. The containing file is no longer parsed as a schema itself, so such keys work like any other now. See #348.
- Fixed: Validating against a schema from a referenced file raised `ArgumentError` in OpenAPI 3.0 documents when a top-level key of that file collides with a JSON Schema keyword, such as `$ref: 'parameters.yaml#/id'`. The containing file is no longer parsed as a schema itself, so such keys work like any other now. See [#348](https://github.com/ahx/openapi_first/issues/348).
- Fixed: `$ref`s nested inside the schema of a parameter or a response header are resolved now, so these values are unpacked and converted as described. Before, only a `$ref` at the top level of the schema was resolved. See #450.
- Fixed: The JSON schema of a parameter that uses a `content` field with a `$ref`'d schema is resolved now.
- Fixed: Loading a document no longer raises `NoMethodError` when a parameter has neither `schema` nor `content`.
Expand Down
40 changes: 20 additions & 20 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
openapi_first (3.4.3)
openapi_first (4.0.0)
drb (~> 2.0)
hana (~> 1.3)
json_schemer (>= 2.1, < 3.0)
Expand Down Expand Up @@ -95,20 +95,20 @@ GEM
date (3.5.1)
diff-lcs (1.6.2)
drb (2.2.3)
erb (6.0.6)
erb (6.0.7)
erubi (1.13.1)
globalid (1.4.0)
activesupport (>= 6.1)
hana (1.3.7)
i18n (1.15.2)
concurrent-ruby (~> 1.0)
io-console (0.8.2)
io-console (0.9.2)
irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
json (2.21.2)
json (3.0.2)
json_schemer (2.5.0)
bigdecimal
hana (~> 1.3)
Expand All @@ -132,12 +132,12 @@ GEM
drb (~> 2.0)
prism (~> 1.5)
mustermann (3.1.1)
net-imap (0.6.6)
net-imap (0.6.7)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
net-protocol (0.3.0)
timeout
net-smtp (0.5.1)
net-protocol
Expand All @@ -146,7 +146,7 @@ GEM
racc (~> 1.4)
nokogiri (1.19.4-x86_64-linux-gnu)
racc (~> 1.4)
parallel (2.1.0)
parallel (2.2.0)
parser (3.3.12.0)
ast (~> 2.4.1)
racc
Expand All @@ -155,7 +155,7 @@ GEM
prettyprint (0.2.0)
prism (1.9.0)
racc (1.8.1)
rack (3.2.6)
rack (3.2.7)
rack-protection (4.2.1)
base64 (>= 0.1.0)
logger (>= 1.6.0)
Expand Down Expand Up @@ -199,7 +199,7 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.4.2)
rbs (4.1.0)
rbs (4.2.0)
logger
prism (>= 1.6.0)
tsort
Expand All @@ -209,7 +209,7 @@ GEM
rbs (>= 4.0.0)
tsort
regexp_parser (2.12.0)
reline (0.6.3)
reline (0.7.0)
io-console (~> 0.5)
rspec (3.13.2)
rspec-core (~> 3.13.0)
Expand All @@ -224,8 +224,8 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.7)
rubocop (1.89.0)
json (~> 2.3)
rubocop (1.91.0)
json (>= 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (>= 1.10)
Expand All @@ -238,16 +238,16 @@ GEM
rubocop-ast (1.50.0)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-performance (1.26.1)
rubocop-performance (1.27.0)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop (>= 1.89.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (1.13.0)
rubydex (0.3.0-arm64-darwin)
rubydex (0.3.0-x86_64-linux)
rubydex (0.4.1-arm64-darwin)
rubydex (0.4.1-x86_64-linux)
securerandom (0.4.1)
simplecov (1.0.3)
simpleidn (0.2.3)
simplecov (1.2.0)
simpleidn (0.3.0)
sinatra (4.2.1)
logger (>= 1.6.0)
mustermann (~> 3.0)
Expand All @@ -256,7 +256,7 @@ GEM
rack-session (>= 2.0.0, < 3)
tilt (~> 2.0)
thor (1.5.0)
tilt (2.8.0)
tilt (2.9.0)
timeout (0.6.1)
tsort (0.2.0)
tzinfo (2.0.6)
Expand All @@ -270,7 +270,7 @@ GEM
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.8.2)
zeitwerk (2.8.3)

PLATFORMS
arm64-darwin-23
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ use OpenapiFirst::Middlewares::ResponseValidation if ENV['RACK_ENV'] == 'test'
use OpenapiFirst::Middlewares::ResponseValidation, raise_error: false
```

If you are adopting OpenAPI you can use these options together with [hooks](#hooks) to get notified about requests/responses that do match your API description.
If you are adopting OpenAPI you can use these options together with [hooks](#hooks) to get notified about requests/responses that do not match your API description.

## Contract Testing

Expand Down Expand Up @@ -239,7 +239,7 @@ OpenapiFirst::Test.setup do |test|

test.ignore_response_error do |validated_response, rack_request|
# Ignore invalid response bodies on certain paths
validated_request.path.start_with?('/api/legacy/stuff') && validated_request.error.type == :invalid_body
rack_request.path.start_with?('/api/legacy/stuff') && validated_response.error.type == :invalid_body
end
end
```
Expand Down Expand Up @@ -275,7 +275,7 @@ Skip coverage for a request and all responses alltogether of a route with `skip_
```ruby
OpenapiFirst::Test.setup do |test|
test.skip_coverage do |path, request_method|
path == '/bookings/{bookingId}' && requests_method == 'DELETE'
path == '/bookings/{bookingId}' && request_method == 'DELETE'
end
end
```
Expand Down Expand Up @@ -371,7 +371,7 @@ definition.validate_request(rack_request, raise_error: true) # Raises OpenapiFir
```ruby
validated_response = definition.validate_response(rack_request, rack_response)

# Inspect the response and access parsed parameters and
# Inspect the response and access parsed parameters
validated_response.valid?
validated_response.invalid?
validated_response.error # => Failure object or nil
Expand All @@ -380,7 +380,7 @@ validated_response.parsed_body
validated_response.parsed_headers

# Or you can raise an exception if validation fails:
definition.validate_response(rack_request,rack_response, raise_error: true) # Raises OpenapiFirst::ResponseInvalidError or OpenapiFirst::ResponseNotFoundError
definition.validate_response(rack_request, rack_response, raise_error: true) # Raises OpenapiFirst::ResponseInvalidError or OpenapiFirst::ResponseNotFoundError
```

## Hooks
Expand Down Expand Up @@ -494,7 +494,7 @@ Here your OpenAPI schema defines endpoints starting with `/resource` but your ac

```ruby
oad = OpenapiFirst.load('openapi.yaml') do |config|
config.path = ->(req) { request.path.delete_prefix('/api') }
config.path = ->(req) { req.path.delete_prefix('/api') }
end
use OpenapiFirst::Middlewares::RequestValidation, oad
```
Expand Down
12 changes: 6 additions & 6 deletions benchmarks/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: ..
specs:
openapi_first (3.4.3)
openapi_first (4.0.0)
drb (~> 2.0)
hana (~> 1.3)
json_schemer (>= 2.1, < 3.0)
Expand Down Expand Up @@ -40,7 +40,7 @@ GEM
webrick
puma (8.0.2)
nio4r (~> 2.0)
rack (3.2.6)
rack (3.2.7)
rack-protection (4.2.1)
base64 (>= 0.1.0)
logger (>= 1.6.0)
Expand All @@ -49,17 +49,17 @@ GEM
base64 (>= 0.1.0)
rack (>= 3.0.0)
regexp_parser (2.12.0)
simpleidn (0.2.3)
simpleidn (0.3.0)
sinatra (4.2.1)
logger (>= 1.6.0)
mustermann (~> 3.0)
rack (>= 3.0.0, < 4)
rack-protection (= 4.2.1)
rack-session (>= 2.0.0, < 3)
tilt (~> 2.0)
tilt (2.7.0)
tilt (2.9.0)
uri (1.1.1)
vernier (1.10.1)
vernier (1.11.0)
webrick (1.9.2)

PLATFORMS
Expand All @@ -79,4 +79,4 @@ DEPENDENCIES
vernier

BUNDLED WITH
2.6.7
4.0.17
2 changes: 1 addition & 1 deletion lib/openapi_first/failure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module OpenapiFirst
# A failure object returned when validation or parsing of a request or response has failed.
# This returned in ValidatedRequest#error and ValidatedResponse#error.
class Failure < Data.define(:type, :message, :errors) # rubocop:disable Style/DataInheritance
class Failure < Data.define(:type, :message, :errors)
TYPES = {
not_found: [NotFoundError, 'Not found.'],
method_not_allowed: [RequestInvalidError, 'Request method is not defined.'],
Expand Down
2 changes: 1 addition & 1 deletion lib/openapi_first/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module OpenapiFirst
VERSION = '3.4.3'
VERSION = '4.0.0'
end
Loading