openapi_first 2.11.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c84ae6fc48433fbbe1845ff66ed2315b58a47ff3ba17ea6756bb6c276db5ed3
4
- data.tar.gz: 3b584da97ce9b7ea712dc66ac55e39a16c1631254dd719551d27a4df9bc13e4c
3
+ metadata.gz: 7474086c282336d7e77afe8143a0bda6e0a4a36fe284a746804bd933bdfd45a6
4
+ data.tar.gz: 7538b4bd0e9f30f3d95c5cb06842b03e15644ce441018577c467629ce62d9772
5
5
  SHA512:
6
- metadata.gz: 863d5ea4124958e81d62e7a72efef17e6fe5326fe4db6d34bc6fe97e8ec863c09dd8d138eaf9b249ce94ce490dad937297cd9d15b98dc73a6c01f30c6f6da2f7
7
- data.tar.gz: 7c5c264ce6f969788986bbaf71cb6060b998ae5819cf5551ddf4f82ab2069c87afda22e97fa9c8aa03ea91bb6133cbf579d63b35a12c0eb30a7e17a774508ec7
6
+ metadata.gz: 2d1469a59f294ce4b386fb60da3e7c019773a8516005d1b3c0f60bd26158e62a879367647cab558579085a3b3c866055ac70b3df255c2fb38d7d918e10892154
7
+ data.tar.gz: 835f24859d1510dfbe6a3fa99f1d9a496a0011441b041f8d92ee884b5938d53b5550fbe336a36adf44e6f8711fc30ec5333a7310de65cc93da8402e107b6beb4
data/CHANGELOG.md CHANGED
@@ -2,6 +2,52 @@
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
+
5
51
  ## 2.11.1
6
52
 
7
53
  - OpenapiFirst can now route requests correctly for paths like `/stuffs` and `/stuffs{format}` (https://github.com/ahx/openapi_first/issues/386)
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, 'openapi/openapi.yaml'
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 do |config|
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, 'openapi.yaml'
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, 'openapi.yaml', raise_error: true
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, 'openapi.yaml' if ENV['RACK_ENV'] == 'test'
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, 'openapi.yaml', raise_error: false
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,13 +189,11 @@ 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. Register all OpenAPI documents to track coverage for.
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 do |config|
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
199
  - Add an `app` method to your tests (which is called by rack-test) that wraps your application with silent request / response validation.
@@ -188,7 +216,7 @@ Here is how to set it up:
188
216
  config.include OpenapiFirst::Test::Methods[MyApp], type: :request
189
217
  end
190
218
  ```
191
- 4. Run your tests. The Coverage feature will tell you about missing or invalid requests/responses:
219
+ 3. Run your tests. The Coverage feature will tell you about missing or invalid requests/responses:
192
220
  ```
193
221
  ✓ GET /stations
194
222
  ✓ 200(application/json)
@@ -203,11 +231,19 @@ Here is how to set it up:
203
231
 
204
232
  ### Configure test coverage
205
233
 
206
- OpenapiFirst::Test raises an error when a response status is not defined. You can deactivate this with:
234
+ OpenapiFirst::Test raises an error when a response status is not defined except for 404 and 500. You can change this:
207
235
 
208
236
  ```ruby
209
237
  OpenapiFirst::Test.setup do |test|
210
- [403, 401].each { test.ignored_unknown_status << it }
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
211
247
  end
212
248
  ```
213
249
 
@@ -215,7 +251,6 @@ Exclude certain _responses_ from coverage with `skip_coverage`:
215
251
 
216
252
  ```ruby
217
253
  OpenapiFirst::Test.setup do |test|
218
- # …
219
254
  test.skip_response_coverage do |response_definition|
220
255
  response_definition.status == '5XX'
221
256
  end
@@ -226,7 +261,6 @@ Skip coverage for a request and all responses alltogether of a route with `skip_
226
261
 
227
262
  ```ruby
228
263
  OpenapiFirst::Test.setup do |test|
229
- # …
230
264
  test.skip_coverage do |path, request_method|
231
265
  path == '/bookings/{bookingId}' && requests_method == 'DELETE'
232
266
  end
@@ -237,32 +271,36 @@ OpenapiFirst::Test raises an error when a request is not defined. You can deacti
237
271
 
238
272
  ```ruby
239
273
  OpenapiFirst::Test.setup do |test|
240
- # …
241
274
  test.ignore_unknown_requests = true
242
275
  end
243
276
  ```
244
277
 
245
278
  ### Test assertions
246
279
 
247
- openapi_first ships with a simple but powerful Test method to run request and response validation in your tests without using the middlewares. This is designed to be used with rack-test or Ruby on Rails integration tests or request specs.
280
+ > [!WARNING]
281
+ > You probably don't need this. Just setup [Contract testing and API coverage](#contract-testing) and use your normal assertions.
248
282
 
249
- Here is how to set it up for Rails integration tests:
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.
284
+
285
+ Here is how to use it with RSpec, but MiniTest works just as good:
250
286
 
251
- Inside your test:
252
287
  ```ruby
253
- # test/integration/trips_api_test.rb
254
- require 'test_helper'
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
+ ```
255
293
 
256
- class TripsApiTest < ActionDispatch::IntegrationTest
257
- include OpenapiFirst::Test::Methods
258
294
 
259
- test 'GET /trips' do
260
- get '/trips',
261
- params: { origin: 'efdbb9d1-02c2-4bc3-afb7-6788d8782b1e', destination: 'b2e783e1-c824-4d63-b37a-d8d698862f1d',
262
- date: '2024-07-02T09:00:00Z' }
295
+ Inside your test :
296
+ ```ruby
297
+ RSpec.describe 'Example App' do
298
+ include Rack::Test::Methods
299
+ include OpenapiFirst::Test::Methods[App]
263
300
 
301
+ it 'is API conform' do
302
+ get '/'
264
303
  assert_api_conform(status: 200)
265
- # assert_api_conform(status: 200, api: :v1) # Or this if you have multiple API descriptions
266
304
  end
267
305
  end
268
306
  ```
@@ -320,27 +358,6 @@ validated_response.parsed_headers
320
358
  definition.validate_response(rack_request,rack_response, raise_error: true) # Raises OpenapiFirst::ResponseInvalidError or OpenapiFirst::ResponseNotFoundError
321
359
  ```
322
360
 
323
- ## Configuration
324
-
325
- You can configure default options globally:
326
-
327
- ```ruby
328
- OpenapiFirst.configure do |config|
329
- # Specify which plugin is used to render error responses returned by the request validation middleware (defaults to :default)
330
- config.request_validation_error_response = :jsonapi
331
- # Configure if the request validation middleware should raise an exception (defaults to false)
332
- config.request_validation_raise_error = true
333
- end
334
- ```
335
-
336
- or configure per instance:
337
-
338
- ```ruby
339
- OpenapiFirst.load('openapi.yaml') do |config|
340
- config.request_validation_error_response = :jsonapi
341
- end
342
- ```
343
-
344
361
  ## Hooks
345
362
 
346
363
  You can integrate your code at certain points during request/response validation via hooks.
@@ -380,11 +397,10 @@ end
380
397
  ## Framework integration
381
398
 
382
399
  Using rack middlewares is supported in probably all Ruby web frameworks.
383
- 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.
384
400
 
385
401
  The contract testing feature is designed to be used via rack-test, which should be compatible all Ruby web frameworks as well.
386
402
 
387
- That aside, closer integration with specific frameworks like Sinatra, Hanami, Roda or Rails would be great. If you have ideas, pain points or PRs, please don't hesitate to [share](https://github.com/ahx/openapi_first/discussions).
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).
388
404
 
389
405
  ## Alternatives
390
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.hooks[:after_request_parameter_property_validation])
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.hooks[:after_request_body_property_validation]
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
@@ -18,17 +18,48 @@ module OpenapiFirst
18
18
  @path = nil
19
19
  end
20
20
 
21
- attr_reader :request_validation_error_response, :hooks
22
- attr_accessor :request_validation_raise_error, :response_validation_raise_error, :path
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
- copy = super
26
- copy.instance_variable_set(:@hooks, @hooks&.transform_values(&:clone))
27
- copy
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.clone
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.hooks[:after_request_validation].each { |hook| hook.call(validated, self) }
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.hooks[:after_response_validation]&.each { |hook| hook.call(validated, rack_request, self) }
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
- response_not_found: [ResponseNotFoundError],
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.fetch(:spec)
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 ||= options.fetch(:spec)
29
- raise "You have to pass spec: when initializing #{self.class}" unless spec
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.fetch(:spec)
20
+ spec = options[:spec]
19
21
  end
20
22
  @raise = options.fetch(:raise_error, OpenapiFirst.configuration.response_validation_raise_error)
21
23
 
22
- raise "You have to pass spec: when initializing #{self.class}" unless spec
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
- message = "Status #{status} is not defined for #{request_method.upcase} #{path}. " \
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 response_not_found(content_type:, contents:, request_method:, path:) unless response
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.response_not_found(content_type:, contents:, request_method:, path:)
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(:response_not_found, message:),
29
+ error: Failure.new(:response_content_type_not_found, message:),
33
30
  response: nil
34
31
  )
35
32
  end
36
- private_class_method :response_not_found
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,
@@ -41,7 +41,7 @@ module OpenapiFirst
41
41
  part.start_with?('{') ? ALLOWED_PARAMETER_CHARACTERS : Regexp.escape(part)
42
42
  end
43
43
 
44
- %r{^#{parts.join}/?$}
44
+ /^#{parts.join}$/
45
45
  end
46
46
  end
47
47
  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 this like `Application.prepend(OpenapiFirst::Test.app_module)` to monitor your app during testing.
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
- @registry[as] = oad
24
+ Test.register(oad, as:)
26
25
  end
27
26
 
28
27
  # Observe a rack app
29
28
  def observe(app, api: :default)
30
- (@apps[api] ||= []) << app
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 :registry, :apps, :report_coverage, :ignored_unknown_status, :minimum_coverage
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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ module Test
5
+ module Coverage
6
+ CoveredRequest = Data.define(:key, :error) do
7
+ def valid? = error.nil?
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ module Test
5
+ module Coverage
6
+ CoveredResponse = Data.define(:key, :error) do
7
+ def valid? = error.nil?
8
+ end
9
+ end
10
+ end
11
+ 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.request_definition.key]&.track(validated_request) if validated_request.known?
40
+ index[validated_request.key]&.track(validated_request)
41
41
  end
42
42
 
43
43
  def track_response(validated_response)
44
- index[validated_response.response_definition.key]&.track(validated_response) if validated_response.known?
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
- attr_reader :current_run
20
+ def start(skip_response: nil, skip_route: nil)
21
+ return if @drb_uri
19
22
 
20
- def install = Test.install
23
+ tracker = Tracker.new(Test.definitions, skip_response:, skip_route:)
21
24
 
22
- def start(skip_response: nil, skip_route: nil)
23
- @current_run = Test.definitions.values.to_h do |oad|
24
- plan = Plan.for(oad, skip_response:, skip_route:)
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
- @current_run = {}
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
- current_run[oad.key]&.track_request(request)
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
- current_run[oad.key]&.track_response(response)
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
- current_run.values
85
+ tracker&.plans || []
51
86
  end
52
87
 
53
- private
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?
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'callable'
4
+
3
5
  module OpenapiFirst
4
6
  module Test
5
7
  class ObserveError < Error; end
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'test/configuration'
4
- require_relative 'test/registry'
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::Test.setup { |test| test.register('myopenapi.yaml') }"
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
- @exit_handler&.call
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
- Rack::Builder.app do
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?(validated_response)
127
- configuration.response_raise_error && !configuration.ignored_unknown_status.include?(validated_response.status)
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.hooks[:after_request_validation].delete(@after_request_validation)
133
- configuration.hooks[:after_response_validation].delete(@after_response_validation)
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
- # @!method parsed_headers
28
- # @return [Hash<String,anything>]
29
- def_delegator :@parsed_values, :headers, :parsed_headers
27
+ # @return [Hash<String,anything>, nil]
28
+ def parsed_headers
29
+ @parsed_values&.headers
30
+ end
30
31
 
31
32
  # The parsed body
32
- # @!method parsed_body
33
- # @return [Hash<String,anything>]
34
- def_delegator :@parsed_values, :body, :parsed_body
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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '2.11.1'
4
+ VERSION = '3.0.0'
5
5
  end
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: 2.11.1
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.6.1
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.6.1
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