openapi-ruby 4.1.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +252 -11
- 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/generators/openapi_ruby/component/component_generator.rb +21 -1
- data/lib/openapi_ruby/adapters/context_resolution.rb +1 -1
- data/lib/openapi_ruby/adapters/minitest.rb +35 -8
- data/lib/openapi_ruby/adapters/rspec.rb +46 -15
- data/lib/openapi_ruby/components/loader.rb +15 -0
- data/lib/openapi_ruby/configuration.rb +13 -1
- data/lib/openapi_ruby/core/document_builder.rb +1 -0
- data/lib/openapi_ruby/dsl/context.rb +1 -1
- data/lib/openapi_ruby/dsl/operation_context.rb +1 -1
- data/lib/openapi_ruby/engine.rb +1 -38
- 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/parameter_names.rb +89 -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/request_builder.rb +1 -1
- data/lib/openapi_ruby/testing/request_validator.rb +15 -4
- data/lib/openapi_ruby/testing/transport.rb +99 -0
- data/lib/openapi_ruby/version.rb +1 -1
- data/lib/openapi_ruby.rb +6 -0
- data/lib/tasks/openapi_ruby.rake +1 -19
- metadata +14 -20
|
@@ -156,8 +156,9 @@ module OpenapiRuby
|
|
|
156
156
|
|
|
157
157
|
headers["Accept"] ||= "application/json"
|
|
158
158
|
|
|
159
|
-
path_param_names = context.path_parameters.
|
|
159
|
+
path_param_names = context.path_parameters.flat_map { |p| ParameterNames.lookup_names(p) }
|
|
160
160
|
query_params = params.reject { |k, _| path_param_names.include?(k.to_s) }
|
|
161
|
+
query_params = ParameterNames.rename_keys(query_params, operation.parameters)
|
|
161
162
|
|
|
162
163
|
if body
|
|
163
164
|
content_type = operation.request_body_definition&.dig("content")&.keys&.first || "application/json"
|
|
@@ -192,10 +193,10 @@ module OpenapiRuby
|
|
|
192
193
|
raise "Request validation failed:\n#{req_errors.join("\n")}" unless req_errors.empty?
|
|
193
194
|
end
|
|
194
195
|
|
|
195
|
-
|
|
196
|
+
openapi_transport.dispatch(method, path, **request_args)
|
|
196
197
|
|
|
197
|
-
unless
|
|
198
|
-
raise "Expected status #{expected_status}, got #{
|
|
198
|
+
unless openapi_response.status == expected_status
|
|
199
|
+
raise "Expected status #{expected_status}, got #{openapi_response.status}\nResponse body: #{openapi_response.body}"
|
|
199
200
|
end
|
|
200
201
|
|
|
201
202
|
if response_ctx.schema_definition
|
|
@@ -204,11 +205,11 @@ module OpenapiRuby
|
|
|
204
205
|
)
|
|
205
206
|
errors = validator.validate(
|
|
206
207
|
response_body: parsed_response_body,
|
|
207
|
-
status_code:
|
|
208
|
+
status_code: openapi_response.status,
|
|
208
209
|
response_context: response_ctx
|
|
209
210
|
)
|
|
210
211
|
unless errors.empty?
|
|
211
|
-
raise "Response body validation failed:\n#{errors.join("\n")}\nResponse body: #{
|
|
212
|
+
raise "Response body validation failed:\n#{errors.join("\n")}\nResponse body: #{openapi_response.body}"
|
|
212
213
|
end
|
|
213
214
|
end
|
|
214
215
|
|
|
@@ -219,6 +220,17 @@ module OpenapiRuby
|
|
|
219
220
|
parsed_response_body
|
|
220
221
|
end
|
|
221
222
|
|
|
223
|
+
# The seam between the DSL and the host's request API. Public so specs
|
|
224
|
+
# that drive requests themselves (rate limiting, pagination loops) can
|
|
225
|
+
# reach the same dispatcher and response the assertions use.
|
|
226
|
+
def openapi_transport
|
|
227
|
+
@openapi_transport ||= Testing::Transport.for(self)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def openapi_response
|
|
231
|
+
openapi_transport.response
|
|
232
|
+
end
|
|
233
|
+
|
|
222
234
|
# submit_openapi_request is public so specs can call it directly
|
|
223
235
|
# (e.g., for rate limiting tests that need multiple requests)
|
|
224
236
|
def submit_openapi_request(metadata)
|
|
@@ -237,7 +249,7 @@ module OpenapiRuby
|
|
|
237
249
|
next if val.nil?
|
|
238
250
|
|
|
239
251
|
case param["in"]
|
|
240
|
-
when "query" then params[
|
|
252
|
+
when "query" then params[ParameterNames.wire_name(param)] = val
|
|
241
253
|
when "header" then headers[name] = val
|
|
242
254
|
end
|
|
243
255
|
end
|
|
@@ -308,19 +320,19 @@ module OpenapiRuby
|
|
|
308
320
|
raise "Request validation failed:\n#{req_errors.join("\n")}" unless req_errors.empty?
|
|
309
321
|
end
|
|
310
322
|
|
|
311
|
-
|
|
323
|
+
openapi_transport.dispatch(method, path, **request_args)
|
|
312
324
|
end
|
|
313
325
|
|
|
314
326
|
def assert_openapi_response(metadata)
|
|
315
327
|
response_ctx = find_in_metadata(metadata, :openapi_response)
|
|
316
328
|
|
|
317
329
|
expected_status = response_ctx.status_code.to_i
|
|
318
|
-
actual_status =
|
|
330
|
+
actual_status = openapi_response.status
|
|
319
331
|
|
|
320
332
|
unless actual_status == expected_status
|
|
321
333
|
raise "Response validation failed:\n" \
|
|
322
334
|
"Expected status #{expected_status}, got #{actual_status}\n" \
|
|
323
|
-
"Response body: #{
|
|
335
|
+
"Response body: #{openapi_response.body}"
|
|
324
336
|
end
|
|
325
337
|
|
|
326
338
|
if response_ctx.schema_definition
|
|
@@ -328,11 +340,11 @@ module OpenapiRuby
|
|
|
328
340
|
validator = Testing::ResponseValidator.new(OpenapiRuby::Adapters::RSpec.validation_document_for(schema_name))
|
|
329
341
|
errors = validator.validate(
|
|
330
342
|
response_body: parsed_response_body,
|
|
331
|
-
status_code:
|
|
343
|
+
status_code: openapi_response.status,
|
|
332
344
|
response_context: response_ctx
|
|
333
345
|
)
|
|
334
346
|
unless errors.empty?
|
|
335
|
-
raise "Response body validation failed:\n#{errors.join("\n")}\nResponse body: #{
|
|
347
|
+
raise "Response body validation failed:\n#{errors.join("\n")}\nResponse body: #{openapi_response.body}"
|
|
336
348
|
end
|
|
337
349
|
end
|
|
338
350
|
end
|
|
@@ -438,10 +450,10 @@ module OpenapiRuby
|
|
|
438
450
|
end
|
|
439
451
|
|
|
440
452
|
def parsed_response_body
|
|
441
|
-
return nil if
|
|
442
|
-
JSON.parse(
|
|
453
|
+
return nil if openapi_response.body.empty?
|
|
454
|
+
JSON.parse(openapi_response.body)
|
|
443
455
|
rescue JSON::ParserError
|
|
444
|
-
|
|
456
|
+
openapi_response.body
|
|
445
457
|
end
|
|
446
458
|
end
|
|
447
459
|
|
|
@@ -468,6 +480,20 @@ module OpenapiRuby
|
|
|
468
480
|
end
|
|
469
481
|
end
|
|
470
482
|
|
|
483
|
+
# Every host but Rails drives requests through rack-test. Hanami also
|
|
484
|
+
# gets a default `app`; elsewhere (Sinatra, Roda, bare Rack) there is no
|
|
485
|
+
# convention for which app is under test, so the suite defines it.
|
|
486
|
+
def self.install_rack_test!(config, app_module: nil)
|
|
487
|
+
require "rack/test"
|
|
488
|
+
|
|
489
|
+
config.include ::Rack::Test::Methods, type: :openapi
|
|
490
|
+
config.include app_module, type: :openapi if app_module
|
|
491
|
+
rescue LoadError
|
|
492
|
+
# No rack-test in the bundle. Testing::Transport raises with setup
|
|
493
|
+
# instructions if a spec then tries to issue a request.
|
|
494
|
+
nil
|
|
495
|
+
end
|
|
496
|
+
|
|
471
497
|
def self.install!
|
|
472
498
|
::RSpec.configure do |config|
|
|
473
499
|
config.extend ExampleGroupHelpers, type: :openapi
|
|
@@ -475,6 +501,11 @@ module OpenapiRuby
|
|
|
475
501
|
|
|
476
502
|
if defined?(::RSpec::Rails)
|
|
477
503
|
config.include ::RSpec::Rails::RequestExampleGroup, type: :openapi
|
|
504
|
+
elsif OpenapiRuby.hanami_host?
|
|
505
|
+
require "openapi_ruby/hanami"
|
|
506
|
+
OpenapiRuby::Hanami.install_rspec!(config)
|
|
507
|
+
else
|
|
508
|
+
install_rack_test!(config)
|
|
478
509
|
end
|
|
479
510
|
|
|
480
511
|
# 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)
|
|
@@ -48,9 +48,21 @@ module OpenapiRuby
|
|
|
48
48
|
# UI (optional)
|
|
49
49
|
attr_accessor :ui_enabled, :ui_config
|
|
50
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
|
+
|
|
51
63
|
def initialize
|
|
52
64
|
@schemas = {}
|
|
53
|
-
@component_paths =
|
|
65
|
+
@component_paths = self.class.default_component_paths
|
|
54
66
|
@component_scope_paths = {}
|
|
55
67
|
@camelize_keys = true
|
|
56
68
|
@request_validation = :disabled
|
|
@@ -32,7 +32,7 @@ module OpenapiRuby
|
|
|
32
32
|
def to_openapi
|
|
33
33
|
result = {}
|
|
34
34
|
|
|
35
|
-
result["parameters"] = @path_parameters if @path_parameters.any?
|
|
35
|
+
result["parameters"] = ParameterNames.parameters_for_document(@path_parameters) if @path_parameters.any?
|
|
36
36
|
|
|
37
37
|
@operations.each do |verb, op|
|
|
38
38
|
result[verb] = op.to_openapi
|
|
@@ -93,7 +93,7 @@ module OpenapiRuby
|
|
|
93
93
|
result["summary"] = @summary if @summary
|
|
94
94
|
result["tags"] = @tags_list if @tags_list.any?
|
|
95
95
|
result.merge!(@metadata)
|
|
96
|
-
result["parameters"] = @parameters if @parameters.any?
|
|
96
|
+
result["parameters"] = ParameterNames.parameters_for_document(@parameters) if @parameters.any?
|
|
97
97
|
result["security"] = @security_list if @security_list
|
|
98
98
|
|
|
99
99
|
result["requestBody"] = build_request_body if @request_body_definition
|
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
|
|
@@ -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
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiRuby
|
|
4
|
+
module Middleware
|
|
5
|
+
# Installs the runtime validation middleware onto a Rack stack. Anything
|
|
6
|
+
# responding to #use works — Rails' `app.middleware` and Hanami's
|
|
7
|
+
# `config.middleware` share that much API, so no host branching is needed.
|
|
8
|
+
module Installer
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def install!(stack, root: OpenapiRuby.app_root)
|
|
12
|
+
config = OpenapiRuby.configuration
|
|
13
|
+
|
|
14
|
+
return if ENV["OPENAPI_RUBY_GENERATING"]
|
|
15
|
+
return if config.request_validation == :disabled && config.response_validation == :disabled
|
|
16
|
+
|
|
17
|
+
config.schemas.each do |name, schema_config|
|
|
18
|
+
schema_path = resolve_schema_path(config, name, root)
|
|
19
|
+
next unless schema_path && File.exist?(schema_path)
|
|
20
|
+
|
|
21
|
+
resolver = SchemaResolver.new(
|
|
22
|
+
spec_path: schema_path,
|
|
23
|
+
strict_reference_validation: config.strict_reference_validation
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
prefix = schema_config[:prefix]
|
|
27
|
+
|
|
28
|
+
if config.request_validation != :disabled
|
|
29
|
+
stack.use RequestValidation,
|
|
30
|
+
schema_resolver: resolver,
|
|
31
|
+
mode: config.request_validation,
|
|
32
|
+
prefix: prefix
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if config.response_validation != :disabled
|
|
36
|
+
stack.use ResponseValidation,
|
|
37
|
+
schema_resolver: resolver,
|
|
38
|
+
mode: config.response_validation,
|
|
39
|
+
prefix: prefix
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def resolve_schema_path(config, schema_name, root)
|
|
45
|
+
ext = (config.schema_output_format == :json) ? "json" : "yaml"
|
|
46
|
+
File.join(root.to_s, config.schema_output_dir, "#{schema_name}.#{ext}")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiRuby
|
|
4
|
+
# Parameters are declared in snake_case so `let(:page_size)` and
|
|
5
|
+
# `params: {page_size: 1}` stay idiomatic Ruby. With `camelize_keys` on, the
|
|
6
|
+
# name that reaches the document *and* the wire is camelCased, so parameters
|
|
7
|
+
# match the component keys instead of contradicting them.
|
|
8
|
+
#
|
|
9
|
+
# Header parameters keep their declared spelling: HTTP header names are
|
|
10
|
+
# conventionally hyphenated and matched case-insensitively, so camelizing
|
|
11
|
+
# them would rename headers nobody asked to rename.
|
|
12
|
+
module ParameterNames
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def camelize?
|
|
16
|
+
OpenapiRuby.configuration.camelize_keys
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def camelize(name)
|
|
20
|
+
return name.to_s unless camelize?
|
|
21
|
+
|
|
22
|
+
Components::KeyTransformer.camelize(name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The name a declared parameter carries in the document and on the wire.
|
|
26
|
+
def wire_name(param)
|
|
27
|
+
name = param["name"] || param[:name]
|
|
28
|
+
return nil if name.nil?
|
|
29
|
+
return name.to_s if header?(param)
|
|
30
|
+
|
|
31
|
+
camelize(name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Both spellings of a declared parameter. Value lookups go through this so a
|
|
35
|
+
# caller that passes the declared name still resolves after the wire name
|
|
36
|
+
# diverged from it.
|
|
37
|
+
def lookup_names(param)
|
|
38
|
+
declared = (param["name"] || param[:name])&.to_s
|
|
39
|
+
return [] unless declared
|
|
40
|
+
|
|
41
|
+
[declared, wire_name(param)].compact.uniq
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parameters_for_document(parameters)
|
|
45
|
+
return parameters unless camelize?
|
|
46
|
+
|
|
47
|
+
parameters.map do |param|
|
|
48
|
+
wire = wire_name(param)
|
|
49
|
+
next param if wire.nil? || wire == param["name"].to_s
|
|
50
|
+
|
|
51
|
+
param.merge("name" => wire)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# `/users/{user_id}` -> `/users/{userId}`. The template variables have to
|
|
56
|
+
# travel with the parameter names: OpenAPI requires each one to be backed by
|
|
57
|
+
# a path parameter of the same name.
|
|
58
|
+
def in_template(template)
|
|
59
|
+
return template unless camelize?
|
|
60
|
+
|
|
61
|
+
template.gsub(/\{(\w+)\}/) { "{#{camelize(::Regexp.last_match(1))}}" }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Rename the keys of a request value hash from their declared spelling to
|
|
65
|
+
# the wire spelling. Keys that match no declared parameter are left alone —
|
|
66
|
+
# undeclared params are passed through as the caller wrote them.
|
|
67
|
+
def rename_keys(values, parameters)
|
|
68
|
+
return values unless camelize?
|
|
69
|
+
|
|
70
|
+
mapping = parameters.each_with_object({}) do |param, acc|
|
|
71
|
+
declared = (param["name"] || param[:name])&.to_s
|
|
72
|
+
next unless declared
|
|
73
|
+
|
|
74
|
+
wire = wire_name(param)
|
|
75
|
+
acc[declared] = wire if wire && wire != declared
|
|
76
|
+
end
|
|
77
|
+
return values if mapping.empty?
|
|
78
|
+
|
|
79
|
+
values.each_with_object({}) do |(key, value), acc|
|
|
80
|
+
renamed = mapping[key.to_s]
|
|
81
|
+
acc[renamed.nil? ? key : renamed] = value
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def header?(param)
|
|
86
|
+
(param["in"] || param[:in]).to_s == "header"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|