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.
@@ -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!
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiRuby
4
+ # Host-neutral logic behind the served schema documents and the Swagger UI.
5
+ # The Rails controllers and RackApp are both thin shells over this, so the
6
+ # two hosts cannot drift apart.
7
+ module Serving
8
+ module_function
9
+
10
+ def schema_names
11
+ OpenapiRuby.configuration.schemas.keys.map(&:to_s)
12
+ end
13
+
14
+ def schema_config_for(schema_name)
15
+ OpenapiRuby.configuration.schemas[schema_name.to_sym]
16
+ end
17
+
18
+ def schema_file_path(schema_name)
19
+ config = OpenapiRuby.configuration
20
+ ext = (config.schema_output_format == :json) ? "json" : "yaml"
21
+ File.join(OpenapiRuby.app_root, config.schema_output_dir, "#{schema_name}.#{ext}")
22
+ end
23
+
24
+ def schema_format
25
+ (OpenapiRuby.configuration.schema_output_format == :json) ? :json : :yaml
26
+ end
27
+
28
+ # Returns [content, content_type], or nil when the schema is unknown or
29
+ # has not been generated yet — callers turn that into a 404.
30
+ #
31
+ # `request` is handed to the configured :openapi_filter untouched. Rails
32
+ # passes an ActionDispatch::Request and RackApp a Rack::Request; the hook
33
+ # only ever needs the shared Rack request API.
34
+ def schema_document(schema_name, request: nil)
35
+ schema_config = schema_config_for(schema_name)
36
+ return nil unless schema_config
37
+
38
+ file_path = schema_file_path(schema_name)
39
+ return nil unless File.exist?(file_path)
40
+
41
+ content = File.read(file_path)
42
+
43
+ if schema_config[:openapi_filter]
44
+ doc = parse_content(file_path, content)
45
+ schema_config[:openapi_filter].call(doc, request)
46
+ content = serialize_doc(file_path, doc)
47
+ end
48
+
49
+ [content, content_type_for(file_path)]
50
+ end
51
+
52
+ def content_type_for(file_path)
53
+ file_path.end_with?(".json") ? "application/json" : "application/x-yaml"
54
+ end
55
+
56
+ def parse_content(file_path, content)
57
+ if file_path.end_with?(".json")
58
+ JSON.parse(content)
59
+ else
60
+ YAML.safe_load(content, permitted_classes: [Date, Time])
61
+ end
62
+ end
63
+
64
+ def serialize_doc(file_path, doc)
65
+ if file_path.end_with?(".json")
66
+ JSON.pretty_generate(doc)
67
+ else
68
+ doc.to_yaml
69
+ end
70
+ end
71
+
72
+ def oauth2_redirect_file
73
+ File.join(gem_root, "app", "views", "openapi_ruby", "oauth2_redirect.html")
74
+ end
75
+
76
+ def gem_root
77
+ File.expand_path("../..", __dir__)
78
+ end
79
+
80
+ # `schema_urls` is an array of {url:, name:} — the caller builds them,
81
+ # since only it knows how the docs are mounted.
82
+ def swagger_ui_html(schema_urls:, ui_config: {})
83
+ <<~HTML
84
+ <!DOCTYPE html>
85
+ <html lang="en">
86
+ <head>
87
+ <meta charset="UTF-8">
88
+ <title>#{ui_config[:title] || "API Documentation"}</title>
89
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
90
+ <style>
91
+ html { box-sizing: border-box; overflow-y: scroll; }
92
+ *, *:before, *:after { box-sizing: inherit; }
93
+ body { margin: 0; background: #fafafa; }
94
+ </style>
95
+ </head>
96
+ <body>
97
+ <div id="swagger-ui"></div>
98
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
99
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
100
+ <script>
101
+ SwaggerUIBundle({
102
+ #{schema_urls_js(schema_urls)},
103
+ dom_id: '#swagger-ui',
104
+ deepLinking: true,
105
+ presets: [
106
+ SwaggerUIBundle.presets.apis,
107
+ SwaggerUIStandalonePreset
108
+ ],
109
+ plugins: [
110
+ SwaggerUIBundle.plugins.DownloadUrl
111
+ ],
112
+ layout: "#{(schema_urls.size > 1) ? "StandaloneLayout" : "BaseLayout"}",
113
+ #{ui_config_js(ui_config)}
114
+ });
115
+ </script>
116
+ </body>
117
+ </html>
118
+ HTML
119
+ end
120
+
121
+ def schema_urls_js(schema_urls)
122
+ if schema_urls.size > 1
123
+ "urls: #{schema_urls.to_json}"
124
+ else
125
+ "url: \"#{schema_urls.first&.fetch(:url)}\""
126
+ end
127
+ end
128
+
129
+ def ui_config_js(ui_config)
130
+ ui_config.except(:title).map { |k, v|
131
+ "#{k}: #{v.to_json}"
132
+ }.join(",\n ")
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiRuby
4
+ module Testing
5
+ # Bridges the adapters onto whichever request-issuing API the host's test
6
+ # context provides. Rails integration tests take keyword arguments and
7
+ # expose `response`; rack-test (Hanami, Sinatra, bare Rack) takes
8
+ # positional arguments plus a Rack env and exposes `last_response`.
9
+ #
10
+ # Both response objects answer #status, #body and #headers, so callers
11
+ # need no further normalisation.
12
+ module Transport
13
+ # Rails is checked first: a suite can include Rack::Test::Methods
14
+ # alongside the integration helpers, and in that case the Rails session
15
+ # is the one that boots the app under test.
16
+ def self.for(context)
17
+ if context.respond_to?(:integration_session)
18
+ RailsIntegration.new(context)
19
+ elsif context.respond_to?(:last_response)
20
+ require_app!(context)
21
+ RackTest.new(context)
22
+ elsif OpenapiRuby.rails_host?
23
+ # A hand-rolled harness that defines the verb methods itself. Left
24
+ # working rather than second-guessed.
25
+ RailsIntegration.new(context)
26
+ else
27
+ # Without rack-test, dispatch would land on the example-group DSL's
28
+ # own `get`, and the user would get told that `get` is unavailable
29
+ # inside an example — true, and no help at all here.
30
+ raise OpenapiRuby::Error,
31
+ "openapi_ruby found no way to issue requests from #{describe(context)}. " \
32
+ "Add rack-test to your bundle, `include Rack::Test::Methods`, and define `app`."
33
+ end
34
+ end
35
+
36
+ # rack-test resolves `app` lazily, so a missing one surfaces as a bare
37
+ # NameError from inside the gem rather than as the setup mistake it is.
38
+ def self.require_app!(context)
39
+ return if context.respond_to?(:app)
40
+
41
+ raise OpenapiRuby::Error,
42
+ "openapi_ruby needs the Rack app under test in #{describe(context)}. " \
43
+ "Define it as `let(:app) { MyApp }` in RSpec, or an `app` method in Minitest."
44
+ end
45
+
46
+ def self.describe(context)
47
+ context.is_a?(Class) ? context.name.to_s : context.class.name.to_s
48
+ end
49
+
50
+ class RailsIntegration
51
+ def initialize(context)
52
+ @context = context
53
+ end
54
+
55
+ def dispatch(method, path, params: nil, headers: nil)
56
+ args = {}
57
+ args[:params] = params unless params.nil?
58
+ args[:headers] = headers unless headers.nil?
59
+ @context.send(method.to_sym, path, **args)
60
+ end
61
+
62
+ def response
63
+ @context.response
64
+ end
65
+ end
66
+
67
+ class RackTest
68
+ # Rack keeps these two out of the HTTP_ namespace.
69
+ UNPREFIXED_HEADERS = {
70
+ "content-type" => "CONTENT_TYPE",
71
+ "content-length" => "CONTENT_LENGTH"
72
+ }.freeze
73
+
74
+ def initialize(context)
75
+ @context = context
76
+ end
77
+
78
+ # rack-test treats a String `params` as the request body and a Hash as
79
+ # form/query data, which is the same split the adapters already make.
80
+ def dispatch(method, path, params: nil, headers: nil)
81
+ @context.send(method.to_sym, path, params || {}, rack_env(headers || {}))
82
+ end
83
+
84
+ def response
85
+ @context.last_response
86
+ end
87
+
88
+ private
89
+
90
+ def rack_env(headers)
91
+ headers.each_with_object({}) do |(name, value), env|
92
+ key = name.to_s
93
+ env[UNPREFIXED_HEADERS.fetch(key.downcase) { "HTTP_#{key.upcase.tr("-", "_")}" }] = value.to_s
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiRuby
4
- VERSION = "4.0.3"
4
+ VERSION = "4.2.0"
5
5
  end
data/lib/openapi_ruby.rb CHANGED
@@ -12,6 +12,7 @@ require "yaml"
12
12
 
13
13
  require_relative "openapi_ruby/version"
14
14
  require_relative "openapi_ruby/errors"
15
+ require_relative "openapi_ruby/host"
15
16
  require_relative "openapi_ruby/configuration"
16
17
 
17
18
  module OpenapiRuby
@@ -57,6 +58,7 @@ require_relative "openapi_ruby/dsl/response_context"
57
58
  require_relative "openapi_ruby/dsl/operation_context"
58
59
  require_relative "openapi_ruby/dsl/context"
59
60
  require_relative "openapi_ruby/dsl/metadata_store"
61
+ require_relative "openapi_ruby/testing/transport"
60
62
  require_relative "openapi_ruby/testing/request_builder"
61
63
  require_relative "openapi_ruby/testing/response_validator"
62
64
  require_relative "openapi_ruby/testing/request_validator"
@@ -70,6 +72,9 @@ require_relative "openapi_ruby/middleware/error_handler"
70
72
  require_relative "openapi_ruby/middleware/schema_resolver"
71
73
  require_relative "openapi_ruby/middleware/request_validation"
72
74
  require_relative "openapi_ruby/middleware/response_validation"
75
+ require_relative "openapi_ruby/middleware/installer"
76
+ require_relative "openapi_ruby/serving"
77
+ require_relative "openapi_ruby/rack_app"
73
78
  require_relative "openapi_ruby/controller_helpers"
74
79
  if defined?(Rails::Engine)
75
80
  require_relative "openapi_ruby/engine"
@@ -1,21 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "openapi_ruby/generator/rake_task_support"
4
-
5
- namespace :openapi_ruby do
6
- desc "Generate OpenAPI schema files from spec definitions and components"
7
- task :generate do
8
- support = OpenapiRuby::Generator::RakeTaskSupport
9
- framework = ENV.fetch("FRAMEWORK") { support.detect_test_framework }.to_s
10
- pattern = ENV.fetch("PATTERN") { support.default_pattern_for(framework) }
11
-
12
- # Spawn a subprocess so RAILS_ENV defaults to "test" cleanly,
13
- # just like rswag did with RSpec::Core::RakeTask.
14
- env = {"RAILS_ENV" => ENV.fetch("RAILS_ENV", "test"), "OPENAPI_RUBY_GENERATING" => "true"}
15
- script = support.generate_script(framework, pattern)
16
- command = "bundle exec ruby -e #{Shellwords.escape(script)}"
17
-
18
- puts "Generating OpenAPI schemas (#{framework})..."
19
- system(env, command) || abort("Schema generation failed")
20
- end
21
- end
3
+ require "openapi_ruby/rake_tasks"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.3
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Morten Hartvig
@@ -51,23 +51,9 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '2.0'
54
- - !ruby/object:Gem::Dependency
55
- name: railties
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '7.0'
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '7.0'
68
- description: A unified OpenAPI toolkit for Rails that combines test-driven spec generation,
69
- reusable schema components as Ruby classes, and runtime request/response validation
70
- middleware. Supports OpenAPI 3.0 and 3.1. Works with both RSpec and Minitest.
54
+ description: A unified OpenAPI toolkit for Rails and Hanami that combines test-driven
55
+ spec generation, reusable schema components as Ruby classes, and runtime request/response
56
+ validation middleware. Supports OpenAPI 3.0 and 3.1. Works with both RSpec and Minitest.
71
57
  email:
72
58
  - morten@hartvigsen.dev
73
59
  executables: []
@@ -88,6 +74,7 @@ files:
88
74
  - lib/generators/openapi_ruby/install/templates/openapi_helper.rb.tt
89
75
  - lib/openapi-ruby.rb
90
76
  - lib/openapi_ruby.rb
77
+ - lib/openapi_ruby/adapters/context_resolution.rb
91
78
  - lib/openapi_ruby/adapters/minitest.rb
92
79
  - lib/openapi_ruby/adapters/rspec.rb
93
80
  - lib/openapi_ruby/components/base.rb
@@ -109,19 +96,26 @@ files:
109
96
  - lib/openapi_ruby/generator/rake_task_support.rb
110
97
  - lib/openapi_ruby/generator/schema_writer.rb
111
98
  - lib/openapi_ruby/generator/test_schema_suppressor.rb
99
+ - lib/openapi_ruby/hanami.rb
100
+ - lib/openapi_ruby/host.rb
112
101
  - lib/openapi_ruby/middleware/coercion.rb
113
102
  - lib/openapi_ruby/middleware/error_handler.rb
103
+ - lib/openapi_ruby/middleware/installer.rb
114
104
  - lib/openapi_ruby/middleware/path_matcher.rb
115
105
  - lib/openapi_ruby/middleware/request_validation.rb
116
106
  - lib/openapi_ruby/middleware/response_validation.rb
117
107
  - lib/openapi_ruby/middleware/schema_resolver.rb
118
108
  - lib/openapi_ruby/minitest.rb
109
+ - lib/openapi_ruby/rack_app.rb
110
+ - lib/openapi_ruby/rake_tasks.rb
119
111
  - lib/openapi_ruby/rspec.rb
112
+ - lib/openapi_ruby/serving.rb
120
113
  - lib/openapi_ruby/testing/assertions.rb
121
114
  - lib/openapi_ruby/testing/coverage.rb
122
115
  - lib/openapi_ruby/testing/request_builder.rb
123
116
  - lib/openapi_ruby/testing/request_validator.rb
124
117
  - lib/openapi_ruby/testing/response_validator.rb
118
+ - lib/openapi_ruby/testing/transport.rb
125
119
  - lib/openapi_ruby/version.rb
126
120
  - lib/tasks/openapi_ruby.rake
127
121
  homepage: https://github.com/openapi-ruby/openapi-ruby
@@ -148,6 +142,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
142
  requirements: []
149
143
  rubygems_version: 3.6.9
150
144
  specification_version: 4
151
- summary: OpenAPI 3.0/3.1 toolkit for Rails — spec generation, schema components, and
152
- runtime validation
145
+ summary: OpenAPI 3.0/3.1 toolkit for Rails and Hanami — spec generation, schema components,
146
+ and runtime validation
153
147
  test_files: []