openapi-ruby 0.1.0 → 2.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/lib/openapi_ruby/adapters/minitest.rb +10 -3
- data/lib/openapi_ruby/adapters/rspec.rb +10 -3
- data/lib/openapi_ruby/components/base.rb +18 -0
- data/lib/openapi_ruby/components/loader.rb +61 -4
- data/lib/openapi_ruby/components/registry.rb +10 -6
- data/lib/openapi_ruby/configuration.rb +8 -0
- data/lib/openapi_ruby/core/document.rb +19 -4
- data/lib/openapi_ruby/core/document_builder.rb +45 -1
- data/lib/openapi_ruby/engine.rb +14 -9
- data/lib/openapi_ruby/generator/schema_writer.rb +9 -0
- data/lib/openapi_ruby/middleware/request_validation.rb +15 -1
- data/lib/openapi_ruby/middleware/response_validation.rb +8 -0
- data/lib/openapi_ruby/middleware/schema_resolver.rb +15 -2
- data/lib/openapi_ruby/testing/request_builder.rb +10 -2
- data/lib/openapi_ruby/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f4d1e0c7be33a19d1264716a6864c964451717c477a2d8be113c636d7ac12744
|
|
4
|
+
data.tar.gz: ef006bb3472303c4aeee1faf6291822a22e0c02c9f2bf81fc7f7296462bb5324
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 80cf5378dfb48cdec73b9a19a57449b4891eddf7282f1ff3e251d77f4f04c7e876fa87ec89feef8dcf5bb10cea22ecd12f1c5dc30fd68a175ecc04ba44a26e1d
|
|
7
|
+
data.tar.gz: 0f3bf8c4096823a12426ec2c289be4200ec45b27262e4f4a991b67d08fa17a65d4673dc0fabda1c67cb9e634ceedb4ace95bc5187034b84ea09920efa0711dd7
|
|
@@ -43,9 +43,16 @@ module OpenapiRuby
|
|
|
43
43
|
request_params = body || params.reject { |k, _| path_param_names(context).include?(k.to_s) }
|
|
44
44
|
request_headers = headers.dup
|
|
45
45
|
|
|
46
|
-
if body
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
if body
|
|
47
|
+
content_type = request_headers["Content-Type"] || context.operations[method.to_s]&.instance_variable_get(:@consumes_list)&.first
|
|
48
|
+
|
|
49
|
+
if content_type&.include?("form-data") || content_type&.include?("x-www-form-urlencoded")
|
|
50
|
+
request_params = body
|
|
51
|
+
request_headers["Content-Type"] ||= content_type
|
|
52
|
+
else
|
|
53
|
+
request_params = body.is_a?(String) ? body : body.to_json
|
|
54
|
+
request_headers["Content-Type"] ||= content_type || "application/json"
|
|
55
|
+
end
|
|
49
56
|
end
|
|
50
57
|
|
|
51
58
|
send_args = {params: request_params}
|
|
@@ -170,9 +170,16 @@ module OpenapiRuby
|
|
|
170
170
|
send_args = {params: body || params}
|
|
171
171
|
send_args[:headers] = headers if headers.any?
|
|
172
172
|
|
|
173
|
-
if
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
if body
|
|
174
|
+
content_type = headers["Content-Type"] || operation&.instance_variable_get(:@consumes_list)&.first
|
|
175
|
+
|
|
176
|
+
if content_type&.include?("form-data") || content_type&.include?("x-www-form-urlencoded")
|
|
177
|
+
send_args[:params] = body
|
|
178
|
+
send_args[:headers] = (headers || {}).merge("Content-Type" => content_type)
|
|
179
|
+
else
|
|
180
|
+
send_args[:params] = body.is_a?(String) ? body : body.to_json
|
|
181
|
+
send_args[:headers] = (headers || {}).merge("Content-Type" => content_type || "application/json")
|
|
182
|
+
end
|
|
176
183
|
end
|
|
177
184
|
|
|
178
185
|
send(method.to_sym, path, **send_args)
|
|
@@ -10,6 +10,7 @@ module OpenapiRuby
|
|
|
10
10
|
base.class_attribute :_skip_key_transformation, default: false
|
|
11
11
|
base.class_attribute :_component_type, default: :schemas
|
|
12
12
|
base.class_attribute :_component_scopes, default: []
|
|
13
|
+
base.class_attribute :_component_scopes_explicitly_set, default: false
|
|
13
14
|
|
|
14
15
|
Registry.instance.register(base) if base.name
|
|
15
16
|
end
|
|
@@ -22,6 +23,7 @@ module OpenapiRuby
|
|
|
22
23
|
subclass._skip_key_transformation = _skip_key_transformation
|
|
23
24
|
subclass._component_type = _component_type
|
|
24
25
|
subclass._component_scopes = _component_scopes.dup
|
|
26
|
+
subclass._component_scopes_explicitly_set = _component_scopes_explicitly_set
|
|
25
27
|
Registry.instance.register(subclass) if subclass.name
|
|
26
28
|
end
|
|
27
29
|
|
|
@@ -45,13 +47,29 @@ module OpenapiRuby
|
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
def component_scopes(*scopes)
|
|
50
|
+
Registry.instance.unregister(self)
|
|
48
51
|
self._component_scopes = scopes.flatten.map(&:to_sym)
|
|
52
|
+
self._component_scopes_explicitly_set = true
|
|
53
|
+
Registry.instance.register(self)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def shared_component
|
|
57
|
+
self._component_scopes = []
|
|
58
|
+
self._component_scopes_explicitly_set = true
|
|
49
59
|
end
|
|
50
60
|
|
|
51
61
|
def component_name
|
|
52
62
|
(name || "Anonymous").demodulize
|
|
53
63
|
end
|
|
54
64
|
|
|
65
|
+
def registry_key
|
|
66
|
+
if _component_scopes.empty?
|
|
67
|
+
component_name
|
|
68
|
+
else
|
|
69
|
+
"#{_component_scopes.sort.join("_")}:#{component_name}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
55
73
|
def to_openapi
|
|
56
74
|
definition = _schema_definition.deep_dup
|
|
57
75
|
|
|
@@ -12,7 +12,7 @@ module OpenapiRuby
|
|
|
12
12
|
|
|
13
13
|
def load!
|
|
14
14
|
define_namespace_modules!
|
|
15
|
-
|
|
15
|
+
load_component_files!
|
|
16
16
|
self
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -63,13 +63,70 @@ module OpenapiRuby
|
|
|
63
63
|
expanded = File.expand_path(path)
|
|
64
64
|
next unless Dir.exist?(expanded)
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
define_nested_modules(expanded, expanded)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def define_nested_modules(base_path, current_path)
|
|
71
|
+
Dir.children(current_path).select { |f| File.directory?(File.join(current_path, f)) }.each do |dir|
|
|
72
|
+
mod_name = dir.camelize.to_sym
|
|
73
|
+
Object.const_set(mod_name, Module.new) unless Object.const_defined?(mod_name)
|
|
74
|
+
|
|
75
|
+
child_path = File.join(current_path, dir)
|
|
76
|
+
define_nested_modules(base_path, child_path)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def load_component_files!
|
|
81
|
+
scope_paths = OpenapiRuby.configuration.component_scope_paths
|
|
82
|
+
|
|
83
|
+
if scope_paths.any?
|
|
84
|
+
load_with_scope_inference(scope_paths)
|
|
85
|
+
else
|
|
86
|
+
component_files.each { |f| require f }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def load_with_scope_inference(scope_paths)
|
|
91
|
+
@paths.each do |base_path|
|
|
92
|
+
expanded = File.expand_path(base_path)
|
|
93
|
+
next unless Dir.exist?(expanded)
|
|
94
|
+
|
|
95
|
+
files = Dir[File.join(expanded, "**", "*.rb")].sort
|
|
96
|
+
|
|
97
|
+
files.each do |file|
|
|
98
|
+
relative = file.sub("#{expanded}/", "")
|
|
99
|
+
inferred_scope = infer_scope(relative, scope_paths)
|
|
100
|
+
|
|
101
|
+
registered_before = Registry.instance.all_registered_classes.dup
|
|
102
|
+
require file
|
|
103
|
+
registered_after = Registry.instance.all_registered_classes
|
|
104
|
+
|
|
105
|
+
new_classes = registered_after - registered_before
|
|
106
|
+
new_classes.each do |klass|
|
|
107
|
+
next if klass._component_scopes_explicitly_set
|
|
108
|
+
|
|
109
|
+
if inferred_scope == :shared
|
|
110
|
+
# Shared components have empty scopes (included in all specs)
|
|
111
|
+
klass._component_scopes = []
|
|
112
|
+
elsif inferred_scope
|
|
113
|
+
Registry.instance.unregister(klass)
|
|
114
|
+
klass._component_scopes = [inferred_scope]
|
|
115
|
+
Registry.instance.register(klass)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
69
118
|
end
|
|
70
119
|
end
|
|
71
120
|
end
|
|
72
121
|
|
|
122
|
+
def infer_scope(relative_path, scope_paths)
|
|
123
|
+
# Match longest prefix first for specificity
|
|
124
|
+
scope_paths.sort_by { |prefix, _| -prefix.length }.each do |prefix, scope|
|
|
125
|
+
return scope&.to_sym if relative_path.start_with?("#{prefix}/")
|
|
126
|
+
end
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
73
130
|
def component_files
|
|
74
131
|
@paths.flat_map do |path|
|
|
75
132
|
expanded = File.expand_path(path)
|
|
@@ -13,21 +13,21 @@ module OpenapiRuby
|
|
|
13
13
|
|
|
14
14
|
def register(component_class)
|
|
15
15
|
type = component_class._component_type
|
|
16
|
-
|
|
16
|
+
key = component_class.registry_key
|
|
17
17
|
|
|
18
18
|
@components[type] ||= {}
|
|
19
19
|
|
|
20
|
-
if @components[type].key?(
|
|
21
|
-
raise DuplicateComponentError, "Component '#{
|
|
20
|
+
if @components[type].key?(key) && @components[type][key] != component_class
|
|
21
|
+
raise DuplicateComponentError, "Component '#{component_class.component_name}' already registered under #{type}"
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
@components[type][
|
|
24
|
+
@components[type][key] = component_class
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def unregister(component_class)
|
|
28
28
|
type = component_class._component_type
|
|
29
|
-
|
|
30
|
-
@components[type]&.delete(
|
|
29
|
+
key = component_class.registry_key
|
|
30
|
+
@components[type]&.delete(key)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def components_for(type)
|
|
@@ -38,6 +38,10 @@ module OpenapiRuby
|
|
|
38
38
|
@components.keys
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
def all_registered_classes
|
|
42
|
+
@components.values.flat_map(&:values)
|
|
43
|
+
end
|
|
44
|
+
|
|
41
45
|
def grouped_by_type
|
|
42
46
|
@components.dup
|
|
43
47
|
end
|
|
@@ -8,8 +8,12 @@ module OpenapiRuby
|
|
|
8
8
|
|
|
9
9
|
# Components
|
|
10
10
|
attr_accessor :component_paths
|
|
11
|
+
attr_accessor :component_scope_paths
|
|
11
12
|
attr_accessor :camelize_keys, :key_transform, :response_validation, :strict_query_params,
|
|
12
13
|
:coerce_params, :error_handler, :schema_output_format, :validate_responses_in_tests, :ui_path, :ui_config, :coverage_report_path
|
|
14
|
+
attr_accessor :strict_reference_validation
|
|
15
|
+
attr_accessor :auto_validation_error_response
|
|
16
|
+
attr_accessor :validation_error_schema
|
|
13
17
|
|
|
14
18
|
# Middleware (runtime validation)
|
|
15
19
|
attr_accessor :request_validation
|
|
@@ -26,6 +30,7 @@ module OpenapiRuby
|
|
|
26
30
|
def initialize
|
|
27
31
|
@schemas = {}
|
|
28
32
|
@component_paths = ["app/api_components"]
|
|
33
|
+
@component_scope_paths = {}
|
|
29
34
|
@camelize_keys = true
|
|
30
35
|
@key_transform = nil
|
|
31
36
|
@request_validation = :disabled
|
|
@@ -39,6 +44,9 @@ module OpenapiRuby
|
|
|
39
44
|
@ui_enabled = false
|
|
40
45
|
@ui_path = "/api-docs"
|
|
41
46
|
@ui_config = {}
|
|
47
|
+
@strict_reference_validation = true
|
|
48
|
+
@auto_validation_error_response = true
|
|
49
|
+
@validation_error_schema = nil
|
|
42
50
|
@coverage_enabled = false
|
|
43
51
|
@coverage_report_path = "tmp/openapi_coverage.json"
|
|
44
52
|
end
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
module OpenapiRuby
|
|
4
4
|
module Core
|
|
5
5
|
class Document
|
|
6
|
-
|
|
6
|
+
MIN_OPENAPI_VERSION = "3.1.0"
|
|
7
|
+
DEFAULT_OPENAPI_VERSION = "3.1.0"
|
|
7
8
|
|
|
8
9
|
attr_reader :data
|
|
9
10
|
|
|
10
|
-
def initialize(info: {}, servers: [])
|
|
11
|
+
def initialize(info: {}, servers: [], openapi_version: DEFAULT_OPENAPI_VERSION)
|
|
12
|
+
validate_version!(openapi_version)
|
|
11
13
|
@data = {
|
|
12
|
-
"openapi" =>
|
|
14
|
+
"openapi" => openapi_version,
|
|
13
15
|
"info" => normalize_info(info),
|
|
14
16
|
"paths" => {}
|
|
15
17
|
}
|
|
@@ -34,7 +36,13 @@ module OpenapiRuby
|
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def to_h
|
|
37
|
-
@data
|
|
39
|
+
result = @data.dup
|
|
40
|
+
result["paths"] = result["paths"].sort.to_h if result["paths"]
|
|
41
|
+
if result["components"]
|
|
42
|
+
result["components"] = result["components"].transform_values { |v| v.sort.to_h }
|
|
43
|
+
end
|
|
44
|
+
result["tags"] = result["tags"].sort_by { |t| t["name"].to_s } if result["tags"]
|
|
45
|
+
result
|
|
38
46
|
end
|
|
39
47
|
|
|
40
48
|
def to_json(*_args)
|
|
@@ -59,6 +67,13 @@ module OpenapiRuby
|
|
|
59
67
|
|
|
60
68
|
private
|
|
61
69
|
|
|
70
|
+
def validate_version!(version)
|
|
71
|
+
if Gem::Version.new(version) < Gem::Version.new(MIN_OPENAPI_VERSION)
|
|
72
|
+
raise OpenapiRuby::ConfigurationError,
|
|
73
|
+
"OpenAPI version must be >= #{MIN_OPENAPI_VERSION}, got #{version}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
62
77
|
def normalize_info(info)
|
|
63
78
|
result = info.transform_keys(&:to_s)
|
|
64
79
|
result["title"] ||= ""
|
|
@@ -7,7 +7,8 @@ module OpenapiRuby
|
|
|
7
7
|
@spec_config = spec_config
|
|
8
8
|
@document = Document.new(
|
|
9
9
|
info: spec_config[:info] || {},
|
|
10
|
-
servers: spec_config[:servers] || []
|
|
10
|
+
servers: spec_config[:servers] || [],
|
|
11
|
+
openapi_version: spec_config[:openapi_version] || Document::DEFAULT_OPENAPI_VERSION
|
|
11
12
|
)
|
|
12
13
|
@paths = {}
|
|
13
14
|
@components = {}
|
|
@@ -33,6 +34,7 @@ module OpenapiRuby
|
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def build
|
|
37
|
+
inject_validation_error_responses! if OpenapiRuby.configuration.auto_validation_error_response
|
|
36
38
|
@paths.each { |template, path_item| @document.add_path(template, path_item) }
|
|
37
39
|
@document.set_components(@components)
|
|
38
40
|
@document.set_security(@security)
|
|
@@ -43,6 +45,48 @@ module OpenapiRuby
|
|
|
43
45
|
def to_h
|
|
44
46
|
build.to_h
|
|
45
47
|
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def inject_validation_error_responses!
|
|
52
|
+
# Add ValidationError response component
|
|
53
|
+
@components["responses"] ||= {}
|
|
54
|
+
@components["responses"]["ValidationError"] ||= {
|
|
55
|
+
"description" => "Request validation failed",
|
|
56
|
+
"content" => {
|
|
57
|
+
"application/json" => {
|
|
58
|
+
"schema" => validation_error_component_schema
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Add 400 to every operation that doesn't already define one
|
|
64
|
+
@paths.each_value do |path_item|
|
|
65
|
+
path_item.each do |key, operation|
|
|
66
|
+
next unless operation.is_a?(Hash) && operation.key?("responses")
|
|
67
|
+
next if key == "parameters"
|
|
68
|
+
|
|
69
|
+
operation["responses"]["400"] ||= {"$ref" => "#/components/responses/ValidationError"}
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def validation_error_component_schema
|
|
75
|
+
custom = OpenapiRuby.configuration.validation_error_schema
|
|
76
|
+
return custom if custom
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
"type" => "object",
|
|
80
|
+
"properties" => {
|
|
81
|
+
"error" => {"type" => "string"},
|
|
82
|
+
"details" => {
|
|
83
|
+
"type" => "array",
|
|
84
|
+
"items" => {"type" => "string"}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"required" => %w[error details]
|
|
88
|
+
}
|
|
89
|
+
end
|
|
46
90
|
end
|
|
47
91
|
end
|
|
48
92
|
end
|
data/lib/openapi_ruby/engine.rb
CHANGED
|
@@ -8,21 +8,29 @@ module OpenapiRuby
|
|
|
8
8
|
config = OpenapiRuby.configuration
|
|
9
9
|
|
|
10
10
|
if config.request_validation != :disabled || config.response_validation != :disabled
|
|
11
|
-
|
|
11
|
+
config.schemas.each do |name, schema_config|
|
|
12
|
+
schema_path = resolve_schema_path(config, name)
|
|
13
|
+
next unless schema_path && File.exist?(schema_path)
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
resolver = Middleware::SchemaResolver.new(
|
|
16
|
+
spec_path: schema_path,
|
|
17
|
+
strict_reference_validation: config.strict_reference_validation
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
prefix = schema_config[:prefix]
|
|
15
21
|
|
|
16
22
|
if config.request_validation != :disabled
|
|
17
23
|
app.middleware.use Middleware::RequestValidation,
|
|
18
24
|
schema_resolver: resolver,
|
|
19
|
-
mode: config.request_validation
|
|
25
|
+
mode: config.request_validation,
|
|
26
|
+
prefix: prefix
|
|
20
27
|
end
|
|
21
28
|
|
|
22
29
|
if config.response_validation != :disabled
|
|
23
30
|
app.middleware.use Middleware::ResponseValidation,
|
|
24
31
|
schema_resolver: resolver,
|
|
25
|
-
mode: config.response_validation
|
|
32
|
+
mode: config.response_validation,
|
|
33
|
+
prefix: prefix
|
|
26
34
|
end
|
|
27
35
|
end
|
|
28
36
|
end
|
|
@@ -46,10 +54,7 @@ module OpenapiRuby
|
|
|
46
54
|
|
|
47
55
|
private
|
|
48
56
|
|
|
49
|
-
def
|
|
50
|
-
return nil if config.schemas.empty?
|
|
51
|
-
|
|
52
|
-
schema_name = config.schemas.keys.first
|
|
57
|
+
def resolve_schema_path(config, schema_name)
|
|
53
58
|
ext = (config.schema_output_format == :json) ? "json" : "yaml"
|
|
54
59
|
Rails.root.join(config.schema_output_dir, "#{schema_name}.#{ext}").to_s
|
|
55
60
|
end
|
|
@@ -23,6 +23,7 @@ module OpenapiRuby
|
|
|
23
23
|
|
|
24
24
|
def write!
|
|
25
25
|
document = build_document
|
|
26
|
+
validate_document!(document) if OpenapiRuby.configuration.strict_reference_validation
|
|
26
27
|
output_path = File.join(output_dir, filename)
|
|
27
28
|
FileUtils.mkdir_p(output_dir)
|
|
28
29
|
File.write(output_path, format_output(document))
|
|
@@ -56,6 +57,14 @@ module OpenapiRuby
|
|
|
56
57
|
"#{@schema_name}.#{ext}"
|
|
57
58
|
end
|
|
58
59
|
|
|
60
|
+
def validate_document!(document)
|
|
61
|
+
errors = document.validate
|
|
62
|
+
return if errors.empty?
|
|
63
|
+
|
|
64
|
+
error_messages = errors.first(10).map { |e| e["error"] || e.to_s }
|
|
65
|
+
warn "[openapi_ruby] Generated schema '#{@schema_name}' has validation errors:\n#{error_messages.join("\n")}"
|
|
66
|
+
end
|
|
67
|
+
|
|
59
68
|
def format_output(document)
|
|
60
69
|
if OpenapiRuby.configuration.schema_output_format == :json
|
|
61
70
|
document.to_json
|
|
@@ -10,13 +10,22 @@ module OpenapiRuby
|
|
|
10
10
|
@coerce = options.fetch(:coerce, OpenapiRuby.configuration.coerce_params)
|
|
11
11
|
@error_handler = options[:error_handler] || ErrorHandler.new
|
|
12
12
|
@mode = options.fetch(:mode, OpenapiRuby.configuration.request_validation)
|
|
13
|
+
@prefix = options[:prefix]
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def call(env)
|
|
16
17
|
return @app.call(env) if @mode == :disabled
|
|
17
18
|
|
|
18
19
|
request = Rack::Request.new(env)
|
|
19
|
-
|
|
20
|
+
|
|
21
|
+
# Skip if request doesn't match prefix
|
|
22
|
+
if @prefix && !request.path_info.start_with?(@prefix)
|
|
23
|
+
return @app.call(env)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Strip prefix for path matching
|
|
27
|
+
match_path = @prefix ? request.path_info.sub(@prefix, "") : request.path_info
|
|
28
|
+
result = @resolver.find_operation(request.request_method, match_path)
|
|
20
29
|
|
|
21
30
|
if result.nil?
|
|
22
31
|
return strict? ? @error_handler.not_found(request.path_info) : @app.call(env)
|
|
@@ -194,6 +203,11 @@ module OpenapiRuby
|
|
|
194
203
|
|
|
195
204
|
if media_type&.include?("json")
|
|
196
205
|
JSON.parse(content)
|
|
206
|
+
elsif media_type&.include?("x-www-form-urlencoded")
|
|
207
|
+
Rack::Utils.parse_nested_query(content)
|
|
208
|
+
elsif media_type&.include?("form-data")
|
|
209
|
+
# Multipart form data is already parsed by Rack into params
|
|
210
|
+
nil
|
|
197
211
|
else
|
|
198
212
|
content
|
|
199
213
|
end
|
|
@@ -9,11 +9,19 @@ module OpenapiRuby
|
|
|
9
9
|
@error_handler = options[:error_handler] || ErrorHandler.new
|
|
10
10
|
@mode = options.fetch(:mode, OpenapiRuby.configuration.response_validation)
|
|
11
11
|
@validate_success_only = options.fetch(:validate_success_only, true)
|
|
12
|
+
@prefix = options[:prefix]
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def call(env)
|
|
15
16
|
return @app.call(env) if @mode == :disabled
|
|
16
17
|
|
|
18
|
+
request = Rack::Request.new(env)
|
|
19
|
+
|
|
20
|
+
# Skip if request doesn't match prefix
|
|
21
|
+
if @prefix && !request.path_info.start_with?(@prefix)
|
|
22
|
+
return @app.call(env)
|
|
23
|
+
end
|
|
24
|
+
|
|
17
25
|
status, headers, body = @app.call(env)
|
|
18
26
|
|
|
19
27
|
# Skip validation for certain status codes
|
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
module OpenapiRuby
|
|
4
4
|
module Middleware
|
|
5
5
|
class SchemaResolver
|
|
6
|
-
def initialize(spec_path: nil, document: nil)
|
|
6
|
+
def initialize(spec_path: nil, document: nil, strict_reference_validation: true)
|
|
7
7
|
@spec_path = spec_path
|
|
8
8
|
@document = document
|
|
9
|
+
@strict_reference_validation = strict_reference_validation
|
|
9
10
|
@path_matcher = nil
|
|
10
11
|
@schemer = nil
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def document
|
|
14
|
-
@document ||= load_document
|
|
15
|
+
@document ||= load_document.tap { |doc| validate_document!(doc) }
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def schemer
|
|
@@ -39,6 +40,18 @@ module OpenapiRuby
|
|
|
39
40
|
|
|
40
41
|
private
|
|
41
42
|
|
|
43
|
+
def validate_document!(doc)
|
|
44
|
+
return unless @strict_reference_validation
|
|
45
|
+
|
|
46
|
+
schemer = JSONSchemer.openapi(doc)
|
|
47
|
+
errors = schemer.validate.to_a
|
|
48
|
+
return if errors.empty?
|
|
49
|
+
|
|
50
|
+
error_messages = errors.first(5).map { |e| e["error"] || e.to_s }
|
|
51
|
+
raise OpenapiRuby::ConfigurationError,
|
|
52
|
+
"OpenAPI document validation failed:\n#{error_messages.join("\n")}"
|
|
53
|
+
end
|
|
54
|
+
|
|
42
55
|
def load_document
|
|
43
56
|
raise ConfigurationError, "No spec_path configured for middleware" unless @spec_path
|
|
44
57
|
|
|
@@ -62,9 +62,17 @@ module OpenapiRuby
|
|
|
62
62
|
def build_body
|
|
63
63
|
return nil unless @body_value
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
content_type = @headers&.fetch("Content-Type", nil)
|
|
66
|
+
consumes = @operation.instance_variable_get(:@consumes_list)
|
|
67
|
+
content_type ||= consumes&.first
|
|
68
|
+
|
|
69
|
+
if content_type&.include?("form-data") || content_type&.include?("x-www-form-urlencoded")
|
|
70
|
+
@body_value
|
|
71
|
+
elsif @body_value.is_a?(Hash) || @body_value.is_a?(Array)
|
|
72
|
+
@body_value.to_json
|
|
73
|
+
else
|
|
74
|
+
@body_value
|
|
66
75
|
end
|
|
67
|
-
@body_value
|
|
68
76
|
end
|
|
69
77
|
|
|
70
78
|
def deep_stringify(value)
|
data/lib/openapi_ruby/version.rb
CHANGED
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: 0.
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Morten Hartvig
|
|
@@ -140,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
140
140
|
- !ruby/object:Gem::Version
|
|
141
141
|
version: '0'
|
|
142
142
|
requirements: []
|
|
143
|
-
rubygems_version:
|
|
143
|
+
rubygems_version: 3.6.9
|
|
144
144
|
specification_version: 4
|
|
145
145
|
summary: OpenAPI 3.1 toolkit for Rails — spec generation, schema components, and runtime
|
|
146
146
|
validation
|