openapi-ruby 4.0.3 → 4.2.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/README.md +277 -10
- data/Rakefile +32 -0
- data/app/controllers/openapi_ruby/schemas_controller.rb +4 -45
- data/app/controllers/openapi_ruby/ui_controller.rb +11 -68
- data/lib/openapi_ruby/adapters/context_resolution.rb +113 -0
- data/lib/openapi_ruby/adapters/minitest.rb +54 -20
- data/lib/openapi_ruby/adapters/rspec.rb +58 -28
- data/lib/openapi_ruby/components/loader.rb +15 -0
- data/lib/openapi_ruby/configuration.rb +20 -1
- data/lib/openapi_ruby/engine.rb +1 -38
- data/lib/openapi_ruby/errors.rb +7 -0
- data/lib/openapi_ruby/generator/rake_task_support.rb +43 -1
- data/lib/openapi_ruby/hanami.rb +69 -0
- data/lib/openapi_ruby/host.rb +53 -0
- data/lib/openapi_ruby/middleware/installer.rb +50 -0
- data/lib/openapi_ruby/rack_app.rb +94 -0
- data/lib/openapi_ruby/rake_tasks.rb +39 -0
- data/lib/openapi_ruby/serving.rb +135 -0
- data/lib/openapi_ruby/testing/transport.rb +99 -0
- data/lib/openapi_ruby/version.rb +1 -1
- data/lib/openapi_ruby.rb +5 -0
- data/lib/tasks/openapi_ruby.rake +1 -19
- metadata +14 -20
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "openapi_ruby"
|
|
4
|
+
require_relative "context_resolution"
|
|
4
5
|
require "cgi"
|
|
5
6
|
require "uri"
|
|
6
7
|
|
|
@@ -12,6 +13,21 @@ module OpenapiRuby
|
|
|
12
13
|
base.extend ClassMethods
|
|
13
14
|
base.class_attribute :_openapi_contexts, default: []
|
|
14
15
|
base.class_attribute :_openapi_schema_name, default: nil
|
|
16
|
+
|
|
17
|
+
install_rack_test!(base)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# On Rails the test class already inherits ActionDispatch's integration
|
|
21
|
+
# helpers. Every other host drives requests through rack-test, and the
|
|
22
|
+
# class defines `app` itself.
|
|
23
|
+
def self.install_rack_test!(base)
|
|
24
|
+
return if OpenapiRuby.rails_host?
|
|
25
|
+
return if base.method_defined?(:last_response)
|
|
26
|
+
|
|
27
|
+
require "rack/test"
|
|
28
|
+
base.include ::Rack::Test::Methods
|
|
29
|
+
rescue LoadError
|
|
30
|
+
nil
|
|
15
31
|
end
|
|
16
32
|
|
|
17
33
|
module ClassMethods
|
|
@@ -20,16 +36,30 @@ module OpenapiRuby
|
|
|
20
36
|
end
|
|
21
37
|
|
|
22
38
|
def api_path(template, &block)
|
|
39
|
+
guard_single_api_path!(template)
|
|
40
|
+
|
|
23
41
|
context = OpenapiRuby::DSL::Context.new(template, schema_name: _openapi_schema_name)
|
|
24
42
|
context.instance_eval(&block) if block
|
|
25
43
|
self._openapi_contexts = _openapi_contexts + [context]
|
|
26
44
|
OpenapiRuby::DSL::MetadataStore.register(context)
|
|
27
45
|
context
|
|
28
46
|
end
|
|
47
|
+
|
|
48
|
+
private def guard_single_api_path!(template)
|
|
49
|
+
return unless OpenapiRuby.configuration.single_api_path_per_class
|
|
50
|
+
|
|
51
|
+
existing = _openapi_contexts.first
|
|
52
|
+
return if existing.nil?
|
|
53
|
+
|
|
54
|
+
raise OpenapiRuby::MultipleApiPaths,
|
|
55
|
+
"#{name || self} already declares api_path #{existing.path_template.inspect}; " \
|
|
56
|
+
"declare #{template.inspect} in its own class."
|
|
57
|
+
end
|
|
29
58
|
end
|
|
30
59
|
|
|
31
|
-
def assert_api_response(method, expected_status, params: {}, headers: {}, body: nil, path_params: {},
|
|
32
|
-
|
|
60
|
+
def assert_api_response(method, expected_status, params: {}, headers: {}, body: nil, path_params: {},
|
|
61
|
+
api_path: nil, &block)
|
|
62
|
+
context = find_context_for(method, path_params, params, expected_status, api_path)
|
|
33
63
|
raise OpenapiRuby::Error, "No api_path defined for #{method.upcase} in #{self.class}" unless context
|
|
34
64
|
|
|
35
65
|
operation = context.operations[method.to_s]
|
|
@@ -96,18 +126,18 @@ module OpenapiRuby
|
|
|
96
126
|
assert req_errors.empty?, "Request validation failed:\n#{req_errors.join("\n")}"
|
|
97
127
|
end
|
|
98
128
|
|
|
99
|
-
|
|
129
|
+
openapi_transport.dispatch(method, path, **request_args)
|
|
100
130
|
|
|
101
131
|
# Validate response
|
|
102
|
-
assert_equal expected_status,
|
|
103
|
-
"Expected status #{expected_status}, got #{
|
|
132
|
+
assert_equal expected_status, openapi_response.status,
|
|
133
|
+
"Expected status #{expected_status}, got #{openapi_response.status}\nResponse body: #{openapi_response.body}"
|
|
104
134
|
|
|
105
135
|
if response_ctx.schema_definition
|
|
106
136
|
validator = Testing::ResponseValidator.new
|
|
107
137
|
body_data = parse_response_body
|
|
108
138
|
errors = validator.validate(
|
|
109
139
|
response_body: body_data,
|
|
110
|
-
status_code:
|
|
140
|
+
status_code: openapi_response.status,
|
|
111
141
|
response_context: response_ctx
|
|
112
142
|
)
|
|
113
143
|
assert errors.empty?, "Response validation failed:\n#{errors.join("\n")}"
|
|
@@ -121,20 +151,24 @@ module OpenapiRuby
|
|
|
121
151
|
parse_response_body
|
|
122
152
|
end
|
|
123
153
|
|
|
124
|
-
|
|
154
|
+
# The seam between the DSL and the host's request API. Public so specs
|
|
155
|
+
# that drive requests themselves (rate limiting, pagination loops) can
|
|
156
|
+
# reach the same dispatcher and response the assertions use.
|
|
157
|
+
def openapi_transport
|
|
158
|
+
@openapi_transport ||= Testing::Transport.for(self)
|
|
159
|
+
end
|
|
125
160
|
|
|
126
|
-
def
|
|
127
|
-
|
|
161
|
+
def openapi_response
|
|
162
|
+
openapi_transport.response
|
|
163
|
+
end
|
|
128
164
|
|
|
129
|
-
|
|
130
|
-
next false unless ctx.operations.key?(method.to_s)
|
|
165
|
+
private
|
|
131
166
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
end
|
|
167
|
+
def find_context_for(method, path_params, params, expected_status, api_path)
|
|
168
|
+
OpenapiRuby::Adapters::ContextResolution.resolve(
|
|
169
|
+
self.class._openapi_contexts, method, path_params,
|
|
170
|
+
params: params, expected_status: expected_status, api_path: api_path, owner: self.class
|
|
171
|
+
)
|
|
138
172
|
end
|
|
139
173
|
|
|
140
174
|
def expand_path(template, params)
|
|
@@ -214,11 +248,11 @@ module OpenapiRuby
|
|
|
214
248
|
end
|
|
215
249
|
|
|
216
250
|
def parse_response_body
|
|
217
|
-
return nil if
|
|
251
|
+
return nil if openapi_response.body.empty?
|
|
218
252
|
|
|
219
|
-
JSON.parse(
|
|
253
|
+
JSON.parse(openapi_response.body)
|
|
220
254
|
rescue JSON::ParserError
|
|
221
|
-
|
|
255
|
+
openapi_response.body
|
|
222
256
|
end
|
|
223
257
|
end
|
|
224
258
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "openapi_ruby"
|
|
4
|
+
require_relative "context_resolution"
|
|
4
5
|
require "cgi"
|
|
5
6
|
require "uri"
|
|
6
7
|
|
|
@@ -21,8 +22,11 @@ module OpenapiRuby
|
|
|
21
22
|
schema_name = metadata[:openapi_schema_name]
|
|
22
23
|
context = DSL::Context.new(template, schema_name: schema_name)
|
|
23
24
|
context.instance_eval(&block) if block
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
# Replace rather than push: RSpec copies parent metadata into child
|
|
26
|
+
# groups by reference, so mutating the array in place would leak this
|
|
27
|
+
# declaration back up to the parent and sideways to its siblings.
|
|
28
|
+
# Building a new one is what makes a nested describe an actual scope.
|
|
29
|
+
metadata[:openapi_api_contexts] = (metadata[:openapi_api_contexts] || []) + [context]
|
|
26
30
|
DSL::MetadataStore.register(context)
|
|
27
31
|
context
|
|
28
32
|
end
|
|
@@ -123,9 +127,10 @@ module OpenapiRuby
|
|
|
123
127
|
# Minitest-style assertion: looks up the api_path context, makes the
|
|
124
128
|
# request, validates the response status + body, then yields to the
|
|
125
129
|
# block for additional expectations.
|
|
126
|
-
def assert_api_response(method, expected_status, params: {}, headers: {}, body: nil, path_params: {},
|
|
130
|
+
def assert_api_response(method, expected_status, params: {}, headers: {}, body: nil, path_params: {},
|
|
131
|
+
api_path: nil, &block)
|
|
127
132
|
meta = ::RSpec.current_example.metadata
|
|
128
|
-
context = find_api_context_for(meta, method, path_params)
|
|
133
|
+
context = find_api_context_for(meta, method, path_params, params, expected_status, api_path)
|
|
129
134
|
raise OpenapiRuby::Error, "No api_path defined for #{method.upcase} in this example group" unless context
|
|
130
135
|
|
|
131
136
|
operation = context.operations[method.to_s]
|
|
@@ -187,10 +192,10 @@ module OpenapiRuby
|
|
|
187
192
|
raise "Request validation failed:\n#{req_errors.join("\n")}" unless req_errors.empty?
|
|
188
193
|
end
|
|
189
194
|
|
|
190
|
-
|
|
195
|
+
openapi_transport.dispatch(method, path, **request_args)
|
|
191
196
|
|
|
192
|
-
unless
|
|
193
|
-
raise "Expected status #{expected_status}, got #{
|
|
197
|
+
unless openapi_response.status == expected_status
|
|
198
|
+
raise "Expected status #{expected_status}, got #{openapi_response.status}\nResponse body: #{openapi_response.body}"
|
|
194
199
|
end
|
|
195
200
|
|
|
196
201
|
if response_ctx.schema_definition
|
|
@@ -199,11 +204,11 @@ module OpenapiRuby
|
|
|
199
204
|
)
|
|
200
205
|
errors = validator.validate(
|
|
201
206
|
response_body: parsed_response_body,
|
|
202
|
-
status_code:
|
|
207
|
+
status_code: openapi_response.status,
|
|
203
208
|
response_context: response_ctx
|
|
204
209
|
)
|
|
205
210
|
unless errors.empty?
|
|
206
|
-
raise "Response body validation failed:\n#{errors.join("\n")}\nResponse body: #{
|
|
211
|
+
raise "Response body validation failed:\n#{errors.join("\n")}\nResponse body: #{openapi_response.body}"
|
|
207
212
|
end
|
|
208
213
|
end
|
|
209
214
|
|
|
@@ -214,6 +219,17 @@ module OpenapiRuby
|
|
|
214
219
|
parsed_response_body
|
|
215
220
|
end
|
|
216
221
|
|
|
222
|
+
# The seam between the DSL and the host's request API. Public so specs
|
|
223
|
+
# that drive requests themselves (rate limiting, pagination loops) can
|
|
224
|
+
# reach the same dispatcher and response the assertions use.
|
|
225
|
+
def openapi_transport
|
|
226
|
+
@openapi_transport ||= Testing::Transport.for(self)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def openapi_response
|
|
230
|
+
openapi_transport.response
|
|
231
|
+
end
|
|
232
|
+
|
|
217
233
|
# submit_openapi_request is public so specs can call it directly
|
|
218
234
|
# (e.g., for rate limiting tests that need multiple requests)
|
|
219
235
|
def submit_openapi_request(metadata)
|
|
@@ -303,19 +319,19 @@ module OpenapiRuby
|
|
|
303
319
|
raise "Request validation failed:\n#{req_errors.join("\n")}" unless req_errors.empty?
|
|
304
320
|
end
|
|
305
321
|
|
|
306
|
-
|
|
322
|
+
openapi_transport.dispatch(method, path, **request_args)
|
|
307
323
|
end
|
|
308
324
|
|
|
309
325
|
def assert_openapi_response(metadata)
|
|
310
326
|
response_ctx = find_in_metadata(metadata, :openapi_response)
|
|
311
327
|
|
|
312
328
|
expected_status = response_ctx.status_code.to_i
|
|
313
|
-
actual_status =
|
|
329
|
+
actual_status = openapi_response.status
|
|
314
330
|
|
|
315
331
|
unless actual_status == expected_status
|
|
316
332
|
raise "Response validation failed:\n" \
|
|
317
333
|
"Expected status #{expected_status}, got #{actual_status}\n" \
|
|
318
|
-
"Response body: #{
|
|
334
|
+
"Response body: #{openapi_response.body}"
|
|
319
335
|
end
|
|
320
336
|
|
|
321
337
|
if response_ctx.schema_definition
|
|
@@ -323,30 +339,25 @@ module OpenapiRuby
|
|
|
323
339
|
validator = Testing::ResponseValidator.new(OpenapiRuby::Adapters::RSpec.validation_document_for(schema_name))
|
|
324
340
|
errors = validator.validate(
|
|
325
341
|
response_body: parsed_response_body,
|
|
326
|
-
status_code:
|
|
342
|
+
status_code: openapi_response.status,
|
|
327
343
|
response_context: response_ctx
|
|
328
344
|
)
|
|
329
345
|
unless errors.empty?
|
|
330
|
-
raise "Response body validation failed:\n#{errors.join("\n")}\nResponse body: #{
|
|
346
|
+
raise "Response body validation failed:\n#{errors.join("\n")}\nResponse body: #{openapi_response.body}"
|
|
331
347
|
end
|
|
332
348
|
end
|
|
333
349
|
end
|
|
334
350
|
|
|
335
351
|
private
|
|
336
352
|
|
|
337
|
-
def find_api_context_for(metadata, method, path_params)
|
|
353
|
+
def find_api_context_for(metadata, method, path_params, params, expected_status, api_path)
|
|
338
354
|
contexts = find_in_metadata(metadata, :openapi_api_contexts) || []
|
|
339
|
-
has_path_params = path_params.any?
|
|
340
|
-
|
|
341
|
-
contexts.find do |ctx|
|
|
342
|
-
next false unless ctx.operations.key?(method.to_s)
|
|
343
355
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
end
|
|
356
|
+
OpenapiRuby::Adapters::ContextResolution.resolve(
|
|
357
|
+
contexts, method, path_params,
|
|
358
|
+
params: params, expected_status: expected_status, api_path: api_path,
|
|
359
|
+
owner: metadata[:full_description] || "this example group"
|
|
360
|
+
)
|
|
350
361
|
end
|
|
351
362
|
|
|
352
363
|
def expand_path(template, params)
|
|
@@ -438,10 +449,10 @@ module OpenapiRuby
|
|
|
438
449
|
end
|
|
439
450
|
|
|
440
451
|
def parsed_response_body
|
|
441
|
-
return nil if
|
|
442
|
-
JSON.parse(
|
|
452
|
+
return nil if openapi_response.body.empty?
|
|
453
|
+
JSON.parse(openapi_response.body)
|
|
443
454
|
rescue JSON::ParserError
|
|
444
|
-
|
|
455
|
+
openapi_response.body
|
|
445
456
|
end
|
|
446
457
|
end
|
|
447
458
|
|
|
@@ -468,6 +479,20 @@ module OpenapiRuby
|
|
|
468
479
|
end
|
|
469
480
|
end
|
|
470
481
|
|
|
482
|
+
# Every host but Rails drives requests through rack-test. Hanami also
|
|
483
|
+
# gets a default `app`; elsewhere (Sinatra, Roda, bare Rack) there is no
|
|
484
|
+
# convention for which app is under test, so the suite defines it.
|
|
485
|
+
def self.install_rack_test!(config, app_module: nil)
|
|
486
|
+
require "rack/test"
|
|
487
|
+
|
|
488
|
+
config.include ::Rack::Test::Methods, type: :openapi
|
|
489
|
+
config.include app_module, type: :openapi if app_module
|
|
490
|
+
rescue LoadError
|
|
491
|
+
# No rack-test in the bundle. Testing::Transport raises with setup
|
|
492
|
+
# instructions if a spec then tries to issue a request.
|
|
493
|
+
nil
|
|
494
|
+
end
|
|
495
|
+
|
|
471
496
|
def self.install!
|
|
472
497
|
::RSpec.configure do |config|
|
|
473
498
|
config.extend ExampleGroupHelpers, type: :openapi
|
|
@@ -475,6 +500,11 @@ module OpenapiRuby
|
|
|
475
500
|
|
|
476
501
|
if defined?(::RSpec::Rails)
|
|
477
502
|
config.include ::RSpec::Rails::RequestExampleGroup, type: :openapi
|
|
503
|
+
elsif OpenapiRuby.hanami_host?
|
|
504
|
+
require "openapi_ruby/hanami"
|
|
505
|
+
OpenapiRuby::Hanami.install_rspec!(config)
|
|
506
|
+
else
|
|
507
|
+
install_rack_test!(config)
|
|
478
508
|
end
|
|
479
509
|
|
|
480
510
|
# Schema writing is handled by the rake task (openapi_ruby:generate),
|
|
@@ -11,6 +11,7 @@ module OpenapiRuby
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def load!
|
|
14
|
+
warn_about_autoloaded_paths!
|
|
14
15
|
define_namespace_modules!
|
|
15
16
|
load_component_files!
|
|
16
17
|
@@loaded = true # rubocop:disable Style/ClassVars
|
|
@@ -60,6 +61,20 @@ module OpenapiRuby
|
|
|
60
61
|
|
|
61
62
|
private
|
|
62
63
|
|
|
64
|
+
# A component under Hanami's app/ directory loads fine here (we require
|
|
65
|
+
# the file directly) but Zeitwerk expects that file to define a deeper,
|
|
66
|
+
# app-namespaced constant — so the mismatch only surfaces on eager load
|
|
67
|
+
# in production.
|
|
68
|
+
def warn_about_autoloaded_paths!
|
|
69
|
+
return unless OpenapiRuby.hanami_host?
|
|
70
|
+
|
|
71
|
+
offending = @paths.select { |path| path.to_s.match?(%r{(\A|/)app/}) }
|
|
72
|
+
return if offending.empty?
|
|
73
|
+
|
|
74
|
+
warn "[openapi_ruby] component_paths #{offending.inspect} sit under Hanami's autoloaded " \
|
|
75
|
+
"app/ directory. Move them outside it (e.g. config/api_components) to avoid Zeitwerk conflicts."
|
|
76
|
+
end
|
|
77
|
+
|
|
63
78
|
def define_namespace_modules!
|
|
64
79
|
@paths.each do |path|
|
|
65
80
|
expanded = File.expand_path(path)
|
|
@@ -18,6 +18,12 @@ module OpenapiRuby
|
|
|
18
18
|
# Middleware (runtime validation)
|
|
19
19
|
attr_accessor :request_validation, :response_validation, :coerce_params
|
|
20
20
|
|
|
21
|
+
# Style 2 only: require each test class to declare a single api_path.
|
|
22
|
+
# Off by default — resolution handles more than one path per class, so this
|
|
23
|
+
# is for suites that want the convention enforced anyway.
|
|
24
|
+
# See Adapters::ContextResolution for how a request is matched.
|
|
25
|
+
attr_accessor :single_api_path_per_class
|
|
26
|
+
|
|
21
27
|
# Test DSL: validate that requests match the declared operation before sending.
|
|
22
28
|
# Enabled by default; set to false to disable.
|
|
23
29
|
attr_accessor :test_request_validation
|
|
@@ -42,15 +48,28 @@ module OpenapiRuby
|
|
|
42
48
|
# UI (optional)
|
|
43
49
|
attr_accessor :ui_enabled, :ui_config
|
|
44
50
|
|
|
51
|
+
RAILS_COMPONENT_PATHS = ["app/api_components"].freeze
|
|
52
|
+
|
|
53
|
+
# Hanami's Zeitwerk owns every constant under app/ and expects
|
|
54
|
+
# app/api_components/schemas/user.rb to define a deeper constant than
|
|
55
|
+
# Components::Loader's plain require does, so the Hanami default keeps
|
|
56
|
+
# components outside its autoload roots.
|
|
57
|
+
HANAMI_COMPONENT_PATHS = ["config/api_components"].freeze
|
|
58
|
+
|
|
59
|
+
def self.default_component_paths
|
|
60
|
+
(OpenapiRuby.hanami_host? ? HANAMI_COMPONENT_PATHS : RAILS_COMPONENT_PATHS).dup
|
|
61
|
+
end
|
|
62
|
+
|
|
45
63
|
def initialize
|
|
46
64
|
@schemas = {}
|
|
47
|
-
@component_paths =
|
|
65
|
+
@component_paths = self.class.default_component_paths
|
|
48
66
|
@component_scope_paths = {}
|
|
49
67
|
@camelize_keys = true
|
|
50
68
|
@request_validation = :disabled
|
|
51
69
|
@response_validation = :disabled
|
|
52
70
|
@coerce_params = true
|
|
53
71
|
@test_request_validation = true
|
|
72
|
+
@single_api_path_per_class = false
|
|
54
73
|
@schema_output_dir = "openapi"
|
|
55
74
|
@schema_output_format = :yaml
|
|
56
75
|
@ui_enabled = false
|
data/lib/openapi_ruby/engine.rb
CHANGED
|
@@ -5,44 +5,7 @@ module OpenapiRuby
|
|
|
5
5
|
isolate_namespace OpenapiRuby
|
|
6
6
|
|
|
7
7
|
initializer "openapi_ruby.middleware" do |app|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
next if ENV["OPENAPI_RUBY_GENERATING"]
|
|
11
|
-
|
|
12
|
-
if config.request_validation != :disabled || config.response_validation != :disabled
|
|
13
|
-
config.schemas.each do |name, schema_config|
|
|
14
|
-
schema_path = resolve_schema_path(config, name)
|
|
15
|
-
next unless schema_path && File.exist?(schema_path)
|
|
16
|
-
|
|
17
|
-
resolver = Middleware::SchemaResolver.new(
|
|
18
|
-
spec_path: schema_path,
|
|
19
|
-
strict_reference_validation: config.strict_reference_validation
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
prefix = schema_config[:prefix]
|
|
23
|
-
|
|
24
|
-
if config.request_validation != :disabled
|
|
25
|
-
app.middleware.use Middleware::RequestValidation,
|
|
26
|
-
schema_resolver: resolver,
|
|
27
|
-
mode: config.request_validation,
|
|
28
|
-
prefix: prefix
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
if config.response_validation != :disabled
|
|
32
|
-
app.middleware.use Middleware::ResponseValidation,
|
|
33
|
-
schema_resolver: resolver,
|
|
34
|
-
mode: config.response_validation,
|
|
35
|
-
prefix: prefix
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
def resolve_schema_path(config, schema_name)
|
|
44
|
-
ext = (config.schema_output_format == :json) ? "json" : "yaml"
|
|
45
|
-
Rails.root.join(config.schema_output_dir, "#{schema_name}.#{ext}").to_s
|
|
8
|
+
Middleware::Installer.install!(app.middleware)
|
|
46
9
|
end
|
|
47
10
|
end
|
|
48
11
|
end
|
data/lib/openapi_ruby/errors.rb
CHANGED
|
@@ -14,6 +14,13 @@ module OpenapiRuby
|
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# Raised when a Style 2 request matches more than one declared api_path.
|
|
18
|
+
class AmbiguousApiPath < Error; end
|
|
19
|
+
|
|
20
|
+
# Raised when a class declares a second api_path and
|
|
21
|
+
# `config.single_api_path_per_class` is on.
|
|
22
|
+
class MultipleApiPaths < Error; end
|
|
23
|
+
|
|
17
24
|
class SchemaValidationError < Error
|
|
18
25
|
attr_reader :validation_errors
|
|
19
26
|
|
|
@@ -24,6 +24,41 @@ module OpenapiRuby
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Which framework hosts the app being generated for. Detected from the
|
|
28
|
+
# filesystem as well as loaded constants: the rake process has not booted
|
|
29
|
+
# the app yet. Hanami keeps its app class in config/app.rb where Rails
|
|
30
|
+
# uses config/application.rb; a config.ru with neither is a Rack app
|
|
31
|
+
# (Sinatra, Roda, bare Rack).
|
|
32
|
+
def detect_host
|
|
33
|
+
# Files describe the app being generated for; loaded constants only say
|
|
34
|
+
# what the Rakefile happened to require, which in a mixed bundle can be
|
|
35
|
+
# either framework.
|
|
36
|
+
return "rails" if File.exist?("config/application.rb")
|
|
37
|
+
return "hanami" if File.exist?("config/app.rb")
|
|
38
|
+
return "rack" if File.exist?("config.ru")
|
|
39
|
+
|
|
40
|
+
OpenapiRuby.host.to_s
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Environment variables the host reads to decide it is running under test.
|
|
44
|
+
# Sinatra takes APP_ENV first and falls back to RACK_ENV; other Rack apps
|
|
45
|
+
# read one or the other, so a Rack host gets both.
|
|
46
|
+
HOST_ENV_VARS = {
|
|
47
|
+
"rails" => ["RAILS_ENV"],
|
|
48
|
+
"hanami" => ["HANAMI_ENV"],
|
|
49
|
+
"rack" => ["APP_ENV", "RACK_ENV"]
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
# Env for the generation subprocess: the host's own environment variables
|
|
53
|
+
# plus the flag that tells the gem it is generating rather than serving.
|
|
54
|
+
def subprocess_env(host = detect_host)
|
|
55
|
+
env = HOST_ENV_VARS.fetch(host, HOST_ENV_VARS["rails"]).to_h do |var|
|
|
56
|
+
[var, ENV.fetch(var, "test")]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
env.merge("OPENAPI_RUBY_GENERATING" => "true")
|
|
60
|
+
end
|
|
61
|
+
|
|
27
62
|
def default_pattern_for(framework)
|
|
28
63
|
case framework
|
|
29
64
|
when "rspec" then "spec/**/*_spec.rb"
|
|
@@ -76,13 +111,20 @@ module OpenapiRuby
|
|
|
76
111
|
|
|
77
112
|
# Loads both adapters and both file globs in one process. Useful
|
|
78
113
|
# during a phased RSpec → Minitest migration where the suite
|
|
79
|
-
# holds both DSL styles. Consumers
|
|
114
|
+
# holds both DSL styles. Consumers can guard
|
|
80
115
|
# `require "rails/test_help"` and `require "rspec/rails"` in
|
|
81
116
|
# their test helpers with `unless OpenapiRuby.schema_generating?`
|
|
82
117
|
# so the two test frameworks don't both register Rails lazy
|
|
83
118
|
# hooks in the same process — only the DSL needs to be live for
|
|
84
119
|
# schema generation.
|
|
85
120
|
#
|
|
121
|
+
# Guarding is per-helper and optional: a suite that references
|
|
122
|
+
# what the guarded require defines at *load* time (shared
|
|
123
|
+
# examples, `fixtures :all`, `include Devise::Test::...` in a
|
|
124
|
+
# class body) fails to load those files at all, which is worse
|
|
125
|
+
# than the hook conflict. Such a helper stays unguarded and the
|
|
126
|
+
# pattern narrows instead.
|
|
127
|
+
#
|
|
86
128
|
# Each glob runs with its own framework's directory at the head
|
|
87
129
|
# of $LOAD_PATH so the typical `require "openapi_helper"` /
|
|
88
130
|
# `require "rails_helper"` / `require "test_helper"` resolves
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openapi_ruby"
|
|
4
|
+
|
|
5
|
+
module OpenapiRuby
|
|
6
|
+
# Hanami 2 integration. Rails receives this wiring from the engine's
|
|
7
|
+
# initializer and from rspec-rails; Hanami has no engines, so the host app
|
|
8
|
+
# makes the calls itself.
|
|
9
|
+
#
|
|
10
|
+
# ::Hanami is spelled with leading colons throughout — inside this namespace
|
|
11
|
+
# a bare `Hanami` would resolve to OpenapiRuby::Hanami.
|
|
12
|
+
module Hanami
|
|
13
|
+
# Included into :openapi example groups so rack-test knows which app to
|
|
14
|
+
# drive. A `let(:app)` in the suite takes precedence over this.
|
|
15
|
+
module RackTestApp
|
|
16
|
+
def app
|
|
17
|
+
::Hanami.app
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# Mounts the runtime validation middleware. In config/app.rb, after
|
|
24
|
+
# requiring the file that holds your OpenapiRuby.configure block:
|
|
25
|
+
#
|
|
26
|
+
# module MyApp
|
|
27
|
+
# class App < Hanami::App
|
|
28
|
+
# OpenapiRuby::Hanami.install_middleware!(config)
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# Takes the app config (preferred — it carries the root) or a bare
|
|
33
|
+
# middleware stack.
|
|
34
|
+
def install_middleware!(config, root: nil)
|
|
35
|
+
stack = config.respond_to?(:middleware) ? config.middleware : config
|
|
36
|
+
root ||= config_root(config) || OpenapiRuby.app_root
|
|
37
|
+
|
|
38
|
+
Middleware::Installer.install!(stack, root: root)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Wires rack-test into :openapi example groups. Called for you by
|
|
42
|
+
# `require "openapi_ruby/rspec"`; exposed for suites that configure RSpec
|
|
43
|
+
# by hand.
|
|
44
|
+
def install_rspec!(rspec_config = nil)
|
|
45
|
+
if rspec_config
|
|
46
|
+
Adapters::RSpec.install_rack_test!(rspec_config, app_module: RackTestApp)
|
|
47
|
+
else
|
|
48
|
+
::RSpec.configure { |config| Adapters::RSpec.install_rack_test!(config, app_module: RackTestApp) }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def config_root(config)
|
|
53
|
+
root = config.respond_to?(:root) ? config.root : nil
|
|
54
|
+
root unless root.to_s.empty?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Requiring this file declares the host, so the component default no longer
|
|
58
|
+
# depends on ::Hanami being loaded before the configuration object was
|
|
59
|
+
# built. Only a pristine default is replaced.
|
|
60
|
+
def apply_component_path_default!
|
|
61
|
+
config = OpenapiRuby.configuration
|
|
62
|
+
return unless config.component_paths == Configuration::RAILS_COMPONENT_PATHS
|
|
63
|
+
|
|
64
|
+
config.component_paths = Configuration::HANAMI_COMPONENT_PATHS.dup
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
OpenapiRuby::Hanami.apply_component_path_default!
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiRuby
|
|
4
|
+
class << self
|
|
5
|
+
# Which framework the gem is running inside. Rails wins a tie: an app with
|
|
6
|
+
# both constants loaded is a Rails app that happens to have another
|
|
7
|
+
# framework's gems in the bundle. :rack covers Sinatra, Roda, and bare Rack
|
|
8
|
+
# — anything whose only shared surface is the Rack SPEC.
|
|
9
|
+
def host
|
|
10
|
+
if defined?(::Rails)
|
|
11
|
+
:rails
|
|
12
|
+
elsif defined?(::Hanami)
|
|
13
|
+
:hanami
|
|
14
|
+
else
|
|
15
|
+
:rack
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def rails_host?
|
|
20
|
+
host == :rails
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def hanami_host?
|
|
24
|
+
host == :hanami
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def rack_host?
|
|
28
|
+
host == :rack
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Application root, used to resolve the configured (relative)
|
|
32
|
+
# schema_output_dir. Falls back to the working directory: a bare Rack app
|
|
33
|
+
# has no root of its own, and the generation subprocess loads test files
|
|
34
|
+
# without booting an app at all.
|
|
35
|
+
def app_root
|
|
36
|
+
root = if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
|
|
37
|
+
::Rails.root
|
|
38
|
+
elsif hanami_booted?
|
|
39
|
+
::Hanami.app.root
|
|
40
|
+
else
|
|
41
|
+
Dir.pwd
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
root.to_s
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def hanami_booted?
|
|
50
|
+
defined?(::Hanami) && ::Hanami.respond_to?(:app?) && ::Hanami.app?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|