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.
@@ -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: {}, &block)
32
- context = find_context_for(method, path_params)
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
- send(method, path, **request_args)
129
+ openapi_transport.dispatch(method, path, **request_args)
100
130
 
101
131
  # Validate response
102
- assert_equal expected_status, response.status,
103
- "Expected status #{expected_status}, got #{response.status}\nResponse body: #{response.body}"
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: response.status,
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
- private
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 find_context_for(method, path_params)
127
- has_path_params = path_params.any?
161
+ def openapi_response
162
+ openapi_transport.response
163
+ end
128
164
 
129
- self.class._openapi_contexts.find do |ctx|
130
- next false unless ctx.operations.key?(method.to_s)
165
+ private
131
166
 
132
- if has_path_params
133
- ctx.path_template.include?("{")
134
- else
135
- !ctx.path_template.include?("{")
136
- end
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 response.body.empty?
251
+ return nil if openapi_response.body.empty?
218
252
 
219
- JSON.parse(response.body)
253
+ JSON.parse(openapi_response.body)
220
254
  rescue JSON::ParserError
221
- response.body
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
- metadata[:openapi_api_contexts] ||= []
25
- metadata[:openapi_api_contexts] << context
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: {}, &block)
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
- send(method, path, **request_args)
195
+ openapi_transport.dispatch(method, path, **request_args)
191
196
 
192
- unless response.status == expected_status
193
- raise "Expected status #{expected_status}, got #{response.status}\nResponse body: #{response.body}"
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: response.status,
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: #{response.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
- send(method.to_sym, path, **request_args)
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 = response.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: #{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: response.status,
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: #{response.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
- if has_path_params
345
- ctx.path_template.include?("{")
346
- else
347
- !ctx.path_template.include?("{")
348
- end
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 response.body.empty?
442
- JSON.parse(response.body)
452
+ return nil if openapi_response.body.empty?
453
+ JSON.parse(openapi_response.body)
443
454
  rescue JSON::ParserError
444
- response.body
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 = ["app/api_components"]
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
@@ -5,44 +5,7 @@ module OpenapiRuby
5
5
  isolate_namespace OpenapiRuby
6
6
 
7
7
  initializer "openapi_ruby.middleware" do |app|
8
- config = OpenapiRuby.configuration
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
@@ -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 should guard
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