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
|
@@ -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
|
data/lib/openapi_ruby/version.rb
CHANGED
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"
|
data/lib/tasks/openapi_ruby.rake
CHANGED
|
@@ -1,21 +1,3 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "openapi_ruby/
|
|
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.
|
|
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
|
-
-
|
|
55
|
-
|
|
56
|
-
|
|
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: []
|
|
@@ -110,19 +96,26 @@ files:
|
|
|
110
96
|
- lib/openapi_ruby/generator/rake_task_support.rb
|
|
111
97
|
- lib/openapi_ruby/generator/schema_writer.rb
|
|
112
98
|
- lib/openapi_ruby/generator/test_schema_suppressor.rb
|
|
99
|
+
- lib/openapi_ruby/hanami.rb
|
|
100
|
+
- lib/openapi_ruby/host.rb
|
|
113
101
|
- lib/openapi_ruby/middleware/coercion.rb
|
|
114
102
|
- lib/openapi_ruby/middleware/error_handler.rb
|
|
103
|
+
- lib/openapi_ruby/middleware/installer.rb
|
|
115
104
|
- lib/openapi_ruby/middleware/path_matcher.rb
|
|
116
105
|
- lib/openapi_ruby/middleware/request_validation.rb
|
|
117
106
|
- lib/openapi_ruby/middleware/response_validation.rb
|
|
118
107
|
- lib/openapi_ruby/middleware/schema_resolver.rb
|
|
119
108
|
- lib/openapi_ruby/minitest.rb
|
|
109
|
+
- lib/openapi_ruby/rack_app.rb
|
|
110
|
+
- lib/openapi_ruby/rake_tasks.rb
|
|
120
111
|
- lib/openapi_ruby/rspec.rb
|
|
112
|
+
- lib/openapi_ruby/serving.rb
|
|
121
113
|
- lib/openapi_ruby/testing/assertions.rb
|
|
122
114
|
- lib/openapi_ruby/testing/coverage.rb
|
|
123
115
|
- lib/openapi_ruby/testing/request_builder.rb
|
|
124
116
|
- lib/openapi_ruby/testing/request_validator.rb
|
|
125
117
|
- lib/openapi_ruby/testing/response_validator.rb
|
|
118
|
+
- lib/openapi_ruby/testing/transport.rb
|
|
126
119
|
- lib/openapi_ruby/version.rb
|
|
127
120
|
- lib/tasks/openapi_ruby.rake
|
|
128
121
|
homepage: https://github.com/openapi-ruby/openapi-ruby
|
|
@@ -149,6 +142,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
149
142
|
requirements: []
|
|
150
143
|
rubygems_version: 3.6.9
|
|
151
144
|
specification_version: 4
|
|
152
|
-
summary: OpenAPI 3.0/3.1 toolkit for Rails — spec generation, schema components,
|
|
153
|
-
runtime validation
|
|
145
|
+
summary: OpenAPI 3.0/3.1 toolkit for Rails and Hanami — spec generation, schema components,
|
|
146
|
+
and runtime validation
|
|
154
147
|
test_files: []
|