openapi_first 2.11.0 → 3.0.0
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.
- checksums.yaml +4 -4
 - data/CHANGELOG.md +50 -0
 - data/README.md +91 -58
 - data/lib/openapi_first/builder.rb +2 -2
 - data/lib/openapi_first/child_configuration.rb +26 -0
 - data/lib/openapi_first/configuration.rb +37 -6
 - data/lib/openapi_first/definition.rb +7 -3
 - data/lib/openapi_first/failure.rb +2 -7
 - data/lib/openapi_first/middlewares/request_validation.rb +7 -5
 - data/lib/openapi_first/middlewares/response_validation.rb +6 -3
 - data/lib/openapi_first/registry.rb +44 -0
 - data/lib/openapi_first/router/find_response.rb +13 -9
 - data/lib/openapi_first/router/path_template.rb +1 -1
 - data/lib/openapi_first/router.rb +7 -2
 - data/lib/openapi_first/schema/validation_error.rb +0 -18
 - data/lib/openapi_first/test/app.rb +30 -0
 - data/lib/openapi_first/test/callable.rb +1 -1
 - data/lib/openapi_first/test/configuration.rb +22 -14
 - data/lib/openapi_first/test/coverage/covered_request.rb +11 -0
 - data/lib/openapi_first/test/coverage/covered_response.rb +11 -0
 - data/lib/openapi_first/test/coverage/plan.rb +2 -2
 - data/lib/openapi_first/test/coverage/tracker.rb +32 -0
 - data/lib/openapi_first/test/coverage.rb +55 -16
 - data/lib/openapi_first/test/observe.rb +2 -0
 - data/lib/openapi_first/test.rb +19 -20
 - data/lib/openapi_first/validated_response.rb +8 -6
 - data/lib/openapi_first/version.rb +1 -1
 - data/lib/openapi_first.rb +6 -1
 - metadata +9 -4
 - data/lib/openapi_first/test/registry.rb +0 -44
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: 7474086c282336d7e77afe8143a0bda6e0a4a36fe284a746804bd933bdfd45a6
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 7538b4bd0e9f30f3d95c5cb06842b03e15644ce441018577c467629ce62d9772
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 2d1469a59f294ce4b386fb60da3e7c019773a8516005d1b3c0f60bd26158e62a879367647cab558579085a3b3c866055ac70b3df255c2fb38d7d918e10892154
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 835f24859d1510dfbe6a3fa99f1d9a496a0011441b041f8d92ee884b5938d53b5550fbe336a36adf44e6f8711fc30ec5333a7310de65cc93da8402e107b6beb4
         
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    | 
         @@ -2,6 +2,56 @@ 
     | 
|
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            ## Unreleased
         
     | 
| 
       4 
4 
     | 
    
         | 
| 
      
 5 
     | 
    
         
            +
            ## 3.0.0
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            ### openapi_first
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            #### Changed
         
     | 
| 
      
 10 
     | 
    
         
            +
            - Breaking: Trailing slashes are no longer ignored in dynamic paths. See [#403](https://github.com/ahx/openapi_first/issues/403).
         
     | 
| 
      
 11 
     | 
    
         
            +
              Before this change `GET /things/24/` matched `/things/{id}:`, but it no longer does.
         
     | 
| 
      
 12 
     | 
    
         
            +
            - Breaking: Failure type `:response_not_found` was split into two more specific types `:response_content_type_not_found` and `:response_status_not_found`. This should be mostly internal stuff. So if your custom error response used `response_not_found`, you will have to adapt.
         
     | 
| 
      
 13 
     | 
    
         
            +
            - Deprecated configuration fields `request_validation_raise_error` and `response_validation_raise_error`. Please pass the `raise_error:` option to the middlewares directly.
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            #### Added
         
     | 
| 
      
 16 
     | 
    
         
            +
            - Added support to register OADs globally via:
         
     | 
| 
      
 17 
     | 
    
         
            +
              ```ruby
         
     | 
| 
      
 18 
     | 
    
         
            +
              OpenapiFirst.configure { |config| config.register('openapi.yaml')  }
         
     | 
| 
      
 19 
     | 
    
         
            +
              ```
         
     | 
| 
      
 20 
     | 
    
         
            +
              This makes the `spec` argument in middlewares optional and removes the necessity to load the OAD in the same place where you use the middlewares and adds a cache for parsed OADs.
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
            #### Removed
         
     | 
| 
      
 23 
     | 
    
         
            +
            - Removed deprecated methods which produced a warning since 2.0.0.
         
     | 
| 
      
 24 
     | 
    
         
            +
            - Removed `OpenapiFirst::Configuration#clone`. Use `#child` instead.
         
     | 
| 
      
 25 
     | 
    
         
            +
            - It's no longer supported to remove locally added hooks during runtime.
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
            #### Fixed
         
     | 
| 
      
 28 
     | 
    
         
            +
            - Update dependency `openapi_parameters` to >= 0.7.0, because that version supports unpacking parameters the use `style: deepObject` with `explode: true`.
         
     | 
| 
      
 29 
     | 
    
         
            +
            - Make `OpenapiFirst::Test.setup` more robust by adding `OpenapiFirst::Configuration#child` so it does not matter if you load our OAD before callig `OpenapiFirst::Test.setup`.
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
            ### openapi_first/test
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
            #### Changed
         
     | 
| 
      
 34 
     | 
    
         
            +
            - `OpenapiFirst::Test.app` now returns an instance of `OpenapiFirst::Test::App`, instead of `Rack::Builer` and delegates methods other than `#call` to the original app. This wrapper adds validated requests, responses to the rack env at `env[OpenapiFirst::Test::REQUEST]`, `env[OpenapiFirst::Test::RESPONSE]`. This makes it possible to test Rails engines. Thanks to Josh! See [#410](https://github.com/ahx/openapi_first/issues/410).
         
     | 
| 
      
 35 
     | 
    
         
            +
            - `OpenapiFirst::Test` now falls back to using globally registered OADs if nothing was registered inside `OpenapiFirst::Test.setup`.
         
     | 
| 
      
 36 
     | 
    
         
            +
            - 401er and 500er status are okay to not be described.
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
            #### Added
         
     | 
| 
      
 39 
     | 
    
         
            +
            - The Coverage feature in `OpenapiFirst::Test` now supports parallel tests via a DRB client/sever. Thanks to Richard! See [#394](https://github.com/ahx/openapi_first/issues/394).
         
     | 
| 
      
 40 
     | 
    
         
            +
            - Added `OpenapiFirst::Test` Configuration options which are useful when adopting OpenAPI:
         
     | 
| 
      
 41 
     | 
    
         
            +
              - `ignore_unknown_response_status = true` to make API coverage no longer complain about undefined response statuses it sees during a test run.
         
     | 
| 
      
 42 
     | 
    
         
            +
              - `minimum_coverage=` is no longer deprecated. This is useful when gradually adopting OpenAPI
         
     | 
| 
      
 43 
     | 
    
         
            +
            - `ignored_unknown_status=` to overwrite the whole list of ignored unknown status at once
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
            #### Removed
         
     | 
| 
      
 46 
     | 
    
         
            +
            - Removed internally used `Test::Coverage.current_run, .plans, .install, .uninstall`. If you are using these, use `OpenapiFirst::Test.setup` instead.
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
            #### Fixed
         
     | 
| 
      
 49 
     | 
    
         
            +
            - Make `OpenapiFirst::Test.setup` more robust by adding `OpenapiFirst::Configuration#child` so it does not matter if you load our OAD before callig `OpenapiFirst::Test.setup`.
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
            ## 2.11.1
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
            - OpenapiFirst can now route requests correctly for paths like `/stuffs` and `/stuffs{format}` (https://github.com/ahx/openapi_first/issues/386)
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
       5 
55 
     | 
    
         
             
            ## 2.11.0
         
     | 
| 
       6 
56 
     | 
    
         | 
| 
       7 
57 
     | 
    
         
             
            - OpenapiFirst::Test.observe now works with `Rack::URLMap` (returned by `Rack::Builder.app`) and probably all objects that respond to `.call`
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -4,18 +4,21 @@ openapi_first is a Ruby gem for request / response validation and contract-testi 
     | 
|
| 
       4 
4 
     | 
    
         | 
| 
       5 
5 
     | 
    
         
             
            ## Usage
         
     | 
| 
       6 
6 
     | 
    
         | 
| 
      
 7 
     | 
    
         
            +
            Configure
         
     | 
| 
      
 8 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 9 
     | 
    
         
            +
            OpenapiFirst.register('openapi/openapi.yaml')
         
     | 
| 
      
 10 
     | 
    
         
            +
            ```
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
       7 
12 
     | 
    
         
             
            Use an OAD to validate incoming requests:
         
     | 
| 
       8 
13 
     | 
    
         
             
            ```ruby
         
     | 
| 
       9 
     | 
    
         
            -
            use OpenapiFirst::Middlewares::RequestValidation 
     | 
| 
      
 14 
     | 
    
         
            +
            use OpenapiFirst::Middlewares::RequestValidation
         
     | 
| 
       10 
15 
     | 
    
         
             
            ```
         
     | 
| 
       11 
16 
     | 
    
         | 
| 
       12 
17 
     | 
    
         
             
            Turn your request tests into [contract tests](#contract-testing) against an OAD:
         
     | 
| 
       13 
18 
     | 
    
         
             
            ```ruby
         
     | 
| 
       14 
19 
     | 
    
         
             
            # spec_helper.rb
         
     | 
| 
       15 
20 
     | 
    
         
             
            require 'openapi_first'
         
     | 
| 
       16 
     | 
    
         
            -
            OpenapiFirst::Test.setup 
     | 
| 
       17 
     | 
    
         
            -
              config.register('openapi/openapi.yaml')
         
     | 
| 
       18 
     | 
    
         
            -
            end
         
     | 
| 
      
 21 
     | 
    
         
            +
            OpenapiFirst::Test.setup
         
     | 
| 
       19 
22 
     | 
    
         | 
| 
       20 
23 
     | 
    
         
             
            require 'my_app'
         
     | 
| 
       21 
24 
     | 
    
         
             
            RSpec.configure do |config|
         
     | 
| 
         @@ -27,6 +30,7 @@ end 
     | 
|
| 
       27 
30 
     | 
    
         | 
| 
       28 
31 
     | 
    
         
             
            <!-- TOC -->
         
     | 
| 
       29 
32 
     | 
    
         | 
| 
      
 33 
     | 
    
         
            +
            - [Configuration](#configuration)
         
     | 
| 
       30 
34 
     | 
    
         
             
            - [Rack Middlewares](#rack-middlewares)
         
     | 
| 
       31 
35 
     | 
    
         
             
              - [Request validation](#request-validation)
         
     | 
| 
       32 
36 
     | 
    
         
             
              - [Response validation](#response-validation)
         
     | 
| 
         @@ -34,7 +38,6 @@ end 
     | 
|
| 
       34 
38 
     | 
    
         
             
              - [Test assertions](#test-assertions)
         
     | 
| 
       35 
39 
     | 
    
         
             
            - [Manual use](#manual-use)
         
     | 
| 
       36 
40 
     | 
    
         
             
            - [Framework integration](#framework-integration)
         
     | 
| 
       37 
     | 
    
         
            -
            - [Configuration](#configuration)
         
     | 
| 
       38 
41 
     | 
    
         
             
            - [Hooks](#hooks)
         
     | 
| 
       39 
42 
     | 
    
         
             
            - [Alternatives](#alternatives)
         
     | 
| 
       40 
43 
     | 
    
         
             
            - [Frequently Asked Questions](#frequently-asked-questions)
         
     | 
| 
         @@ -44,6 +47,33 @@ end 
     | 
|
| 
       44 
47 
     | 
    
         | 
| 
       45 
48 
     | 
    
         
             
            <!-- /TOC -->
         
     | 
| 
       46 
49 
     | 
    
         | 
| 
      
 50 
     | 
    
         
            +
            ## Configuration
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
            You should register OADs globally so you don't have to load the file multiple times or to refernce them by Symbol (like :v1 in this example).
         
     | 
| 
      
 53 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 54 
     | 
    
         
            +
            OpenapiFirst.configure do |config|
         
     | 
| 
      
 55 
     | 
    
         
            +
              config.register('openapi/openapi.yaml') # :default
         
     | 
| 
      
 56 
     | 
    
         
            +
              config.register('openapi/v1.openapi.yaml', as: :v1)
         
     | 
| 
      
 57 
     | 
    
         
            +
            end
         
     | 
| 
      
 58 
     | 
    
         
            +
            ```
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
            You can configure default options globally:
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 63 
     | 
    
         
            +
            OpenapiFirst.configure do |config|
         
     | 
| 
      
 64 
     | 
    
         
            +
              # Specify which plugin is used to render error responses returned by the request validation middleware (defaults to :default)
         
     | 
| 
      
 65 
     | 
    
         
            +
              config.request_validation_error_response = :jsonapi
         
     | 
| 
      
 66 
     | 
    
         
            +
            end
         
     | 
| 
      
 67 
     | 
    
         
            +
            ```
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
            or configure per instance:
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 72 
     | 
    
         
            +
            OpenapiFirst.load('openapi.yaml') do |config|
         
     | 
| 
      
 73 
     | 
    
         
            +
              config.request_validation_error_response = :jsonapi
         
     | 
| 
      
 74 
     | 
    
         
            +
            end
         
     | 
| 
      
 75 
     | 
    
         
            +
            ```
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
       47 
77 
     | 
    
         
             
            ## Rack Middlewares
         
     | 
| 
       48 
78 
     | 
    
         | 
| 
       49 
79 
     | 
    
         
             
            ### Request validation
         
     | 
| 
         @@ -51,10 +81,10 @@ end 
     | 
|
| 
       51 
81 
     | 
    
         
             
            The request validation middleware returns a 4xx if the request is invalid or not defined in the API description. It adds a request object to the current Rack environment at `env[OpenapiFirst::REQUEST]` with the request parameters parsed exactly as described in your API description plus access to meta information from your API description. See _[Manual use](#manual-use)_ for more details about that object.
         
     | 
| 
       52 
82 
     | 
    
         | 
| 
       53 
83 
     | 
    
         
             
            ```ruby
         
     | 
| 
       54 
     | 
    
         
            -
            use OpenapiFirst::Middlewares::RequestValidation 
     | 
| 
      
 84 
     | 
    
         
            +
            use OpenapiFirst::Middlewares::RequestValidation
         
     | 
| 
       55 
85 
     | 
    
         | 
| 
       56 
86 
     | 
    
         
             
            # Pass `raise_error: true` to raise an error if request is invalid:
         
     | 
| 
       57 
     | 
    
         
            -
            use OpenapiFirst::Middlewares::RequestValidation,  
     | 
| 
      
 87 
     | 
    
         
            +
            use OpenapiFirst::Middlewares::RequestValidation, raise_error: true
         
     | 
| 
       58 
88 
     | 
    
         
             
            ```
         
     | 
| 
       59 
89 
     | 
    
         | 
| 
       60 
90 
     | 
    
         
             
            #### Error responses
         
     | 
| 
         @@ -92,7 +122,7 @@ content-type: "application/problem+json" 
     | 
|
| 
       92 
122 
     | 
    
         
             
            openapi_first offers a [JSON:API](https://jsonapi.org/) error response by passing `error_response: :jsonapi`:
         
     | 
| 
       93 
123 
     | 
    
         | 
| 
       94 
124 
     | 
    
         
             
            ```ruby
         
     | 
| 
       95 
     | 
    
         
            -
            use OpenapiFirst::Middlewares::RequestValidation, 'openapi.yaml, error_response: :jsonapi 
     | 
| 
      
 125 
     | 
    
         
            +
            use OpenapiFirst::Middlewares::RequestValidation, 'openapi.yaml', error_response: :jsonapi
         
     | 
| 
       96 
126 
     | 
    
         
             
            ```
         
     | 
| 
       97 
127 
     | 
    
         | 
| 
       98 
128 
     | 
    
         
             
            <details>
         
     | 
| 
         @@ -145,10 +175,10 @@ This middleware raises an error by default if the response is not valid. 
     | 
|
| 
       145 
175 
     | 
    
         
             
            This can be useful in a test or staging environment, especially if you are adopting OpenAPI for an existing implementation.
         
     | 
| 
       146 
176 
     | 
    
         | 
| 
       147 
177 
     | 
    
         
             
            ```ruby
         
     | 
| 
       148 
     | 
    
         
            -
            use OpenapiFirst::Middlewares::ResponseValidation 
     | 
| 
      
 178 
     | 
    
         
            +
            use OpenapiFirst::Middlewares::ResponseValidation if ENV['RACK_ENV'] == 'test'
         
     | 
| 
       149 
179 
     | 
    
         | 
| 
       150 
180 
     | 
    
         
             
            # Pass `raise_error: false` to not raise an error:
         
     | 
| 
       151 
     | 
    
         
            -
            use OpenapiFirst::Middlewares::ResponseValidation,  
     | 
| 
      
 181 
     | 
    
         
            +
            use OpenapiFirst::Middlewares::ResponseValidation, raise_error: false
         
     | 
| 
       152 
182 
     | 
    
         
             
            ```
         
     | 
| 
       153 
183 
     | 
    
         | 
| 
       154 
184 
     | 
    
         
             
            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.
         
     | 
| 
         @@ -159,16 +189,14 @@ You can see your OpenAPI API description as a contract that your clients can rel 
     | 
|
| 
       159 
189 
     | 
    
         | 
| 
       160 
190 
     | 
    
         
             
            Here is how to set it up:
         
     | 
| 
       161 
191 
     | 
    
         | 
| 
       162 
     | 
    
         
            -
            1.  
     | 
| 
       163 
     | 
    
         
            -
              This should go at the top of your test helper file before loading your application code.
         
     | 
| 
      
 192 
     | 
    
         
            +
            1. Setup the test mode
         
     | 
| 
       164 
193 
     | 
    
         
             
                ```ruby
         
     | 
| 
      
 194 
     | 
    
         
            +
                # spec_helper.rb
         
     | 
| 
       165 
195 
     | 
    
         
             
                require 'openapi_first'
         
     | 
| 
       166 
     | 
    
         
            -
                OpenapiFirst::Test.setup 
     | 
| 
       167 
     | 
    
         
            -
                  config.register('openapi/openapi.yaml')
         
     | 
| 
       168 
     | 
    
         
            -
                end
         
     | 
| 
      
 196 
     | 
    
         
            +
                OpenapiFirst::Test.setup
         
     | 
| 
       169 
197 
     | 
    
         
             
                ```
         
     | 
| 
       170 
198 
     | 
    
         
             
            2. Observe your application. You can do this in multiple ways:
         
     | 
| 
       171 
     | 
    
         
            -
                - Add an `app` method to your tests 
     | 
| 
      
 199 
     | 
    
         
            +
                - Add an `app` method to your tests (which is called by rack-test) that wraps your application with silent request / response validation.
         
     | 
| 
       172 
200 
     | 
    
         
             
                  ```ruby
         
     | 
| 
       173 
201 
     | 
    
         
             
                  module RequestSpecHelpers
         
     | 
| 
       174 
202 
     | 
    
         
             
                    def app
         
     | 
| 
         @@ -188,18 +216,34 @@ Here is how to set it up: 
     | 
|
| 
       188 
216 
     | 
    
         
             
                    config.include OpenapiFirst::Test::Methods[MyApp], type: :request
         
     | 
| 
       189 
217 
     | 
    
         
             
                  end
         
     | 
| 
       190 
218 
     | 
    
         
             
                  ```
         
     | 
| 
       191 
     | 
    
         
            -
             
     | 
| 
      
 219 
     | 
    
         
            +
            3. Run your tests. The Coverage feature will tell you about missing or invalid requests/responses:
         
     | 
| 
      
 220 
     | 
    
         
            +
                  ```
         
     | 
| 
      
 221 
     | 
    
         
            +
                  ✓ GET /stations
         
     | 
| 
      
 222 
     | 
    
         
            +
                    ✓ 200(application/json)
         
     | 
| 
      
 223 
     | 
    
         
            +
                    ❌ 200(application/xml) – No responses tracked!
         
     | 
| 
      
 224 
     | 
    
         
            +
                    ❌ 400(application/problem+json) – No responses tracked!
         
     | 
| 
      
 225 
     | 
    
         
            +
                  ```
         
     | 
| 
      
 226 
     | 
    
         
            +
             
     | 
| 
      
 227 
     | 
    
         
            +
                  Now add tests for all those "❌" to make them "✓" and you're green!
         
     | 
| 
       192 
228 
     | 
    
         | 
| 
       193 
     | 
    
         
            -
             
     | 
| 
      
 229 
     | 
    
         
            +
            > [!NOTE]
         
     | 
| 
      
 230 
     | 
    
         
            +
            > Check out [faraday-openapi](https://codeberg.org/ahx/faraday-openapi) to have your API _client_ validate request/responses against an OAD, which is useful to validate HTTP mocks during testing.
         
     | 
| 
       194 
231 
     | 
    
         | 
| 
       195 
232 
     | 
    
         
             
            ### Configure test coverage
         
     | 
| 
       196 
233 
     | 
    
         | 
| 
       197 
     | 
    
         
            -
            OpenapiFirst::Test raises an error when a  
     | 
| 
      
 234 
     | 
    
         
            +
            OpenapiFirst::Test raises an error when a response status is not defined except for 404 and 500. You can change this:
         
     | 
| 
       198 
235 
     | 
    
         | 
| 
       199 
236 
     | 
    
         
             
            ```ruby
         
     | 
| 
       200 
237 
     | 
    
         
             
            OpenapiFirst::Test.setup do |test|
         
     | 
| 
       201 
     | 
    
         
            -
               
     | 
| 
       202 
     | 
    
         
            -
             
     | 
| 
      
 238 
     | 
    
         
            +
              test.ignored_unknown_status << 403
         
     | 
| 
      
 239 
     | 
    
         
            +
            end
         
     | 
| 
      
 240 
     | 
    
         
            +
            ```
         
     | 
| 
      
 241 
     | 
    
         
            +
             
     | 
| 
      
 242 
     | 
    
         
            +
            Or you can ignore all unknown response status:
         
     | 
| 
      
 243 
     | 
    
         
            +
             
     | 
| 
      
 244 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 245 
     | 
    
         
            +
            OpenapiFirst::Test.setup do |test|
         
     | 
| 
      
 246 
     | 
    
         
            +
              test.ignore_all_unknown_status = true
         
     | 
| 
       203 
247 
     | 
    
         
             
            end
         
     | 
| 
       204 
248 
     | 
    
         
             
            ```
         
     | 
| 
       205 
249 
     | 
    
         | 
| 
         @@ -207,7 +251,6 @@ Exclude certain _responses_ from coverage with `skip_coverage`: 
     | 
|
| 
       207 
251 
     | 
    
         | 
| 
       208 
252 
     | 
    
         
             
            ```ruby
         
     | 
| 
       209 
253 
     | 
    
         
             
            OpenapiFirst::Test.setup do |test|
         
     | 
| 
       210 
     | 
    
         
            -
              # …
         
     | 
| 
       211 
254 
     | 
    
         
             
              test.skip_response_coverage do |response_definition|
         
     | 
| 
       212 
255 
     | 
    
         
             
                response_definition.status == '5XX'
         
     | 
| 
       213 
256 
     | 
    
         
             
              end
         
     | 
| 
         @@ -218,34 +261,46 @@ Skip coverage for a request and all responses alltogether of a route with `skip_ 
     | 
|
| 
       218 
261 
     | 
    
         | 
| 
       219 
262 
     | 
    
         
             
            ```ruby
         
     | 
| 
       220 
263 
     | 
    
         
             
            OpenapiFirst::Test.setup do |test|
         
     | 
| 
       221 
     | 
    
         
            -
              # …
         
     | 
| 
       222 
264 
     | 
    
         
             
              test.skip_coverage do |path, request_method|
         
     | 
| 
       223 
265 
     | 
    
         
             
                path == '/bookings/{bookingId}' && requests_method == 'DELETE'
         
     | 
| 
       224 
266 
     | 
    
         
             
              end
         
     | 
| 
       225 
267 
     | 
    
         
             
            end
         
     | 
| 
       226 
268 
     | 
    
         
             
            ```
         
     | 
| 
       227 
269 
     | 
    
         | 
| 
      
 270 
     | 
    
         
            +
            OpenapiFirst::Test raises an error when a request is not defined. You can deactivate this with:
         
     | 
| 
      
 271 
     | 
    
         
            +
             
     | 
| 
      
 272 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 273 
     | 
    
         
            +
            OpenapiFirst::Test.setup do |test|
         
     | 
| 
      
 274 
     | 
    
         
            +
              test.ignore_unknown_requests = true
         
     | 
| 
      
 275 
     | 
    
         
            +
            end
         
     | 
| 
      
 276 
     | 
    
         
            +
            ```
         
     | 
| 
      
 277 
     | 
    
         
            +
             
     | 
| 
       228 
278 
     | 
    
         
             
            ### Test assertions
         
     | 
| 
       229 
279 
     | 
    
         | 
| 
       230 
     | 
    
         
            -
             
     | 
| 
      
 280 
     | 
    
         
            +
            > [!WARNING]
         
     | 
| 
      
 281 
     | 
    
         
            +
            > You probably don't need this. Just setup [Contract testing and API coverage](#contract-testing) and use your normal assertions.
         
     | 
| 
      
 282 
     | 
    
         
            +
             
     | 
| 
      
 283 
     | 
    
         
            +
            openapi_first ships with a simple but powerful Test method `assert_api_conform` to run request and response validation in your tests without using the middlewares. This is designed to be used with rack-test.
         
     | 
| 
       231 
284 
     | 
    
         | 
| 
       232 
     | 
    
         
            -
            Here is how to  
     | 
| 
      
 285 
     | 
    
         
            +
            Here is how to use it with RSpec, but MiniTest works just as good:
         
     | 
| 
       233 
286 
     | 
    
         | 
| 
       234 
     | 
    
         
            -
            Inside your test:
         
     | 
| 
       235 
287 
     | 
    
         
             
            ```ruby
         
     | 
| 
       236 
     | 
    
         
            -
            #  
     | 
| 
       237 
     | 
    
         
            -
             
     | 
| 
      
 288 
     | 
    
         
            +
            # spec_helper.rb
         
     | 
| 
      
 289 
     | 
    
         
            +
            OpenapiFirst::Test.setup do |test|
         
     | 
| 
      
 290 
     | 
    
         
            +
              test.register(File.join(__dir__, '../examples/openapi.yaml'), as: :example_app)
         
     | 
| 
      
 291 
     | 
    
         
            +
            end
         
     | 
| 
      
 292 
     | 
    
         
            +
            ```
         
     | 
| 
       238 
293 
     | 
    
         | 
| 
       239 
     | 
    
         
            -
            class TripsApiTest < ActionDispatch::IntegrationTest
         
     | 
| 
       240 
     | 
    
         
            -
              include OpenapiFirst::Test::Methods
         
     | 
| 
       241 
294 
     | 
    
         | 
| 
       242 
     | 
    
         
            -
             
     | 
| 
       243 
     | 
    
         
            -
             
     | 
| 
       244 
     | 
    
         
            -
             
     | 
| 
       245 
     | 
    
         
            -
             
     | 
| 
      
 295 
     | 
    
         
            +
            Inside your test :
         
     | 
| 
      
 296 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 297 
     | 
    
         
            +
            RSpec.describe 'Example App' do
         
     | 
| 
      
 298 
     | 
    
         
            +
              include Rack::Test::Methods
         
     | 
| 
      
 299 
     | 
    
         
            +
              include OpenapiFirst::Test::Methods[App]
         
     | 
| 
       246 
300 
     | 
    
         | 
| 
      
 301 
     | 
    
         
            +
              it 'is API conform' do
         
     | 
| 
      
 302 
     | 
    
         
            +
                get '/'
         
     | 
| 
       247 
303 
     | 
    
         
             
                assert_api_conform(status: 200)
         
     | 
| 
       248 
     | 
    
         
            -
                # assert_api_conform(status: 200, api: :v1) # Or this if you have multiple API descriptions
         
     | 
| 
       249 
304 
     | 
    
         
             
              end
         
     | 
| 
       250 
305 
     | 
    
         
             
            end
         
     | 
| 
       251 
306 
     | 
    
         
             
            ```
         
     | 
| 
         @@ -303,27 +358,6 @@ validated_response.parsed_headers 
     | 
|
| 
       303 
358 
     | 
    
         
             
            definition.validate_response(rack_request,rack_response, raise_error: true) # Raises OpenapiFirst::ResponseInvalidError or OpenapiFirst::ResponseNotFoundError
         
     | 
| 
       304 
359 
     | 
    
         
             
            ```
         
     | 
| 
       305 
360 
     | 
    
         | 
| 
       306 
     | 
    
         
            -
            ## Configuration
         
     | 
| 
       307 
     | 
    
         
            -
             
     | 
| 
       308 
     | 
    
         
            -
            You can configure default options globally:
         
     | 
| 
       309 
     | 
    
         
            -
             
     | 
| 
       310 
     | 
    
         
            -
            ```ruby
         
     | 
| 
       311 
     | 
    
         
            -
            OpenapiFirst.configure do |config|
         
     | 
| 
       312 
     | 
    
         
            -
              # Specify which plugin is used to render error responses returned by the request validation middleware (defaults to :default)
         
     | 
| 
       313 
     | 
    
         
            -
              config.request_validation_error_response = :jsonapi
         
     | 
| 
       314 
     | 
    
         
            -
              # Configure if the request validation middleware should raise an exception (defaults to false)
         
     | 
| 
       315 
     | 
    
         
            -
              config.request_validation_raise_error = true
         
     | 
| 
       316 
     | 
    
         
            -
            end
         
     | 
| 
       317 
     | 
    
         
            -
            ```
         
     | 
| 
       318 
     | 
    
         
            -
             
     | 
| 
       319 
     | 
    
         
            -
            or configure per instance:
         
     | 
| 
       320 
     | 
    
         
            -
             
     | 
| 
       321 
     | 
    
         
            -
            ```ruby
         
     | 
| 
       322 
     | 
    
         
            -
            OpenapiFirst.load('openapi.yaml') do |config|
         
     | 
| 
       323 
     | 
    
         
            -
              config.request_validation_error_response = :jsonapi
         
     | 
| 
       324 
     | 
    
         
            -
            end
         
     | 
| 
       325 
     | 
    
         
            -
            ```
         
     | 
| 
       326 
     | 
    
         
            -
             
     | 
| 
       327 
361 
     | 
    
         
             
            ## Hooks
         
     | 
| 
       328 
362 
     | 
    
         | 
| 
       329 
363 
     | 
    
         
             
            You can integrate your code at certain points during request/response validation via hooks.
         
     | 
| 
         @@ -363,11 +397,10 @@ end 
     | 
|
| 
       363 
397 
     | 
    
         
             
            ## Framework integration
         
     | 
| 
       364 
398 
     | 
    
         | 
| 
       365 
399 
     | 
    
         
             
            Using rack middlewares is supported in probably all Ruby web frameworks.
         
     | 
| 
       366 
     | 
    
         
            -
            If you are using Ruby on Rails for example, you can add the request validation middleware globally in `config/application.rb` or inside specific controllers.
         
     | 
| 
       367 
400 
     | 
    
         | 
| 
       368 
401 
     | 
    
         
             
            The contract testing feature is designed to be used via rack-test, which should be compatible all Ruby web frameworks as well.
         
     | 
| 
       369 
402 
     | 
    
         | 
| 
       370 
     | 
    
         
            -
            That aside, closer integration with specific frameworks like Sinatra, Hanami, Roda or  
     | 
| 
      
 403 
     | 
    
         
            +
            That aside, closer integration with specific frameworks like Sinatra, Hanami, Roda or others would be great. If you have ideas, pain points or PRs, please don't hesitate to [share](https://github.com/ahx/openapi_first/discussions).
         
     | 
| 
       371 
404 
     | 
    
         | 
| 
       372 
405 
     | 
    
         
             
            ## Alternatives
         
     | 
| 
       373 
406 
     | 
    
         | 
| 
         @@ -125,7 +125,7 @@ module OpenapiFirst 
     | 
|
| 
       125 
125 
     | 
    
         
             
                  end
         
     | 
| 
       126 
126 
     | 
    
         | 
| 
       127 
127 
     | 
    
         
             
                  Schema::Hash.new(schemas, required:, configuration: schemer_configuration,
         
     | 
| 
       128 
     | 
    
         
            -
                                            after_property_validation: config. 
     | 
| 
      
 128 
     | 
    
         
            +
                                            after_property_validation: config.after_request_parameter_property_validation)
         
     | 
| 
       129 
129 
     | 
    
         
             
                end
         
     | 
| 
       130 
130 
     | 
    
         | 
| 
       131 
131 
     | 
    
         
             
                def build_requests(path:, request_method:, operation_object:, parameters:)
         
     | 
| 
         @@ -139,7 +139,7 @@ module OpenapiFirst 
     | 
|
| 
       139 
139 
     | 
    
         
             
                  content_objects.map do |content_type, content_object|
         
     | 
| 
       140 
140 
     | 
    
         
             
                    content_schema = content_object['schema'].schema(
         
     | 
| 
       141 
141 
     | 
    
         
             
                      configuration: schemer_configuration,
         
     | 
| 
       142 
     | 
    
         
            -
                      after_property_validation: config. 
     | 
| 
      
 142 
     | 
    
         
            +
                      after_property_validation: config.after_request_body_property_validation
         
     | 
| 
       143 
143 
     | 
    
         
             
                    )
         
     | 
| 
       144 
144 
     | 
    
         
             
                    Request.new(path:, request_method:, parameters:,
         
     | 
| 
       145 
145 
     | 
    
         
             
                                operation_object: operation_object.resolved,
         
     | 
| 
         @@ -0,0 +1,26 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module OpenapiFirst
         
     | 
| 
      
 4 
     | 
    
         
            +
              # A subclass to configuration that points to its parent
         
     | 
| 
      
 5 
     | 
    
         
            +
              class ChildConfiguration < Configuration
         
     | 
| 
      
 6 
     | 
    
         
            +
                def initialize(parent:)
         
     | 
| 
      
 7 
     | 
    
         
            +
                  super()
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @parent = parent
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @request_validation_error_response = parent.request_validation_error_response
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @request_validation_raise_error = parent.request_validation_raise_error
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @response_validation_raise_error = parent.response_validation_raise_error
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @path = parent.path
         
     | 
| 
      
 13 
     | 
    
         
            +
                end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                private attr_reader :parent
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                HOOKS.each do |hook|
         
     | 
| 
      
 18 
     | 
    
         
            +
                  define_method(hook) do |&block|
         
     | 
| 
      
 19 
     | 
    
         
            +
                    return hooks[hook].chain(parent.hooks[hook]) if block.nil?
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                    hooks[hook] << block
         
     | 
| 
      
 22 
     | 
    
         
            +
                    block
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
              end
         
     | 
| 
      
 26 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -14,21 +14,52 @@ module OpenapiFirst 
     | 
|
| 
       14 
14 
     | 
    
         
             
                  @request_validation_error_response = OpenapiFirst.find_error_response(:default)
         
     | 
| 
       15 
15 
     | 
    
         
             
                  @request_validation_raise_error = false
         
     | 
| 
       16 
16 
     | 
    
         
             
                  @response_validation_raise_error = true
         
     | 
| 
       17 
     | 
    
         
            -
                  @hooks =  
     | 
| 
      
 17 
     | 
    
         
            +
                  @hooks = HOOKS.to_h { [_1, Set.new] }
         
     | 
| 
       18 
18 
     | 
    
         
             
                  @path = nil
         
     | 
| 
       19 
19 
     | 
    
         
             
                end
         
     | 
| 
       20 
20 
     | 
    
         | 
| 
       21 
     | 
    
         
            -
                 
     | 
| 
       22 
     | 
    
         
            -
             
     | 
| 
      
 21 
     | 
    
         
            +
                def register(path_or_definition, as: :default)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  OpenapiFirst.register(path_or_definition, as:)
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                attr_reader :hooks, :request_validation_error_response
         
     | 
| 
      
 26 
     | 
    
         
            +
                attr_accessor :path
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                # @deprecated
         
     | 
| 
      
 29 
     | 
    
         
            +
                attr_reader :request_validation_raise_error
         
     | 
| 
      
 30 
     | 
    
         
            +
                # @deprecated
         
     | 
| 
      
 31 
     | 
    
         
            +
                attr_reader :response_validation_raise_error
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                # Return a child configuration that still receives updates of global hooks.
         
     | 
| 
      
 34 
     | 
    
         
            +
                def child
         
     | 
| 
      
 35 
     | 
    
         
            +
                  ChildConfiguration.new(parent: self)
         
     | 
| 
      
 36 
     | 
    
         
            +
                end
         
     | 
| 
       23 
37 
     | 
    
         | 
| 
      
 38 
     | 
    
         
            +
                # @visibility private
         
     | 
| 
       24 
39 
     | 
    
         
             
                def clone
         
     | 
| 
       25 
     | 
    
         
            -
                   
     | 
| 
       26 
     | 
    
         
            -
             
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
      
 40 
     | 
    
         
            +
                  raise NoMethodError, 'OpenapiFirst::Configuration#clone was removed. You want to call #child instead'
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                # @deprecated Pass `raise_error:` to OpenapiFirst::Middlewares::RequestValidation directly
         
     | 
| 
      
 44 
     | 
    
         
            +
                def request_validation_raise_error=(value)
         
     | 
| 
      
 45 
     | 
    
         
            +
                  message = 'Setting OpenapiFirst::Configuration#request_validation_raise_error will be removed. ' \
         
     | 
| 
      
 46 
     | 
    
         
            +
                            'Please pass `raise_error:` to `OpenapiFirst::Middlewares::RequestValidation directly`'
         
     | 
| 
      
 47 
     | 
    
         
            +
                  warn message, category: :deprecated
         
     | 
| 
      
 48 
     | 
    
         
            +
                  @request_validation_raise_error = value
         
     | 
| 
      
 49 
     | 
    
         
            +
                end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                # @deprecated Pass `raise_error:` to OpenapiFirst::Middlewares::ResponseValidation directly
         
     | 
| 
      
 52 
     | 
    
         
            +
                def response_validation_raise_error=(value)
         
     | 
| 
      
 53 
     | 
    
         
            +
                  message = 'Setting OpenapiFirst::Configuration#request_validation_raise_error will be removed. ' \
         
     | 
| 
      
 54 
     | 
    
         
            +
                            'Please pass `raise_error:` to `OpenapiFirst::Middlewares::ResponseValidation directly`'
         
     | 
| 
      
 55 
     | 
    
         
            +
                  warn message
         
     | 
| 
      
 56 
     | 
    
         
            +
                  @response_validation_raise_error = value
         
     | 
| 
       28 
57 
     | 
    
         
             
                end
         
     | 
| 
       29 
58 
     | 
    
         | 
| 
       30 
59 
     | 
    
         
             
                HOOKS.each do |hook|
         
     | 
| 
       31 
60 
     | 
    
         
             
                  define_method(hook) do |&block|
         
     | 
| 
      
 61 
     | 
    
         
            +
                    return hooks[hook] if block.nil?
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
       32 
63 
     | 
    
         
             
                    hooks[hook] << block
         
     | 
| 
       33 
64 
     | 
    
         
             
                    block
         
     | 
| 
       34 
65 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -22,7 +22,7 @@ module OpenapiFirst 
     | 
|
| 
       22 
22 
     | 
    
         
             
                # @param filepath [String] The file path of the OpenAPI document.
         
     | 
| 
       23 
23 
     | 
    
         
             
                def initialize(contents, filepath = nil)
         
     | 
| 
       24 
24 
     | 
    
         
             
                  @filepath = filepath
         
     | 
| 
       25 
     | 
    
         
            -
                  @config = OpenapiFirst.configuration. 
     | 
| 
      
 25 
     | 
    
         
            +
                  @config = OpenapiFirst.configuration.child
         
     | 
| 
       26 
26 
     | 
    
         
             
                  yield @config if block_given?
         
     | 
| 
       27 
27 
     | 
    
         
             
                  @config.freeze
         
     | 
| 
       28 
28 
     | 
    
         
             
                  @router = Builder.build_router(contents, filepath:, config:)
         
     | 
| 
         @@ -58,6 +58,10 @@ module OpenapiFirst 
     | 
|
| 
       58 
58 
     | 
    
         
             
                  "#{title} @ #{version}"
         
     | 
| 
       59 
59 
     | 
    
         
             
                end
         
     | 
| 
       60 
60 
     | 
    
         | 
| 
      
 61 
     | 
    
         
            +
                def inspect
         
     | 
| 
      
 62 
     | 
    
         
            +
                  "#<#{self.class.name} @key='#{key}'>"
         
     | 
| 
      
 63 
     | 
    
         
            +
                end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
       61 
65 
     | 
    
         
             
                # Validates the request against the API description.
         
     | 
| 
       62 
66 
     | 
    
         
             
                # @param [Rack::Request] request The Rack request object.
         
     | 
| 
       63 
67 
     | 
    
         
             
                # @param [Boolean] raise_error Whether to raise an error if validation fails.
         
     | 
| 
         @@ -69,7 +73,7 @@ module OpenapiFirst 
     | 
|
| 
       69 
73 
     | 
    
         
             
                  else
         
     | 
| 
       70 
74 
     | 
    
         
             
                    route.request_definition.validate(request, route_params: route.params)
         
     | 
| 
       71 
75 
     | 
    
         
             
                  end.tap do |validated|
         
     | 
| 
       72 
     | 
    
         
            -
                    @config. 
     | 
| 
      
 76 
     | 
    
         
            +
                    @config.after_request_validation.each { |hook| hook.call(validated, self) }
         
     | 
| 
       73 
77 
     | 
    
         
             
                    raise validated.error.exception(validated) if validated.error && raise_error
         
     | 
| 
       74 
78 
     | 
    
         
             
                  end
         
     | 
| 
       75 
79 
     | 
    
         
             
                end
         
     | 
| 
         @@ -91,7 +95,7 @@ module OpenapiFirst 
     | 
|
| 
       91 
95 
     | 
    
         
             
                              else
         
     | 
| 
       92 
96 
     | 
    
         
             
                                response_match.response.validate(rack_response)
         
     | 
| 
       93 
97 
     | 
    
         
             
                              end
         
     | 
| 
       94 
     | 
    
         
            -
                  @config. 
     | 
| 
      
 98 
     | 
    
         
            +
                  @config.after_response_validation&.each { |hook| hook.call(validated, rack_request, self) }
         
     | 
| 
       95 
99 
     | 
    
         
             
                  raise validated.error.exception(validated) if raise_error && validated.invalid?
         
     | 
| 
       96 
100 
     | 
    
         | 
| 
       97 
101 
     | 
    
         
             
                  validated
         
     | 
| 
         @@ -13,7 +13,8 @@ module OpenapiFirst 
     | 
|
| 
       13 
13 
     | 
    
         
             
                  invalid_header: [RequestInvalidError, 'Request header is invalid:'],
         
     | 
| 
       14 
14 
     | 
    
         
             
                  invalid_path: [RequestInvalidError, 'Path segment is invalid:'],
         
     | 
| 
       15 
15 
     | 
    
         
             
                  invalid_cookie: [RequestInvalidError, 'Cookie value is invalid:'],
         
     | 
| 
       16 
     | 
    
         
            -
                   
     | 
| 
      
 16 
     | 
    
         
            +
                  response_content_type_not_found: [ResponseNotFoundError],
         
     | 
| 
      
 17 
     | 
    
         
            +
                  response_status_not_found: [ResponseNotFoundError],
         
     | 
| 
       17 
18 
     | 
    
         
             
                  invalid_response_body: [ResponseInvalidError, 'Response body is invalid:'],
         
     | 
| 
       18 
19 
     | 
    
         
             
                  invalid_response_header: [ResponseInvalidError, 'Response header is invalid:']
         
     | 
| 
       19 
20 
     | 
    
         
             
                }.freeze
         
     | 
| 
         @@ -65,12 +66,6 @@ module OpenapiFirst 
     | 
|
| 
       65 
66 
     | 
    
         
             
                  [message_prefix, @message || generate_message].compact.join(' ')
         
     | 
| 
       66 
67 
     | 
    
         
             
                end
         
     | 
| 
       67 
68 
     | 
    
         | 
| 
       68 
     | 
    
         
            -
                # @deprecated Please use {#type} instead
         
     | 
| 
       69 
     | 
    
         
            -
                def error_type
         
     | 
| 
       70 
     | 
    
         
            -
                  warn 'OpenapiFirst::Failure#error_type is deprecated. Use #type instead.'
         
     | 
| 
       71 
     | 
    
         
            -
                  type
         
     | 
| 
       72 
     | 
    
         
            -
                end
         
     | 
| 
       73 
     | 
    
         
            -
             
     | 
| 
       74 
69 
     | 
    
         
             
                private
         
     | 
| 
       75 
70 
     | 
    
         | 
| 
       76 
71 
     | 
    
         
             
                def generate_message
         
     | 
| 
         @@ -6,9 +6,11 @@ module OpenapiFirst 
     | 
|
| 
       6 
6 
     | 
    
         
             
                # A Rack middleware to validate requests against an OpenAPI API description
         
     | 
| 
       7 
7 
     | 
    
         
             
                class RequestValidation
         
     | 
| 
       8 
8 
     | 
    
         
             
                  # @param app The parent Rack application
         
     | 
| 
       9 
     | 
    
         
            -
                  # @param spec [String, OpenapiFirst::Definition] Path to the OpenAPI file or an instance of Definition.
         
     | 
| 
      
 9 
     | 
    
         
            +
                  # @param spec [String, Symbol, OpenapiFirst::Definition] Path to the OpenAPI file or an instance of Definition.
         
     | 
| 
      
 10 
     | 
    
         
            +
                  #   If you pass a Symbol, it will load the OAD registered via `OpenapiFirst.register`
         
     | 
| 
      
 11 
     | 
    
         
            +
                  #   If you leave this blank, it will load the OAD registered as `:default`
         
     | 
| 
       10 
12 
     | 
    
         
             
                  # @param options Hash
         
     | 
| 
       11 
     | 
    
         
            -
                  #   :spec [String, OpenapiFirst::Definition] Path to the OpenAPI file or an instance of Definition.
         
     | 
| 
      
 13 
     | 
    
         
            +
                  #   :spec [String, Symbol, OpenapiFirst::Definition] Path to the OpenAPI file or an instance of Definition.
         
     | 
| 
       12 
14 
     | 
    
         
             
                  #         This will be deprecated. Please use spec argument instead.
         
     | 
| 
       13 
15 
     | 
    
         
             
                  #   :raise_error    A Boolean indicating whether to raise an error if validation fails.
         
     | 
| 
       14 
16 
     | 
    
         
             
                  #                   default: false
         
     | 
| 
         @@ -20,13 +22,13 @@ module OpenapiFirst 
     | 
|
| 
       20 
22 
     | 
    
         
             
                    @app = app
         
     | 
| 
       21 
23 
     | 
    
         
             
                    if spec.is_a?(Hash)
         
     | 
| 
       22 
24 
     | 
    
         
             
                      options = spec
         
     | 
| 
       23 
     | 
    
         
            -
                      spec = options 
     | 
| 
      
 25 
     | 
    
         
            +
                      spec = options[:spec]
         
     | 
| 
       24 
26 
     | 
    
         
             
                    end
         
     | 
| 
       25 
27 
     | 
    
         
             
                    @raise = options.fetch(:raise_error, OpenapiFirst.configuration.request_validation_raise_error)
         
     | 
| 
       26 
28 
     | 
    
         
             
                    @error_response_class = error_response_option(options[:error_response])
         
     | 
| 
       27 
29 
     | 
    
         | 
| 
       28 
     | 
    
         
            -
                    spec ||=  
     | 
| 
       29 
     | 
    
         
            -
                     
     | 
| 
      
 30 
     | 
    
         
            +
                    spec ||= :default
         
     | 
| 
      
 31 
     | 
    
         
            +
                    spec = OpenapiFirst[spec] if spec.is_a?(Symbol)
         
     | 
| 
       30 
32 
     | 
    
         | 
| 
       31 
33 
     | 
    
         
             
                    @definition = spec.is_a?(Definition) ? spec : OpenapiFirst.load(spec)
         
     | 
| 
       32 
34 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -6,7 +6,9 @@ module OpenapiFirst 
     | 
|
| 
       6 
6 
     | 
    
         
             
              module Middlewares
         
     | 
| 
       7 
7 
     | 
    
         
             
                # A Rack middleware to validate requests against an OpenAPI API description
         
     | 
| 
       8 
8 
     | 
    
         
             
                class ResponseValidation
         
     | 
| 
       9 
     | 
    
         
            -
                  # @param spec [String, OpenapiFirst::Definition] Path to the OpenAPI file or an instance of Definition
         
     | 
| 
      
 9 
     | 
    
         
            +
                  # @param spec [String, Symbol, OpenapiFirst::Definition] Path to the OpenAPI file or an instance of Definition.
         
     | 
| 
      
 10 
     | 
    
         
            +
                  #   If you pass a Symbol, it will load the OAD registered via `OpenapiFirst.register`
         
     | 
| 
      
 11 
     | 
    
         
            +
                  #   If you leave this blank, it will load the OAD registered as `:default`
         
     | 
| 
       10 
12 
     | 
    
         
             
                  # @param options Hash
         
     | 
| 
       11 
13 
     | 
    
         
             
                  #   :spec [String, OpenapiFirst::Definition] Path to the OpenAPI file or an instance of Definition.
         
     | 
| 
       12 
14 
     | 
    
         
             
                  #         This will be deprecated. Please use spec argument instead.
         
     | 
| 
         @@ -15,11 +17,12 @@ module OpenapiFirst 
     | 
|
| 
       15 
17 
     | 
    
         
             
                    @app = app
         
     | 
| 
       16 
18 
     | 
    
         
             
                    if spec.is_a?(Hash)
         
     | 
| 
       17 
19 
     | 
    
         
             
                      options = spec
         
     | 
| 
       18 
     | 
    
         
            -
                      spec = options 
     | 
| 
      
 20 
     | 
    
         
            +
                      spec = options[:spec]
         
     | 
| 
       19 
21 
     | 
    
         
             
                    end
         
     | 
| 
       20 
22 
     | 
    
         
             
                    @raise = options.fetch(:raise_error, OpenapiFirst.configuration.response_validation_raise_error)
         
     | 
| 
       21 
23 
     | 
    
         | 
| 
       22 
     | 
    
         
            -
                     
     | 
| 
      
 24 
     | 
    
         
            +
                    spec ||= :default
         
     | 
| 
      
 25 
     | 
    
         
            +
                    spec = OpenapiFirst[spec] if spec.is_a?(Symbol)
         
     | 
| 
       23 
26 
     | 
    
         | 
| 
       24 
27 
     | 
    
         
             
                    @definition = spec.is_a?(Definition) ? spec : OpenapiFirst.load(spec)
         
     | 
| 
       25 
28 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -0,0 +1,44 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module OpenapiFirst
         
     | 
| 
      
 4 
     | 
    
         
            +
              class NotRegisteredError < Error; end
         
     | 
| 
      
 5 
     | 
    
         
            +
              class AlreadyRegisteredError < Error; end
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
              # @visibility private
         
     | 
| 
      
 8 
     | 
    
         
            +
              module Registry
         
     | 
| 
      
 9 
     | 
    
         
            +
                def definitions
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @definitions ||= {}
         
     | 
| 
      
 11 
     | 
    
         
            +
                end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                # Register an OpenAPI definition for testing
         
     | 
| 
      
 14 
     | 
    
         
            +
                # @param path_or_definition [String, Definition] Path to the OpenAPI file or a Definition object
         
     | 
| 
      
 15 
     | 
    
         
            +
                # @param as [Symbol] Name to register the API definition as
         
     | 
| 
      
 16 
     | 
    
         
            +
                def register(path_or_definition, as: :default)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  if definitions.key?(as) && as == :default
         
     | 
| 
      
 18 
     | 
    
         
            +
                    raise(
         
     | 
| 
      
 19 
     | 
    
         
            +
                      AlreadyRegisteredError,
         
     | 
| 
      
 20 
     | 
    
         
            +
                      "#{definitions[as].inspect} is already registered " \
         
     | 
| 
      
 21 
     | 
    
         
            +
                      "as ':default' so you cannot register #{path_or_definition.inspect} without " \
         
     | 
| 
      
 22 
     | 
    
         
            +
                      'giving it a custom name. Please call register with a custom key like: ' \
         
     | 
| 
      
 23 
     | 
    
         
            +
                      "#{name}.register(#{path_or_definition.inspect}, as: :my_other_api)"
         
     | 
| 
      
 24 
     | 
    
         
            +
                    )
         
     | 
| 
      
 25 
     | 
    
         
            +
                  end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                  definition = OpenapiFirst.load(path_or_definition)
         
     | 
| 
      
 28 
     | 
    
         
            +
                  definitions[as] = definition
         
     | 
| 
      
 29 
     | 
    
         
            +
                  definition
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                def [](api)
         
     | 
| 
      
 33 
     | 
    
         
            +
                  return api if api.is_a?(Definition)
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                  definitions.fetch(api) do
         
     | 
| 
      
 36 
     | 
    
         
            +
                    option = api == :default ? '' : ", as: #{api.inspect}"
         
     | 
| 
      
 37 
     | 
    
         
            +
                    raise(NotRegisteredError,
         
     | 
| 
      
 38 
     | 
    
         
            +
                          "API description '#{api.inspect}' not found." \
         
     | 
| 
      
 39 
     | 
    
         
            +
                          "Please call #{name}.register('myopenapi.yaml'#{option}) " \
         
     | 
| 
      
 40 
     | 
    
         
            +
                          'once before running your app.')
         
     | 
| 
      
 41 
     | 
    
         
            +
                  end
         
     | 
| 
      
 42 
     | 
    
         
            +
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
      
 44 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -10,18 +10,15 @@ module OpenapiFirst 
     | 
|
| 
       10 
10 
     | 
    
         | 
| 
       11 
11 
     | 
    
         
             
                  def self.call(responses, status, content_type, request_method:, path:)
         
     | 
| 
       12 
12 
     | 
    
         
             
                    contents = find_status(responses, status)
         
     | 
| 
       13 
     | 
    
         
            -
                    if contents.nil?
         
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
     | 
    
         
            -
                                "Defined statuses are: #{responses.keys.join(', ')}."
         
     | 
| 
       16 
     | 
    
         
            -
                      return Match.new(error: Failure.new(:response_not_found, message:), response: nil)
         
     | 
| 
       17 
     | 
    
         
            -
                    end
         
     | 
| 
      
 13 
     | 
    
         
            +
                    return response_status_not_found(status:, request_method:, path:, responses:) if contents.nil?
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
       18 
15 
     | 
    
         
             
                    response = FindContent.call(contents, content_type)
         
     | 
| 
       19 
     | 
    
         
            -
                    return  
     | 
| 
      
 16 
     | 
    
         
            +
                    return response_content_type_not_found(content_type:, contents:, request_method:, path:) unless response
         
     | 
| 
       20 
17 
     | 
    
         | 
| 
       21 
18 
     | 
    
         
             
                    Match.new(response:, error: nil)
         
     | 
| 
       22 
19 
     | 
    
         
             
                  end
         
     | 
| 
       23 
20 
     | 
    
         | 
| 
       24 
     | 
    
         
            -
                  def self. 
     | 
| 
      
 21 
     | 
    
         
            +
                  def self.response_content_type_not_found(content_type:, contents:, request_method:, path:)
         
     | 
| 
       25 
22 
     | 
    
         
             
                    empty_content = content_type.nil? || content_type.empty?
         
     | 
| 
       26 
23 
     | 
    
         
             
                    message =
         
     | 
| 
       27 
24 
     | 
    
         
             
                      "Content-Type should be #{contents.keys.join(' or ')}, " \
         
     | 
| 
         @@ -29,11 +26,18 @@ module OpenapiFirst 
     | 
|
| 
       29 
26 
     | 
    
         
             
                      "#{request_method.upcase} #{path}"
         
     | 
| 
       30 
27 
     | 
    
         | 
| 
       31 
28 
     | 
    
         
             
                    Match.new(
         
     | 
| 
       32 
     | 
    
         
            -
                      error: Failure.new(: 
     | 
| 
      
 29 
     | 
    
         
            +
                      error: Failure.new(:response_content_type_not_found, message:),
         
     | 
| 
       33 
30 
     | 
    
         
             
                      response: nil
         
     | 
| 
       34 
31 
     | 
    
         
             
                    )
         
     | 
| 
       35 
32 
     | 
    
         
             
                  end
         
     | 
| 
       36 
     | 
    
         
            -
                  private_class_method : 
     | 
| 
      
 33 
     | 
    
         
            +
                  private_class_method :response_content_type_not_found
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                  def self.response_status_not_found(status:, request_method:, path:, responses:)
         
     | 
| 
      
 36 
     | 
    
         
            +
                    message = "Status #{status} is not defined for #{request_method.upcase} #{path}. " \
         
     | 
| 
      
 37 
     | 
    
         
            +
                              "Defined statuses are: #{responses.keys.join(', ')}."
         
     | 
| 
      
 38 
     | 
    
         
            +
                    Match.new(error: Failure.new(:response_status_not_found, message:), response: nil)
         
     | 
| 
      
 39 
     | 
    
         
            +
                  end
         
     | 
| 
      
 40 
     | 
    
         
            +
                  private_class_method :response_status_not_found
         
     | 
| 
       37 
41 
     | 
    
         | 
| 
       38 
42 
     | 
    
         
             
                  def self.find_status(responses, status)
         
     | 
| 
       39 
43 
     | 
    
         
             
                    # According to OAS status has to be a string,
         
     | 
    
        data/lib/openapi_first/router.rb
    CHANGED
    
    | 
         @@ -97,10 +97,15 @@ module OpenapiFirst 
     | 
|
| 
       97 
97 
     | 
    
         
             
                  found = @static[request_path]
         
     | 
| 
       98 
98 
     | 
    
         
             
                  return [found, {}] if found
         
     | 
| 
       99 
99 
     | 
    
         | 
| 
       100 
     | 
    
         
            -
                  @dynamic. 
     | 
| 
      
 100 
     | 
    
         
            +
                  matches = @dynamic.filter_map do |_path, path_item|
         
     | 
| 
       101 
101 
     | 
    
         
             
                    params = path_item[:template].match(request_path)
         
     | 
| 
       102 
     | 
    
         
            -
                     
     | 
| 
      
 102 
     | 
    
         
            +
                    next unless params
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
                    [path_item, params]
         
     | 
| 
       103 
105 
     | 
    
         
             
                  end
         
     | 
| 
      
 106 
     | 
    
         
            +
                  return matches.first if matches.length == 1
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                  matches&.min_by { |match| match[1].values.sum(&:length) }
         
     | 
| 
       104 
109 
     | 
    
         
             
                end
         
     | 
| 
       105 
110 
     | 
    
         
             
              end
         
     | 
| 
       106 
111 
     | 
    
         
             
            end
         
     | 
| 
         @@ -40,24 +40,6 @@ module OpenapiFirst 
     | 
|
| 
       40 
40 
     | 
    
         
             
                      "value at #{location} is invalid (#{type.inspect})"
         
     | 
| 
       41 
41 
     | 
    
         
             
                    end
         
     | 
| 
       42 
42 
     | 
    
         
             
                  end
         
     | 
| 
       43 
     | 
    
         
            -
             
     | 
| 
       44 
     | 
    
         
            -
                  # @deprecated Please use {#message} instead
         
     | 
| 
       45 
     | 
    
         
            -
                  def error
         
     | 
| 
       46 
     | 
    
         
            -
                    warn 'OpenapiFirst::Schema::ValidationError#error is deprecated. Use #message instead.'
         
     | 
| 
       47 
     | 
    
         
            -
                    message
         
     | 
| 
       48 
     | 
    
         
            -
                  end
         
     | 
| 
       49 
     | 
    
         
            -
             
     | 
| 
       50 
     | 
    
         
            -
                  # @deprecated Please use {#data_pointer} instead
         
     | 
| 
       51 
     | 
    
         
            -
                  def instance_location
         
     | 
| 
       52 
     | 
    
         
            -
                    warn 'OpenapiFirst::Schema::ValidationError#instance_location is deprecated. Use #data_pointer instead.'
         
     | 
| 
       53 
     | 
    
         
            -
                    data_pointer
         
     | 
| 
       54 
     | 
    
         
            -
                  end
         
     | 
| 
       55 
     | 
    
         
            -
             
     | 
| 
       56 
     | 
    
         
            -
                  # @deprecated Please use {#schema_pointer} instead
         
     | 
| 
       57 
     | 
    
         
            -
                  def schema_location
         
     | 
| 
       58 
     | 
    
         
            -
                    warn 'OpenapiFirst::Schema::ValidationError#schema_location is deprecated. Use #schema_pointer instead.'
         
     | 
| 
       59 
     | 
    
         
            -
                    schema_pointer
         
     | 
| 
       60 
     | 
    
         
            -
                  end
         
     | 
| 
       61 
43 
     | 
    
         
             
                end
         
     | 
| 
       62 
44 
     | 
    
         
             
              end
         
     | 
| 
       63 
45 
     | 
    
         
             
            end
         
     | 
| 
         @@ -0,0 +1,30 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative 'observe'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module OpenapiFirst
         
     | 
| 
      
 6 
     | 
    
         
            +
              module Test
         
     | 
| 
      
 7 
     | 
    
         
            +
                REQUEST = 'openapi.test.request'
         
     | 
| 
      
 8 
     | 
    
         
            +
                RESPONSE = 'openapi.test.response'
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                # A wrapper of the original app
         
     | 
| 
      
 11 
     | 
    
         
            +
                # with silent request/response validation to track requests/responses.
         
     | 
| 
      
 12 
     | 
    
         
            +
                class App < SimpleDelegator
         
     | 
| 
      
 13 
     | 
    
         
            +
                  def initialize(app, api:)
         
     | 
| 
      
 14 
     | 
    
         
            +
                    super(app)
         
     | 
| 
      
 15 
     | 
    
         
            +
                    @app = app
         
     | 
| 
      
 16 
     | 
    
         
            +
                    @definition = Test[api]
         
     | 
| 
      
 17 
     | 
    
         
            +
                  end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                  def call(env)
         
     | 
| 
      
 20 
     | 
    
         
            +
                    request = Rack::Request.new(env)
         
     | 
| 
      
 21 
     | 
    
         
            +
                    env[Test::REQUEST] = @definition.validate_request(request, raise_error: false)
         
     | 
| 
      
 22 
     | 
    
         
            +
                    response = @app.call(env)
         
     | 
| 
      
 23 
     | 
    
         
            +
                    status, headers, body = response
         
     | 
| 
      
 24 
     | 
    
         
            +
                    env[Test::RESPONSE] =
         
     | 
| 
      
 25 
     | 
    
         
            +
                      @definition.validate_response(request, Rack::Response[status, headers, body], raise_error: false)
         
     | 
| 
      
 26 
     | 
    
         
            +
                    response
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end
         
     | 
| 
      
 28 
     | 
    
         
            +
                end
         
     | 
| 
      
 29 
     | 
    
         
            +
              end
         
     | 
| 
      
 30 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -6,7 +6,7 @@ module OpenapiFirst 
     | 
|
| 
       6 
6 
     | 
    
         
             
                # This is used by Openapi::Test.observe
         
     | 
| 
       7 
7 
     | 
    
         
             
                module Callable
         
     | 
| 
       8 
8 
     | 
    
         
             
                  # Returns a Module with a `call(env)` method that wraps super inside silent request/response validation
         
     | 
| 
       9 
     | 
    
         
            -
                  # You can use  
     | 
| 
      
 9 
     | 
    
         
            +
                  # You can use `Application.prepend(OpenapiFirst::Test::Callable[oad])` to monitor your app during testing.
         
     | 
| 
       10 
10 
     | 
    
         
             
                  def self.[](definition)
         
     | 
| 
       11 
11 
     | 
    
         
             
                    Module.new.tap do |mod|
         
     | 
| 
       12 
12 
     | 
    
         
             
                      mod.define_method(:call) do |env|
         
     | 
| 
         @@ -11,28 +11,33 @@ module OpenapiFirst 
     | 
|
| 
       11 
11 
     | 
    
         
             
                    @skip_response_coverage = nil
         
     | 
| 
       12 
12 
     | 
    
         
             
                    @skip_coverage = nil
         
     | 
| 
       13 
13 
     | 
    
         
             
                    @response_raise_error = true
         
     | 
| 
       14 
     | 
    
         
            -
                    @ignored_unknown_status = [404]
         
     | 
| 
      
 14 
     | 
    
         
            +
                    @ignored_unknown_status = Set.new([401, 404, 500])
         
     | 
| 
      
 15 
     | 
    
         
            +
                    @ignore_unknown_response_status = false
         
     | 
| 
       15 
16 
     | 
    
         
             
                    @report_coverage = true
         
     | 
| 
       16 
17 
     | 
    
         
             
                    @ignore_unknown_requests = false
         
     | 
| 
       17 
     | 
    
         
            -
                    @registry = {}
         
     | 
| 
       18 
     | 
    
         
            -
                    @apps = {}
         
     | 
| 
       19 
18 
     | 
    
         
             
                  end
         
     | 
| 
       20 
19 
     | 
    
         | 
| 
       21 
20 
     | 
    
         
             
                  # Register OADs, but don't load them just yet
         
     | 
| 
       22 
21 
     | 
    
         
             
                  # @param [OpenapiFirst::OAD] oad The OAD to register
         
     | 
| 
       23 
22 
     | 
    
         
             
                  # @param [Symbol] as The name to register the OAD under
         
     | 
| 
       24 
23 
     | 
    
         
             
                  def register(oad, as: :default)
         
     | 
| 
       25 
     | 
    
         
            -
                     
     | 
| 
      
 24 
     | 
    
         
            +
                    Test.register(oad, as:)
         
     | 
| 
       26 
25 
     | 
    
         
             
                  end
         
     | 
| 
       27 
26 
     | 
    
         | 
| 
       28 
27 
     | 
    
         
             
                  # Observe a rack app
         
     | 
| 
       29 
28 
     | 
    
         
             
                  def observe(app, api: :default)
         
     | 
| 
       30 
     | 
    
         
            -
                    ( 
     | 
| 
      
 29 
     | 
    
         
            +
                    Observe.observe(app, api:)
         
     | 
| 
       31 
30 
     | 
    
         
             
                  end
         
     | 
| 
       32 
31 
     | 
    
         | 
| 
       33 
32 
     | 
    
         
             
                  attr_accessor :coverage_formatter_options, :coverage_formatter, :response_raise_error,
         
     | 
| 
       34 
     | 
    
         
            -
                                :ignore_unknown_requests
         
     | 
| 
       35 
     | 
    
         
            -
                  attr_reader : 
     | 
| 
      
 33 
     | 
    
         
            +
                                :ignore_unknown_requests, :ignore_unknown_response_status, :minimum_coverage
         
     | 
| 
      
 34 
     | 
    
         
            +
                  attr_reader :report_coverage, :ignored_unknown_status
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                  # Set ignored unknown status codes.
         
     | 
| 
      
 37 
     | 
    
         
            +
                  # @param [Array<Integer>] status Status codes that are okay not to cover in an OAD
         
     | 
| 
      
 38 
     | 
    
         
            +
                  def ignored_unknown_status=(status)
         
     | 
| 
      
 39 
     | 
    
         
            +
                    @ignored_unknown_status = status.to_set
         
     | 
| 
      
 40 
     | 
    
         
            +
                  end
         
     | 
| 
       36 
41 
     | 
    
         | 
| 
       37 
42 
     | 
    
         
             
                  # Configure report coverage
         
     | 
| 
       38 
43 
     | 
    
         
             
                  # @param [Boolean, :warn] value Whether to report coverage or just warn.
         
     | 
| 
         @@ -45,13 +50,6 @@ module OpenapiFirst 
     | 
|
| 
       45 
50 
     | 
    
         
             
                    @report_coverage = value
         
     | 
| 
       46 
51 
     | 
    
         
             
                  end
         
     | 
| 
       47 
52 
     | 
    
         | 
| 
       48 
     | 
    
         
            -
                  # @deprecated Use skip_response_coverage, ignored_unknown_status or skip_coverage to configure coverage
         
     | 
| 
       49 
     | 
    
         
            -
                  def minimum_coverage=(value)
         
     | 
| 
       50 
     | 
    
         
            -
                    warn 'OpenapiFirst::Test::Configuration#minimum_coverage= is deprecated. ' \
         
     | 
| 
       51 
     | 
    
         
            -
                         'Use skip_response_coverage, ignored_unknown_status to configure coverage instead.'
         
     | 
| 
       52 
     | 
    
         
            -
                    @minimum_coverage = value
         
     | 
| 
       53 
     | 
    
         
            -
                  end
         
     | 
| 
       54 
     | 
    
         
            -
             
     | 
| 
       55 
53 
     | 
    
         
             
                  def skip_response_coverage(&block)
         
     | 
| 
       56 
54 
     | 
    
         
             
                    return @skip_response_coverage unless block_given?
         
     | 
| 
       57 
55 
     | 
    
         | 
| 
         @@ -63,6 +61,16 @@ module OpenapiFirst 
     | 
|
| 
       63 
61 
     | 
    
         | 
| 
       64 
62 
     | 
    
         
             
                    @skip_coverage = block
         
     | 
| 
       65 
63 
     | 
    
         
             
                  end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                  alias ignore_unknown_response_status? ignore_unknown_response_status
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                  def ignore_response?(validated_response)
         
     | 
| 
      
 68 
     | 
    
         
            +
                    return false if validated_response.known?
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                    return true if ignored_unknown_status.include?(validated_response.status)
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                    ignore_unknown_response_status? && validated_response.error.type == :response_status_not_found
         
     | 
| 
      
 73 
     | 
    
         
            +
                  end
         
     | 
| 
       66 
74 
     | 
    
         
             
                end
         
     | 
| 
       67 
75 
     | 
    
         
             
              end
         
     | 
| 
       68 
76 
     | 
    
         
             
            end
         
     | 
| 
         @@ -37,11 +37,11 @@ module OpenapiFirst 
     | 
|
| 
       37 
37 
     | 
    
         
             
                    private attr_reader :index
         
     | 
| 
       38 
38 
     | 
    
         | 
| 
       39 
39 
     | 
    
         
             
                    def track_request(validated_request)
         
     | 
| 
       40 
     | 
    
         
            -
                      index[validated_request. 
     | 
| 
      
 40 
     | 
    
         
            +
                      index[validated_request.key]&.track(validated_request)
         
     | 
| 
       41 
41 
     | 
    
         
             
                    end
         
     | 
| 
       42 
42 
     | 
    
         | 
| 
       43 
43 
     | 
    
         
             
                    def track_response(validated_response)
         
     | 
| 
       44 
     | 
    
         
            -
                      index[validated_response. 
     | 
| 
      
 44 
     | 
    
         
            +
                      index[validated_response.key]&.track(validated_response)
         
     | 
| 
       45 
45 
     | 
    
         
             
                    end
         
     | 
| 
       46 
46 
     | 
    
         | 
| 
       47 
47 
     | 
    
         
             
                    def done?
         
     | 
| 
         @@ -0,0 +1,32 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module OpenapiFirst
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Test
         
     | 
| 
      
 5 
     | 
    
         
            +
                module Coverage
         
     | 
| 
      
 6 
     | 
    
         
            +
                  # Class that allows tracking requests and response for OAD definitions.
         
     | 
| 
      
 7 
     | 
    
         
            +
                  # For each definition it builds a plan and forwards tracking to the correct plan.
         
     | 
| 
      
 8 
     | 
    
         
            +
                  class Tracker
         
     | 
| 
      
 9 
     | 
    
         
            +
                    attr_reader :plans_by_key
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                    def initialize(definitions, skip_response: nil, skip_route: nil)
         
     | 
| 
      
 12 
     | 
    
         
            +
                      @plans_by_key = definitions.values.to_h do |oad|
         
     | 
| 
      
 13 
     | 
    
         
            +
                        plan = Plan.for(oad, skip_response:, skip_route:)
         
     | 
| 
      
 14 
     | 
    
         
            +
                        [oad.key, plan]
         
     | 
| 
      
 15 
     | 
    
         
            +
                      end
         
     | 
| 
      
 16 
     | 
    
         
            +
                    end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                    def track_request(key, request)
         
     | 
| 
      
 19 
     | 
    
         
            +
                      @plans_by_key[key]&.track_request(request)
         
     | 
| 
      
 20 
     | 
    
         
            +
                    end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                    def track_response(key, response)
         
     | 
| 
      
 23 
     | 
    
         
            +
                      @plans_by_key[key]&.track_response(response)
         
     | 
| 
      
 24 
     | 
    
         
            +
                    end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                    def plans
         
     | 
| 
      
 27 
     | 
    
         
            +
                      @plans_by_key.values
         
     | 
| 
      
 28 
     | 
    
         
            +
                    end
         
     | 
| 
      
 29 
     | 
    
         
            +
                  end
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
              end
         
     | 
| 
      
 32 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -1,6 +1,10 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            require_relative 'coverage/plan'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require_relative 'coverage/tracker'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require_relative 'coverage/covered_request'
         
     | 
| 
      
 6 
     | 
    
         
            +
            require_relative 'coverage/covered_response'
         
     | 
| 
      
 7 
     | 
    
         
            +
            require 'drb'
         
     | 
| 
       4 
8 
     | 
    
         | 
| 
       5 
9 
     | 
    
         
             
            module OpenapiFirst
         
     | 
| 
       6 
10 
     | 
    
         
             
              module Test
         
     | 
| 
         @@ -12,45 +16,80 @@ module OpenapiFirst 
     | 
|
| 
       12 
16 
     | 
    
         | 
| 
       13 
17 
     | 
    
         
             
                  Result = Data.define(:plans, :coverage)
         
     | 
| 
       14 
18 
     | 
    
         | 
| 
       15 
     | 
    
         
            -
                  @current_run = {}
         
     | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
       17 
19 
     | 
    
         
             
                  class << self
         
     | 
| 
       18 
     | 
    
         
            -
                     
     | 
| 
      
 20 
     | 
    
         
            +
                    def start(skip_response: nil, skip_route: nil)
         
     | 
| 
      
 21 
     | 
    
         
            +
                      return if @drb_uri
         
     | 
| 
       19 
22 
     | 
    
         | 
| 
       20 
     | 
    
         
            -
             
     | 
| 
      
 23 
     | 
    
         
            +
                      tracker = Tracker.new(Test.definitions, skip_response:, skip_route:)
         
     | 
| 
       21 
24 
     | 
    
         | 
| 
       22 
     | 
    
         
            -
             
     | 
| 
       23 
     | 
    
         
            -
                       
     | 
| 
       24 
     | 
    
         
            -
             
     | 
| 
       25 
     | 
    
         
            -
                        [oad.key, plan]
         
     | 
| 
       26 
     | 
    
         
            -
                      end
         
     | 
| 
      
 25 
     | 
    
         
            +
                      # We need a custom DRbServer (not using DRb.start_service) because otherwise
         
     | 
| 
      
 26 
     | 
    
         
            +
                      # we'd conflict with Rails's DRb server
         
     | 
| 
      
 27 
     | 
    
         
            +
                      @drb_uri = DRb::DRbServer.new(nil, tracker).uri
         
     | 
| 
       27 
28 
     | 
    
         
             
                    end
         
     | 
| 
       28 
29 
     | 
    
         | 
| 
       29 
     | 
    
         
            -
                    def uninstall = Test.uninstall
         
     | 
| 
       30 
     | 
    
         
            -
             
     | 
| 
       31 
30 
     | 
    
         
             
                    # Clear current coverage run
         
     | 
| 
       32 
31 
     | 
    
         
             
                    def reset
         
     | 
| 
       33 
     | 
    
         
            -
                      @ 
     | 
| 
      
 32 
     | 
    
         
            +
                      @tracker = nil
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                      return unless @drb_uri
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                      service = DRb.fetch_server(@drb_uri)
         
     | 
| 
      
 37 
     | 
    
         
            +
                      service&.stop_service
         
     | 
| 
      
 38 
     | 
    
         
            +
                      @drb_uri = nil
         
     | 
| 
       34 
39 
     | 
    
         
             
                    end
         
     | 
| 
       35 
40 
     | 
    
         | 
| 
       36 
41 
     | 
    
         
             
                    def track_request(request, oad)
         
     | 
| 
       37 
     | 
    
         
            -
                       
     | 
| 
      
 42 
     | 
    
         
            +
                      return unless request.known?
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                      # The call to `track_request` may happen remotely in the main process that started
         
     | 
| 
      
 45 
     | 
    
         
            +
                      # the coverage collection.
         
     | 
| 
      
 46 
     | 
    
         
            +
                      # To make this work we need to keep arguments trivial, which is the reason the request
         
     | 
| 
      
 47 
     | 
    
         
            +
                      # is wrapped in a CoveredRequest data object.
         
     | 
| 
      
 48 
     | 
    
         
            +
                      tracker&.track_request(
         
     | 
| 
      
 49 
     | 
    
         
            +
                        oad.key,
         
     | 
| 
      
 50 
     | 
    
         
            +
                        CoveredRequest.new(
         
     | 
| 
      
 51 
     | 
    
         
            +
                          key: request.request_definition.key,
         
     | 
| 
      
 52 
     | 
    
         
            +
                          error: request.error
         
     | 
| 
      
 53 
     | 
    
         
            +
                        )
         
     | 
| 
      
 54 
     | 
    
         
            +
                      )
         
     | 
| 
       38 
55 
     | 
    
         
             
                    end
         
     | 
| 
       39 
56 
     | 
    
         | 
| 
       40 
57 
     | 
    
         
             
                    def track_response(response, _request, oad)
         
     | 
| 
       41 
     | 
    
         
            -
                       
     | 
| 
      
 58 
     | 
    
         
            +
                      return unless response.known?
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                      # The call to `track_response` may happen remotely in the main process that started
         
     | 
| 
      
 61 
     | 
    
         
            +
                      # the coverage collection.
         
     | 
| 
      
 62 
     | 
    
         
            +
                      # To make this work we need to keep arguments trivial, which is the reason the response
         
     | 
| 
      
 63 
     | 
    
         
            +
                      # is wrapped in a CoveredResponse data object.
         
     | 
| 
      
 64 
     | 
    
         
            +
                      tracker&.track_response(
         
     | 
| 
      
 65 
     | 
    
         
            +
                        oad.key,
         
     | 
| 
      
 66 
     | 
    
         
            +
                        CoveredResponse.new(
         
     | 
| 
      
 67 
     | 
    
         
            +
                          key: response.response_definition.key,
         
     | 
| 
      
 68 
     | 
    
         
            +
                          error: response.error
         
     | 
| 
      
 69 
     | 
    
         
            +
                        )
         
     | 
| 
      
 70 
     | 
    
         
            +
                      )
         
     | 
| 
       42 
71 
     | 
    
         
             
                    end
         
     | 
| 
       43 
72 
     | 
    
         | 
| 
       44 
73 
     | 
    
         
             
                    def result
         
     | 
| 
       45 
74 
     | 
    
         
             
                      Result.new(plans:, coverage:)
         
     | 
| 
       46 
75 
     | 
    
         
             
                    end
         
     | 
| 
       47 
76 
     | 
    
         | 
| 
      
 77 
     | 
    
         
            +
                    private
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                    def current_run
         
     | 
| 
      
 80 
     | 
    
         
            +
                      tracker.plans_by_key
         
     | 
| 
      
 81 
     | 
    
         
            +
                    end
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
       48 
83 
     | 
    
         
             
                    # Returns all plans (Plan) that were registered for this run
         
     | 
| 
       49 
84 
     | 
    
         
             
                    def plans
         
     | 
| 
       50 
     | 
    
         
            -
                       
     | 
| 
      
 85 
     | 
    
         
            +
                      tracker&.plans || []
         
     | 
| 
       51 
86 
     | 
    
         
             
                    end
         
     | 
| 
       52 
87 
     | 
    
         | 
| 
       53 
     | 
    
         
            -
                     
     | 
| 
      
 88 
     | 
    
         
            +
                    def tracker
         
     | 
| 
      
 89 
     | 
    
         
            +
                      return unless @drb_uri
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                      @tracker ||= DRbObject.new_with_uri(@drb_uri)
         
     | 
| 
      
 92 
     | 
    
         
            +
                    end
         
     | 
| 
       54 
93 
     | 
    
         | 
| 
       55 
94 
     | 
    
         
             
                    def coverage
         
     | 
| 
       56 
95 
     | 
    
         
             
                      return 0 if plans.empty?
         
     | 
    
        data/lib/openapi_first/test.rb
    CHANGED
    
    | 
         @@ -1,15 +1,15 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            require_relative 'test/configuration'
         
     | 
| 
       4 
     | 
    
         
            -
            require_relative ' 
     | 
| 
      
 4 
     | 
    
         
            +
            require_relative 'registry'
         
     | 
| 
       5 
5 
     | 
    
         | 
| 
       6 
6 
     | 
    
         
             
            module OpenapiFirst
         
     | 
| 
       7 
7 
     | 
    
         
             
              # Test integration
         
     | 
| 
       8 
8 
     | 
    
         
             
              module Test
         
     | 
| 
       9 
9 
     | 
    
         
             
                autoload :Coverage, 'openapi_first/test/coverage'
         
     | 
| 
       10 
10 
     | 
    
         
             
                autoload :Methods, 'openapi_first/test/methods'
         
     | 
| 
       11 
     | 
    
         
            -
                autoload :Callable, 'openapi_first/test/callable'
         
     | 
| 
       12 
11 
     | 
    
         
             
                autoload :Observe, 'openapi_first/test/observe'
         
     | 
| 
      
 12 
     | 
    
         
            +
                autoload :App, 'openapi_first/test/app'
         
     | 
| 
       13 
13 
     | 
    
         
             
                extend Registry
         
     | 
| 
       14 
14 
     | 
    
         | 
| 
       15 
15 
     | 
    
         
             
                class CoverageError < Error; end
         
     | 
| 
         @@ -25,6 +25,10 @@ module OpenapiFirst 
     | 
|
| 
       25 
25 
     | 
    
         
             
                  false
         
     | 
| 
       26 
26 
     | 
    
         
             
                end
         
     | 
| 
       27 
27 
     | 
    
         | 
| 
      
 28 
     | 
    
         
            +
                def self.definitions
         
     | 
| 
      
 29 
     | 
    
         
            +
                  super.empty? ? OpenapiFirst.definitions : super
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
       28 
32 
     | 
    
         
             
                def self.configuration
         
     | 
| 
       29 
33 
     | 
    
         
             
                  @configuration ||= Configuration.new
         
     | 
| 
       30 
34 
     | 
    
         
             
                end
         
     | 
| 
         @@ -32,29 +36,28 @@ module OpenapiFirst 
     | 
|
| 
       32 
36 
     | 
    
         
             
                # Sets up OpenAPI test coverage and OAD registration.
         
     | 
| 
       33 
37 
     | 
    
         
             
                # @yieldparam [OpenapiFirst::Test::Configuration] configuration A configuration to setup test integration
         
     | 
| 
       34 
38 
     | 
    
         
             
                def self.setup
         
     | 
| 
       35 
     | 
    
         
            -
                  unless block_given?
         
     | 
| 
       36 
     | 
    
         
            -
                    raise ArgumentError, "Please provide a block to #{self.class}.confgure to register you API descriptions"
         
     | 
| 
       37 
     | 
    
         
            -
                  end
         
     | 
| 
       38 
     | 
    
         
            -
             
     | 
| 
       39 
39 
     | 
    
         
             
                  install
         
     | 
| 
       40 
     | 
    
         
            -
                  yield configuration
         
     | 
| 
      
 40 
     | 
    
         
            +
                  yield configuration if block_given?
         
     | 
| 
       41 
41 
     | 
    
         | 
| 
       42 
     | 
    
         
            -
                  configuration.registry.each { |name, oad| register(oad, as: name) }
         
     | 
| 
       43 
     | 
    
         
            -
                  configuration.apps.each { |name, apps| apps.each { |app| observe(app, api: name) } }
         
     | 
| 
       44 
42 
     | 
    
         
             
                  Coverage.start(skip_response: configuration.skip_response_coverage, skip_route: configuration.skip_coverage)
         
     | 
| 
       45 
43 
     | 
    
         | 
| 
       46 
44 
     | 
    
         
             
                  if definitions.empty?
         
     | 
| 
       47 
45 
     | 
    
         
             
                    raise NotRegisteredError,
         
     | 
| 
       48 
46 
     | 
    
         
             
                          'No API descriptions have been registered. ' \
         
     | 
| 
       49 
47 
     | 
    
         
             
                          'Please register your API description via ' \
         
     | 
| 
       50 
     | 
    
         
            -
                          "OpenapiFirst 
     | 
| 
      
 48 
     | 
    
         
            +
                          "`OpenapiFirst.register('myopenapi.yaml)` or " \
         
     | 
| 
      
 49 
     | 
    
         
            +
                          'in a block passed to `OpenapiFirst::Test.setup` like this: ' \
         
     | 
| 
      
 50 
     | 
    
         
            +
                          "`OpenapiFirst::Test.setup { |test| test.register('myopenapi.yaml') }` " \
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
       51 
52 
     | 
    
         
             
                  end
         
     | 
| 
       52 
53 
     | 
    
         | 
| 
       53 
54 
     | 
    
         
             
                  @exit_handler = method(:handle_exit)
         
     | 
| 
       54 
55 
     | 
    
         | 
| 
      
 56 
     | 
    
         
            +
                  main_process = Process.pid
         
     | 
| 
       55 
57 
     | 
    
         
             
                  @setup ||= at_exit do
         
     | 
| 
       56 
58 
     | 
    
         
             
                    # :nocov:
         
     | 
| 
       57 
     | 
    
         
            -
                     
     | 
| 
      
 59 
     | 
    
         
            +
                    # Only handle exit once in the main process
         
     | 
| 
      
 60 
     | 
    
         
            +
                    @exit_handler&.call if Process.pid == main_process
         
     | 
| 
       58 
61 
     | 
    
         
             
                    # :nocov:
         
     | 
| 
       59 
62 
     | 
    
         
             
                  end
         
     | 
| 
       60 
63 
     | 
    
         
             
                end
         
     | 
| 
         @@ -88,11 +91,7 @@ module OpenapiFirst 
     | 
|
| 
       88 
91 
     | 
    
         
             
                # the middlewares or manual request, response validation.
         
     | 
| 
       89 
92 
     | 
    
         
             
                def self.app(app, spec: nil, api: :default)
         
     | 
| 
       90 
93 
     | 
    
         
             
                  spec ||= self[api]
         
     | 
| 
       91 
     | 
    
         
            -
                   
     | 
| 
       92 
     | 
    
         
            -
                    use OpenapiFirst::Middlewares::ResponseValidation, spec:, raise_error: false
         
     | 
| 
       93 
     | 
    
         
            -
                    use OpenapiFirst::Middlewares::RequestValidation, spec:, raise_error: false, error_response: false
         
     | 
| 
       94 
     | 
    
         
            -
                    run app
         
     | 
| 
       95 
     | 
    
         
            -
                  end
         
     | 
| 
      
 94 
     | 
    
         
            +
                  App.new(app, api: spec)
         
     | 
| 
       96 
95 
     | 
    
         
             
                end
         
     | 
| 
       97 
96 
     | 
    
         | 
| 
       98 
97 
     | 
    
         
             
                def self.install
         
     | 
| 
         @@ -123,14 +122,14 @@ module OpenapiFirst 
     | 
|
| 
       123 
122 
     | 
    
         
             
                  !configuration.ignore_unknown_requests
         
     | 
| 
       124 
123 
     | 
    
         
             
                end
         
     | 
| 
       125 
124 
     | 
    
         | 
| 
       126 
     | 
    
         
            -
                def self.raise_response_error?( 
     | 
| 
       127 
     | 
    
         
            -
                  configuration.response_raise_error && !configuration. 
     | 
| 
      
 125 
     | 
    
         
            +
                def self.raise_response_error?(invalid_response)
         
     | 
| 
      
 126 
     | 
    
         
            +
                  configuration.response_raise_error && !configuration.ignore_response?(invalid_response)
         
     | 
| 
       128 
127 
     | 
    
         
             
                end
         
     | 
| 
       129 
128 
     | 
    
         | 
| 
       130 
129 
     | 
    
         
             
                def self.uninstall
         
     | 
| 
       131 
130 
     | 
    
         
             
                  configuration = OpenapiFirst.configuration
         
     | 
| 
       132 
     | 
    
         
            -
                  configuration. 
     | 
| 
       133 
     | 
    
         
            -
                  configuration. 
     | 
| 
      
 131 
     | 
    
         
            +
                  configuration.after_request_validation.delete(@after_request_validation)
         
     | 
| 
      
 132 
     | 
    
         
            +
                  configuration.after_response_validation.delete(@after_response_validation)
         
     | 
| 
       134 
133 
     | 
    
         
             
                  definitions.clear
         
     | 
| 
       135 
134 
     | 
    
         
             
                  @configuration = nil
         
     | 
| 
       136 
135 
     | 
    
         
             
                  @installed = nil
         
     | 
| 
         @@ -24,14 +24,16 @@ module OpenapiFirst 
     | 
|
| 
       24 
24 
     | 
    
         
             
                attr_reader :response_definition
         
     | 
| 
       25 
25 
     | 
    
         | 
| 
       26 
26 
     | 
    
         
             
                # The parsed headers
         
     | 
| 
       27 
     | 
    
         
            -
                #  
     | 
| 
       28 
     | 
    
         
            -
                 
     | 
| 
       29 
     | 
    
         
            -
             
     | 
| 
      
 27 
     | 
    
         
            +
                # @return [Hash<String,anything>, nil]
         
     | 
| 
      
 28 
     | 
    
         
            +
                def parsed_headers
         
     | 
| 
      
 29 
     | 
    
         
            +
                  @parsed_values&.headers
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
       30 
31 
     | 
    
         | 
| 
       31 
32 
     | 
    
         
             
                # The parsed body
         
     | 
| 
       32 
     | 
    
         
            -
                #  
     | 
| 
       33 
     | 
    
         
            -
                 
     | 
| 
       34 
     | 
    
         
            -
             
     | 
| 
      
 33 
     | 
    
         
            +
                # @return [Hash<String,anything>, nil]
         
     | 
| 
      
 34 
     | 
    
         
            +
                def parsed_body
         
     | 
| 
      
 35 
     | 
    
         
            +
                  @parsed_values&.body
         
     | 
| 
      
 36 
     | 
    
         
            +
                end
         
     | 
| 
       35 
37 
     | 
    
         | 
| 
       36 
38 
     | 
    
         
             
                # Checks if the response is valid.
         
     | 
| 
       37 
39 
     | 
    
         
             
                # @return [Boolean] true if the response is valid, false otherwise.
         
     | 
    
        data/lib/openapi_first.rb
    CHANGED
    
    | 
         @@ -3,7 +3,9 @@ 
     | 
|
| 
       3 
3 
     | 
    
         
             
            require_relative 'openapi_first/json'
         
     | 
| 
       4 
4 
     | 
    
         
             
            require_relative 'openapi_first/file_loader'
         
     | 
| 
       5 
5 
     | 
    
         
             
            require_relative 'openapi_first/errors'
         
     | 
| 
      
 6 
     | 
    
         
            +
            require_relative 'openapi_first/registry'
         
     | 
| 
       6 
7 
     | 
    
         
             
            require_relative 'openapi_first/configuration'
         
     | 
| 
      
 8 
     | 
    
         
            +
            require_relative 'openapi_first/child_configuration'
         
     | 
| 
       7 
9 
     | 
    
         
             
            require_relative 'openapi_first/definition'
         
     | 
| 
       8 
10 
     | 
    
         
             
            require_relative 'openapi_first/version'
         
     | 
| 
       9 
11 
     | 
    
         
             
            require_relative 'openapi_first/middlewares/response_validation'
         
     | 
| 
         @@ -11,6 +13,8 @@ require_relative 'openapi_first/middlewares/request_validation' 
     | 
|
| 
       11 
13 
     | 
    
         | 
| 
       12 
14 
     | 
    
         
             
            # OpenapiFirst is a toolchain to build HTTP APIS based on OpenAPI API descriptions.
         
     | 
| 
       13 
15 
     | 
    
         
             
            module OpenapiFirst
         
     | 
| 
      
 16 
     | 
    
         
            +
              extend Registry
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
       14 
18 
     | 
    
         
             
              autoload :Test, 'openapi_first/test'
         
     | 
| 
       15 
19 
     | 
    
         | 
| 
       16 
20 
     | 
    
         
             
              # Key in rack to find instance of Request
         
     | 
| 
         @@ -26,7 +30,7 @@ module OpenapiFirst 
     | 
|
| 
       26 
30 
     | 
    
         
             
              # @return [Configuration]
         
     | 
| 
       27 
31 
     | 
    
         
             
              # @yield [Configuration]
         
     | 
| 
       28 
32 
     | 
    
         
             
              def self.configure
         
     | 
| 
       29 
     | 
    
         
            -
                yield configuration
         
     | 
| 
      
 33 
     | 
    
         
            +
                yield configuration if block_given?
         
     | 
| 
       30 
34 
     | 
    
         
             
              end
         
     | 
| 
       31 
35 
     | 
    
         | 
| 
       32 
36 
     | 
    
         
             
              ERROR_RESPONSES = {} # rubocop:disable Style/MutableConstant
         
     | 
| 
         @@ -54,6 +58,7 @@ module OpenapiFirst 
     | 
|
| 
       54 
58 
     | 
    
         
             
              # @return [Definition]
         
     | 
| 
       55 
59 
     | 
    
         
             
              def self.load(filepath_or_definition, only: nil, &)
         
     | 
| 
       56 
60 
     | 
    
         
             
                return filepath_or_definition if filepath_or_definition.is_a?(Definition)
         
     | 
| 
      
 61 
     | 
    
         
            +
                return self[filepath_or_definition] if filepath_or_definition.is_a?(Symbol)
         
     | 
| 
       57 
62 
     | 
    
         | 
| 
       58 
63 
     | 
    
         
             
                filepath = filepath_or_definition
         
     | 
| 
       59 
64 
     | 
    
         
             
                raise FileNotFoundError, "File not found: #{filepath}" unless File.exist?(filepath)
         
     | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: openapi_first
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version:  
     | 
| 
      
 4 
     | 
    
         
            +
              version: 3.0.0
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - Andreas Haller
         
     | 
| 
         @@ -49,7 +49,7 @@ dependencies: 
     | 
|
| 
       49 
49 
     | 
    
         
             
                requirements:
         
     | 
| 
       50 
50 
     | 
    
         
             
                - - ">="
         
     | 
| 
       51 
51 
     | 
    
         
             
                  - !ruby/object:Gem::Version
         
     | 
| 
       52 
     | 
    
         
            -
                    version: 0. 
     | 
| 
      
 52 
     | 
    
         
            +
                    version: 0.7.0
         
     | 
| 
       53 
53 
     | 
    
         
             
                - - "<"
         
     | 
| 
       54 
54 
     | 
    
         
             
                  - !ruby/object:Gem::Version
         
     | 
| 
       55 
55 
     | 
    
         
             
                    version: '2.0'
         
     | 
| 
         @@ -59,7 +59,7 @@ dependencies: 
     | 
|
| 
       59 
59 
     | 
    
         
             
                requirements:
         
     | 
| 
       60 
60 
     | 
    
         
             
                - - ">="
         
     | 
| 
       61 
61 
     | 
    
         
             
                  - !ruby/object:Gem::Version
         
     | 
| 
       62 
     | 
    
         
            -
                    version: 0. 
     | 
| 
      
 62 
     | 
    
         
            +
                    version: 0.7.0
         
     | 
| 
       63 
63 
     | 
    
         
             
                - - "<"
         
     | 
| 
       64 
64 
     | 
    
         
             
                  - !ruby/object:Gem::Version
         
     | 
| 
       65 
65 
     | 
    
         
             
                    version: '2.0'
         
     | 
| 
         @@ -94,6 +94,7 @@ files: 
     | 
|
| 
       94 
94 
     | 
    
         
             
            - README.md
         
     | 
| 
       95 
95 
     | 
    
         
             
            - lib/openapi_first.rb
         
     | 
| 
       96 
96 
     | 
    
         
             
            - lib/openapi_first/builder.rb
         
     | 
| 
      
 97 
     | 
    
         
            +
            - lib/openapi_first/child_configuration.rb
         
     | 
| 
       97 
98 
     | 
    
         
             
            - lib/openapi_first/configuration.rb
         
     | 
| 
       98 
99 
     | 
    
         
             
            - lib/openapi_first/definition.rb
         
     | 
| 
       99 
100 
     | 
    
         
             
            - lib/openapi_first/error_response.rb
         
     | 
| 
         @@ -107,6 +108,7 @@ files: 
     | 
|
| 
       107 
108 
     | 
    
         
             
            - lib/openapi_first/middlewares/request_validation.rb
         
     | 
| 
       108 
109 
     | 
    
         
             
            - lib/openapi_first/middlewares/response_validation.rb
         
     | 
| 
       109 
110 
     | 
    
         
             
            - lib/openapi_first/ref_resolver.rb
         
     | 
| 
      
 111 
     | 
    
         
            +
            - lib/openapi_first/registry.rb
         
     | 
| 
       110 
112 
     | 
    
         
             
            - lib/openapi_first/request.rb
         
     | 
| 
       111 
113 
     | 
    
         
             
            - lib/openapi_first/request_body_parsers.rb
         
     | 
| 
       112 
114 
     | 
    
         
             
            - lib/openapi_first/request_parser.rb
         
     | 
| 
         @@ -123,20 +125,23 @@ files: 
     | 
|
| 
       123 
125 
     | 
    
         
             
            - lib/openapi_first/schema/validation_error.rb
         
     | 
| 
       124 
126 
     | 
    
         
             
            - lib/openapi_first/schema/validation_result.rb
         
     | 
| 
       125 
127 
     | 
    
         
             
            - lib/openapi_first/test.rb
         
     | 
| 
      
 128 
     | 
    
         
            +
            - lib/openapi_first/test/app.rb
         
     | 
| 
       126 
129 
     | 
    
         
             
            - lib/openapi_first/test/callable.rb
         
     | 
| 
       127 
130 
     | 
    
         
             
            - lib/openapi_first/test/configuration.rb
         
     | 
| 
       128 
131 
     | 
    
         
             
            - lib/openapi_first/test/coverage.rb
         
     | 
| 
      
 132 
     | 
    
         
            +
            - lib/openapi_first/test/coverage/covered_request.rb
         
     | 
| 
      
 133 
     | 
    
         
            +
            - lib/openapi_first/test/coverage/covered_response.rb
         
     | 
| 
       129 
134 
     | 
    
         
             
            - lib/openapi_first/test/coverage/plan.rb
         
     | 
| 
       130 
135 
     | 
    
         
             
            - lib/openapi_first/test/coverage/request_task.rb
         
     | 
| 
       131 
136 
     | 
    
         
             
            - lib/openapi_first/test/coverage/response_task.rb
         
     | 
| 
       132 
137 
     | 
    
         
             
            - lib/openapi_first/test/coverage/route_task.rb
         
     | 
| 
       133 
138 
     | 
    
         
             
            - lib/openapi_first/test/coverage/terminal_formatter.rb
         
     | 
| 
      
 139 
     | 
    
         
            +
            - lib/openapi_first/test/coverage/tracker.rb
         
     | 
| 
       134 
140 
     | 
    
         
             
            - lib/openapi_first/test/methods.rb
         
     | 
| 
       135 
141 
     | 
    
         
             
            - lib/openapi_first/test/minitest_helpers.rb
         
     | 
| 
       136 
142 
     | 
    
         
             
            - lib/openapi_first/test/observe.rb
         
     | 
| 
       137 
143 
     | 
    
         
             
            - lib/openapi_first/test/observer_middleware.rb
         
     | 
| 
       138 
144 
     | 
    
         
             
            - lib/openapi_first/test/plain_helpers.rb
         
     | 
| 
       139 
     | 
    
         
            -
            - lib/openapi_first/test/registry.rb
         
     | 
| 
       140 
145 
     | 
    
         
             
            - lib/openapi_first/validated_request.rb
         
     | 
| 
       141 
146 
     | 
    
         
             
            - lib/openapi_first/validated_response.rb
         
     | 
| 
       142 
147 
     | 
    
         
             
            - lib/openapi_first/validators/request_body.rb
         
     | 
| 
         @@ -1,44 +0,0 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            # frozen_string_literal: true
         
     | 
| 
       2 
     | 
    
         
            -
             
     | 
| 
       3 
     | 
    
         
            -
            module OpenapiFirst
         
     | 
| 
       4 
     | 
    
         
            -
              module Test
         
     | 
| 
       5 
     | 
    
         
            -
                class NotRegisteredError < Error; end
         
     | 
| 
       6 
     | 
    
         
            -
                class AlreadyRegisteredError < Error; end
         
     | 
| 
       7 
     | 
    
         
            -
             
     | 
| 
       8 
     | 
    
         
            -
                # @visibility private
         
     | 
| 
       9 
     | 
    
         
            -
                module Registry
         
     | 
| 
       10 
     | 
    
         
            -
                  def definitions
         
     | 
| 
       11 
     | 
    
         
            -
                    @definitions ||= {}
         
     | 
| 
       12 
     | 
    
         
            -
                  end
         
     | 
| 
       13 
     | 
    
         
            -
             
     | 
| 
       14 
     | 
    
         
            -
                  # Register an OpenAPI definition for testing
         
     | 
| 
       15 
     | 
    
         
            -
                  # @param path_or_definition [String, Definition] Path to the OpenAPI file or a Definition object
         
     | 
| 
       16 
     | 
    
         
            -
                  # @param as [Symbol] Name to register the API definition as
         
     | 
| 
       17 
     | 
    
         
            -
                  def register(path_or_definition, as: :default)
         
     | 
| 
       18 
     | 
    
         
            -
                    if definitions.key?(as) && as == :default
         
     | 
| 
       19 
     | 
    
         
            -
                      raise(
         
     | 
| 
       20 
     | 
    
         
            -
                        AlreadyRegisteredError,
         
     | 
| 
       21 
     | 
    
         
            -
                        "#{definitions[as].filepath.inspect} is already registered " \
         
     | 
| 
       22 
     | 
    
         
            -
                        "as ':default' so you cannot register #{path_or_definition.inspect} without " \
         
     | 
| 
       23 
     | 
    
         
            -
                        'giving it a custom name. Please call register with a custom key like: ' \
         
     | 
| 
       24 
     | 
    
         
            -
                        "#{name}.register(#{path_or_definition.inspect}, as: :my_other_api)"
         
     | 
| 
       25 
     | 
    
         
            -
                      )
         
     | 
| 
       26 
     | 
    
         
            -
                    end
         
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
       28 
     | 
    
         
            -
                    definition = OpenapiFirst.load(path_or_definition)
         
     | 
| 
       29 
     | 
    
         
            -
                    definitions[as] = definition
         
     | 
| 
       30 
     | 
    
         
            -
                    definition
         
     | 
| 
       31 
     | 
    
         
            -
                  end
         
     | 
| 
       32 
     | 
    
         
            -
             
     | 
| 
       33 
     | 
    
         
            -
                  def [](api)
         
     | 
| 
       34 
     | 
    
         
            -
                    definitions.fetch(api) do
         
     | 
| 
       35 
     | 
    
         
            -
                      option = api == :default ? '' : ", as: #{api.inspect}"
         
     | 
| 
       36 
     | 
    
         
            -
                      raise(NotRegisteredError,
         
     | 
| 
       37 
     | 
    
         
            -
                            "API description '#{api.inspect}' not found." \
         
     | 
| 
       38 
     | 
    
         
            -
                            "Please call #{name}.register('myopenapi.yaml'#{option}) " \
         
     | 
| 
       39 
     | 
    
         
            -
                            'once before running tests.')
         
     | 
| 
       40 
     | 
    
         
            -
                    end
         
     | 
| 
       41 
     | 
    
         
            -
                  end
         
     | 
| 
       42 
     | 
    
         
            -
                end
         
     | 
| 
       43 
     | 
    
         
            -
              end
         
     | 
| 
       44 
     | 
    
         
            -
            end
         
     |