openapi-ruby 4.1.0 → 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 +219 -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/openapi_ruby/adapters/minitest.rb +33 -7
- data/lib/openapi_ruby/adapters/rspec.rb +43 -13
- data/lib/openapi_ruby/components/loader.rb +15 -0
- data/lib/openapi_ruby/configuration.rb +13 -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/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 +13 -20
|
@@ -192,10 +192,10 @@ module OpenapiRuby
|
|
|
192
192
|
raise "Request validation failed:\n#{req_errors.join("\n")}" unless req_errors.empty?
|
|
193
193
|
end
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
openapi_transport.dispatch(method, path, **request_args)
|
|
196
196
|
|
|
197
|
-
unless
|
|
198
|
-
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}"
|
|
199
199
|
end
|
|
200
200
|
|
|
201
201
|
if response_ctx.schema_definition
|
|
@@ -204,11 +204,11 @@ module OpenapiRuby
|
|
|
204
204
|
)
|
|
205
205
|
errors = validator.validate(
|
|
206
206
|
response_body: parsed_response_body,
|
|
207
|
-
status_code:
|
|
207
|
+
status_code: openapi_response.status,
|
|
208
208
|
response_context: response_ctx
|
|
209
209
|
)
|
|
210
210
|
unless errors.empty?
|
|
211
|
-
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}"
|
|
212
212
|
end
|
|
213
213
|
end
|
|
214
214
|
|
|
@@ -219,6 +219,17 @@ module OpenapiRuby
|
|
|
219
219
|
parsed_response_body
|
|
220
220
|
end
|
|
221
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
|
+
|
|
222
233
|
# submit_openapi_request is public so specs can call it directly
|
|
223
234
|
# (e.g., for rate limiting tests that need multiple requests)
|
|
224
235
|
def submit_openapi_request(metadata)
|
|
@@ -308,19 +319,19 @@ module OpenapiRuby
|
|
|
308
319
|
raise "Request validation failed:\n#{req_errors.join("\n")}" unless req_errors.empty?
|
|
309
320
|
end
|
|
310
321
|
|
|
311
|
-
|
|
322
|
+
openapi_transport.dispatch(method, path, **request_args)
|
|
312
323
|
end
|
|
313
324
|
|
|
314
325
|
def assert_openapi_response(metadata)
|
|
315
326
|
response_ctx = find_in_metadata(metadata, :openapi_response)
|
|
316
327
|
|
|
317
328
|
expected_status = response_ctx.status_code.to_i
|
|
318
|
-
actual_status =
|
|
329
|
+
actual_status = openapi_response.status
|
|
319
330
|
|
|
320
331
|
unless actual_status == expected_status
|
|
321
332
|
raise "Response validation failed:\n" \
|
|
322
333
|
"Expected status #{expected_status}, got #{actual_status}\n" \
|
|
323
|
-
"Response body: #{
|
|
334
|
+
"Response body: #{openapi_response.body}"
|
|
324
335
|
end
|
|
325
336
|
|
|
326
337
|
if response_ctx.schema_definition
|
|
@@ -328,11 +339,11 @@ module OpenapiRuby
|
|
|
328
339
|
validator = Testing::ResponseValidator.new(OpenapiRuby::Adapters::RSpec.validation_document_for(schema_name))
|
|
329
340
|
errors = validator.validate(
|
|
330
341
|
response_body: parsed_response_body,
|
|
331
|
-
status_code:
|
|
342
|
+
status_code: openapi_response.status,
|
|
332
343
|
response_context: response_ctx
|
|
333
344
|
)
|
|
334
345
|
unless errors.empty?
|
|
335
|
-
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}"
|
|
336
347
|
end
|
|
337
348
|
end
|
|
338
349
|
end
|
|
@@ -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)
|
|
@@ -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
|
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,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack"
|
|
4
|
+
|
|
5
|
+
module OpenapiRuby
|
|
6
|
+
# Serves the generated schema documents and the Swagger UI as a plain Rack
|
|
7
|
+
# app, for hosts with no engine to mount:
|
|
8
|
+
#
|
|
9
|
+
# # Hanami — config/routes.rb
|
|
10
|
+
# mount OpenapiRuby::RackApp, at: "/api-docs"
|
|
11
|
+
#
|
|
12
|
+
# Routes mirror the Rails engine's (config/routes.rb) so both hosts expose
|
|
13
|
+
# the same paths. Schema URLs are derived from SCRIPT_NAME, so the app works
|
|
14
|
+
# at any mount point.
|
|
15
|
+
#
|
|
16
|
+
# Deliberately not namespaced under OpenapiRuby::Rack: that constant would
|
|
17
|
+
# shadow the top-level ::Rack for every file in this gem.
|
|
18
|
+
class RackApp
|
|
19
|
+
SCHEMA_PATH = %r{\A/schemas/(?<name>.+)\z}
|
|
20
|
+
SCHEMA_EXTENSION = /\.(json|ya?ml)\z/
|
|
21
|
+
|
|
22
|
+
def self.call(env)
|
|
23
|
+
(@app ||= new).call(env)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(env)
|
|
27
|
+
request = ::Rack::Request.new(env)
|
|
28
|
+
return method_not_allowed unless request.get? || request.head?
|
|
29
|
+
|
|
30
|
+
status, headers, body = case request.path_info
|
|
31
|
+
when "", "/" then ui(request)
|
|
32
|
+
when "/schemas" then schema_index
|
|
33
|
+
when "/oauth2-redirect.html" then oauth2_redirect
|
|
34
|
+
when SCHEMA_PATH then schema(::Regexp.last_match(:name), request)
|
|
35
|
+
else not_found
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Drop the body ourselves: Rails runs Rack::Head above the engine, a bare
|
|
39
|
+
# Hanami mount has nothing between the router and here.
|
|
40
|
+
[status, headers, request.head? ? [] : body]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def ui(request)
|
|
46
|
+
return not_found unless OpenapiRuby.configuration.ui_enabled
|
|
47
|
+
|
|
48
|
+
html = Serving.swagger_ui_html(
|
|
49
|
+
schema_urls: schema_urls(request),
|
|
50
|
+
ui_config: OpenapiRuby.configuration.ui_config
|
|
51
|
+
)
|
|
52
|
+
respond(200, "text/html", html)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def oauth2_redirect
|
|
56
|
+
return not_found unless OpenapiRuby.configuration.ui_enabled
|
|
57
|
+
|
|
58
|
+
respond(200, "text/html", File.read(Serving.oauth2_redirect_file))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def schema_index
|
|
62
|
+
respond(200, "application/json", {schemas: Serving.schema_names}.to_json)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def schema(name, request)
|
|
66
|
+
document = Serving.schema_document(name.sub(SCHEMA_EXTENSION, ""), request: request)
|
|
67
|
+
return not_found unless document
|
|
68
|
+
|
|
69
|
+
content, content_type = document
|
|
70
|
+
respond(200, content_type, content)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def schema_urls(request)
|
|
74
|
+
OpenapiRuby.configuration.schemas.map do |name, schema_config|
|
|
75
|
+
{
|
|
76
|
+
url: "#{request.script_name}/schemas/#{name}.#{Serving.schema_format}",
|
|
77
|
+
name: schema_config.dig(:info, :title) || name.to_s
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def respond(status, content_type, body, headers = {})
|
|
83
|
+
[status, {"content-type" => content_type}.merge(headers), [body]]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def not_found
|
|
87
|
+
respond(404, "application/json", {error: "Not found"}.to_json)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def method_not_allowed
|
|
91
|
+
respond(405, "application/json", {error: "Method not allowed"}.to_json, "allow" => "GET, HEAD")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rake"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require "openapi_ruby/generator/rake_task_support"
|
|
6
|
+
|
|
7
|
+
module OpenapiRuby
|
|
8
|
+
# The Rails engine picks up lib/tasks/*.rake on its own; other hosts add
|
|
9
|
+
#
|
|
10
|
+
# require "openapi_ruby/rake_tasks"
|
|
11
|
+
#
|
|
12
|
+
# to their Rakefile. Both routes end up here, so the task is defined once.
|
|
13
|
+
module RakeTasks
|
|
14
|
+
extend Rake::DSL
|
|
15
|
+
|
|
16
|
+
def self.install!
|
|
17
|
+
return if Rake::Task.task_defined?("openapi_ruby:generate")
|
|
18
|
+
|
|
19
|
+
namespace :openapi_ruby do
|
|
20
|
+
desc "Generate OpenAPI schema files from spec definitions and components"
|
|
21
|
+
task :generate do
|
|
22
|
+
support = OpenapiRuby::Generator::RakeTaskSupport
|
|
23
|
+
framework = ENV.fetch("FRAMEWORK") { support.detect_test_framework }.to_s
|
|
24
|
+
pattern = ENV.fetch("PATTERN") { support.default_pattern_for(framework) }
|
|
25
|
+
|
|
26
|
+
# Spawn a subprocess so the host's env defaults to "test" cleanly,
|
|
27
|
+
# just like rswag did with RSpec::Core::RakeTask.
|
|
28
|
+
script = support.generate_script(framework, pattern)
|
|
29
|
+
command = "bundle exec ruby -e #{Shellwords.escape(script)}"
|
|
30
|
+
|
|
31
|
+
puts "Generating OpenAPI schemas (#{framework})..."
|
|
32
|
+
system(support.subprocess_env, command) || abort("Schema generation failed")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
OpenapiRuby::RakeTasks.install!
|