openapi-ruby 3.0.3 → 3.1.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/rspec.rb +1 -1
- data/lib/openapi_ruby/components/loader.rb +19 -1
- data/lib/openapi_ruby/components/registry.rb +17 -6
- data/lib/openapi_ruby/core/document_builder.rb +7 -1
- data/lib/openapi_ruby/dsl/context.rb +2 -4
- data/lib/openapi_ruby/engine.rb +2 -3
- data/lib/openapi_ruby/generator/schema_writer.rb +3 -2
- data/lib/openapi_ruby/middleware/path_matcher.rb +5 -1
- data/lib/openapi_ruby/middleware/request_validation.rb +34 -4
- data/lib/openapi_ruby/version.rb +1 -1
- data/lib/tasks/openapi_ruby.rake +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e392a1235f97dc71aaf0ec1790ee0bd64c4bcbae412063af363af91e95cb608d
|
|
4
|
+
data.tar.gz: 90f056e8bc51aa18d6de20a38ac706c079955567066e034d9408a8bd75b2f966
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bde611e9f9b188969e3bbc8a6f1062efe7738ebc15e7eef40079406cccd0eab35051c767db9abfbcba0eea1e91ec39a265f7654468f8478efef8462e63b67f42
|
|
7
|
+
data.tar.gz: c02da938cd7edd9053fc89730916dbc31b87829fc945c36248269e6a77a569cf77ea07c14938550517a16909615ee926fc05b1b54bf848a3997ac7cfa1bf4c47
|
|
@@ -26,7 +26,6 @@ module OpenapiRuby
|
|
|
26
26
|
define_method(method) do |summary = nil, &block|
|
|
27
27
|
path_ctx = metadata[:openapi_path_context]
|
|
28
28
|
op_context = DSL::OperationContext.new(method, summary)
|
|
29
|
-
path_ctx.path_parameters.each { |p| op_context.parameter(p) }
|
|
30
29
|
path_ctx.operations[method.to_s] = op_context
|
|
31
30
|
|
|
32
31
|
describe "#{method.to_s.upcase} #{path_ctx.path_template}" do
|
|
@@ -118,6 +117,7 @@ module OpenapiRuby
|
|
|
118
117
|
# Merge individual parameter let values
|
|
119
118
|
operation&.parameters&.each do |param|
|
|
120
119
|
name = param["name"]
|
|
120
|
+
next unless name
|
|
121
121
|
val = resolve_let(name.to_sym)
|
|
122
122
|
next if val.nil?
|
|
123
123
|
|
|
@@ -13,10 +13,12 @@ module OpenapiRuby
|
|
|
13
13
|
def load!
|
|
14
14
|
define_namespace_modules!
|
|
15
15
|
load_component_files!
|
|
16
|
+
@@loaded = true # rubocop:disable Style/ClassVars
|
|
16
17
|
self
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def to_openapi_hash
|
|
21
|
+
ensure_loaded!
|
|
20
22
|
Registry.instance.to_openapi_hash(scope: @scope)
|
|
21
23
|
end
|
|
22
24
|
|
|
@@ -158,6 +160,11 @@ module OpenapiRuby
|
|
|
158
160
|
if inferred_scope == :shared
|
|
159
161
|
klass._component_scopes = []
|
|
160
162
|
klass._component_scopes_explicitly_set = true
|
|
163
|
+
elsif inferred_scope.is_a?(Array)
|
|
164
|
+
Registry.instance.unregister(klass)
|
|
165
|
+
klass._component_scopes = inferred_scope
|
|
166
|
+
klass._component_scopes_explicitly_set = true
|
|
167
|
+
Registry.instance.register(klass)
|
|
161
168
|
else
|
|
162
169
|
Registry.instance.unregister(klass)
|
|
163
170
|
klass._component_scopes = [inferred_scope]
|
|
@@ -181,14 +188,25 @@ module OpenapiRuby
|
|
|
181
188
|
|
|
182
189
|
def infer_scope(relative_path, scope_paths)
|
|
183
190
|
scope_paths.sort_by { |prefix, _| -prefix.length }.each do |prefix, scope|
|
|
184
|
-
|
|
191
|
+
if relative_path.start_with?("#{prefix}/")
|
|
192
|
+
return scope.is_a?(Array) ? scope.map(&:to_sym) : scope&.to_sym
|
|
193
|
+
end
|
|
185
194
|
end
|
|
186
195
|
nil
|
|
187
196
|
end
|
|
188
197
|
|
|
189
198
|
def filter_type(type)
|
|
199
|
+
ensure_loaded!
|
|
190
200
|
to_openapi_hash[type.to_s] || {}
|
|
191
201
|
end
|
|
202
|
+
|
|
203
|
+
@@loaded = false # rubocop:disable Style/ClassVars
|
|
204
|
+
|
|
205
|
+
def ensure_loaded!
|
|
206
|
+
return if @@loaded
|
|
207
|
+
|
|
208
|
+
load!
|
|
209
|
+
end
|
|
192
210
|
end
|
|
193
211
|
end
|
|
194
212
|
end
|
|
@@ -89,16 +89,18 @@ module OpenapiRuby
|
|
|
89
89
|
|
|
90
90
|
def to_openapi_hash(scope: nil)
|
|
91
91
|
result = {}
|
|
92
|
+
# Track which components are scope-specific vs multi-scope/shared
|
|
93
|
+
# so scope-specific ones take precedence on name collisions.
|
|
94
|
+
specificity = {}
|
|
95
|
+
|
|
92
96
|
@components.each do |type, components|
|
|
93
97
|
type_key = type.to_s
|
|
94
98
|
result[type_key] = {}
|
|
99
|
+
specificity[type_key] = {}
|
|
100
|
+
|
|
95
101
|
components.each_value do |klass|
|
|
96
102
|
next if klass._schema_hidden
|
|
97
|
-
|
|
98
|
-
# - Components with matching scope: included
|
|
99
|
-
# - Components explicitly marked as shared (empty scopes + explicitly_set): included
|
|
100
|
-
# - Components with non-matching scope: excluded
|
|
101
|
-
# - Components with no scope assigned (empty scopes + NOT explicitly_set): excluded
|
|
103
|
+
|
|
102
104
|
if scope
|
|
103
105
|
if klass._component_scopes.empty?
|
|
104
106
|
next unless klass._component_scopes_explicitly_set
|
|
@@ -107,7 +109,16 @@ module OpenapiRuby
|
|
|
107
109
|
end
|
|
108
110
|
end
|
|
109
111
|
|
|
110
|
-
|
|
112
|
+
name = klass.component_name
|
|
113
|
+
is_specific = klass._component_scopes.length == 1 && klass._component_scopes.include?(scope)
|
|
114
|
+
|
|
115
|
+
# Scope-specific components take precedence over shared/multi-scope
|
|
116
|
+
if specificity[type_key][name] && !is_specific
|
|
117
|
+
next
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
result[type_key][name] = klass.to_openapi
|
|
121
|
+
specificity[type_key][name] = is_specific
|
|
111
122
|
end
|
|
112
123
|
result.delete(type_key) if result[type_key].empty?
|
|
113
124
|
end
|
|
@@ -60,12 +60,18 @@ module OpenapiRuby
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
# Add 400 to
|
|
63
|
+
# Add 400 to operations that have parameters or a request body
|
|
64
64
|
@paths.each_value do |path_item|
|
|
65
|
+
path_params = path_item["parameters"]
|
|
66
|
+
|
|
65
67
|
path_item.each do |key, operation|
|
|
66
68
|
next unless operation.is_a?(Hash) && operation.key?("responses")
|
|
67
69
|
next if key == "parameters"
|
|
68
70
|
|
|
71
|
+
has_params = operation.key?("parameters") || path_params
|
|
72
|
+
has_body = operation.key?("requestBody")
|
|
73
|
+
next unless has_params || has_body
|
|
74
|
+
|
|
69
75
|
operation["responses"]["400"] ||= {"$ref" => "#/components/responses/SchemaValidationError"}
|
|
70
76
|
end
|
|
71
77
|
end
|
|
@@ -23,8 +23,6 @@ module OpenapiRuby
|
|
|
23
23
|
HTTP_METHODS.each do |method|
|
|
24
24
|
define_method(method) do |summary = nil, &block|
|
|
25
25
|
op = OperationContext.new(method, summary)
|
|
26
|
-
# Copy path-level parameters to operation
|
|
27
|
-
@path_parameters.each { |p| op.parameter(p) }
|
|
28
26
|
op.instance_eval(&block) if block
|
|
29
27
|
@operations[method.to_s] = op
|
|
30
28
|
op
|
|
@@ -34,8 +32,8 @@ module OpenapiRuby
|
|
|
34
32
|
def to_openapi
|
|
35
33
|
result = {}
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
result["parameters"] = @path_parameters if @path_parameters.any?
|
|
36
|
+
|
|
39
37
|
@operations.each do |verb, op|
|
|
40
38
|
result[verb] = op.to_openapi
|
|
41
39
|
end
|
data/lib/openapi_ruby/engine.rb
CHANGED
|
@@ -7,6 +7,8 @@ module OpenapiRuby
|
|
|
7
7
|
initializer "openapi_ruby.middleware" do |app|
|
|
8
8
|
config = OpenapiRuby.configuration
|
|
9
9
|
|
|
10
|
+
next if ENV["OPENAPI_RUBY_GENERATING"]
|
|
11
|
+
|
|
10
12
|
if config.request_validation != :disabled || config.response_validation != :disabled
|
|
11
13
|
config.schemas.each do |name, schema_config|
|
|
12
14
|
schema_path = resolve_schema_path(config, name)
|
|
@@ -36,9 +38,6 @@ module OpenapiRuby
|
|
|
36
38
|
end
|
|
37
39
|
end
|
|
38
40
|
|
|
39
|
-
# Components are loaded on demand via Components::Loader (e.g. in test helpers),
|
|
40
|
-
# not at boot time — avoids cross-file dependency ordering issues.
|
|
41
|
-
|
|
42
41
|
private
|
|
43
42
|
|
|
44
43
|
def resolve_schema_path(config, schema_name)
|
|
@@ -38,9 +38,10 @@ module OpenapiRuby
|
|
|
38
38
|
builder.add_path(context.path_template, context.to_openapi)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
# Merge components from registry
|
|
41
|
+
# Merge components from registry (Loader ensures files are loaded)
|
|
42
42
|
scope = @schema_config[:component_scope]
|
|
43
|
-
|
|
43
|
+
loader = Components::Loader.new(scope: scope)
|
|
44
|
+
components = loader.to_openapi_hash
|
|
44
45
|
builder.merge_components(components)
|
|
45
46
|
|
|
46
47
|
builder.build
|
|
@@ -21,7 +21,11 @@ module OpenapiRuby
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
23
|
def build_matchers(templates)
|
|
24
|
-
templates
|
|
24
|
+
# Sort templates so that static paths come before parameterized ones.
|
|
25
|
+
# This ensures `/admin_users/me` matches before `/admin_users/{id}`.
|
|
26
|
+
sorted = templates.sort_by { |t| [t.count("{"), t] }
|
|
27
|
+
|
|
28
|
+
sorted.map do |template|
|
|
25
29
|
pattern = Regexp.new("\\A" + template.gsub(/\{(\w+)\}/) { "(?<#{::Regexp.last_match(1)}>[^/]+)" } + "\\z")
|
|
26
30
|
[template, pattern]
|
|
27
31
|
end
|
|
@@ -139,8 +139,8 @@ module OpenapiRuby
|
|
|
139
139
|
errors = []
|
|
140
140
|
# Coerce string values for validation based on schema type
|
|
141
141
|
coerced = coerce_for_validation(value, schema)
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
schema_validator = resolve_schema(schema)
|
|
143
|
+
schema_validator.validate(coerced).each do |err|
|
|
144
144
|
msg = err["error"] || err["type"] || "validation failed"
|
|
145
145
|
errors << "Invalid #{context}: #{msg}"
|
|
146
146
|
end
|
|
@@ -170,9 +170,26 @@ module OpenapiRuby
|
|
|
170
170
|
end
|
|
171
171
|
|
|
172
172
|
def coerce_for_validation(value, schema)
|
|
173
|
-
|
|
173
|
+
resolved = resolve_schema_definition(schema)
|
|
174
|
+
|
|
175
|
+
if value.is_a?(Hash) && resolved.is_a?(Hash)
|
|
176
|
+
properties = resolved["properties"] || {}
|
|
177
|
+
value.each_with_object({}) do |(k, v), coerced|
|
|
178
|
+
prop_schema = properties[k] || properties[k.to_s]
|
|
179
|
+
coerced[k] = prop_schema ? coerce_for_validation(v, prop_schema) : v
|
|
180
|
+
end
|
|
181
|
+
elsif value.is_a?(String)
|
|
182
|
+
coerce_string(value, resolved)
|
|
183
|
+
else
|
|
184
|
+
value
|
|
185
|
+
end
|
|
186
|
+
rescue ArgumentError, TypeError
|
|
187
|
+
value
|
|
188
|
+
end
|
|
174
189
|
|
|
175
|
-
|
|
190
|
+
def coerce_string(value, schema)
|
|
191
|
+
type = schema.is_a?(Hash) ? schema["type"] : nil
|
|
192
|
+
case type
|
|
176
193
|
when "integer"
|
|
177
194
|
Integer(value)
|
|
178
195
|
when "number"
|
|
@@ -190,6 +207,19 @@ module OpenapiRuby
|
|
|
190
207
|
value
|
|
191
208
|
end
|
|
192
209
|
|
|
210
|
+
def resolve_schema_definition(schema)
|
|
211
|
+
return schema unless schema.is_a?(Hash) && schema["$ref"]
|
|
212
|
+
|
|
213
|
+
ref_path = schema["$ref"].sub("#/", "").split("/")
|
|
214
|
+
ref_path.reduce(document) { |doc, key| doc&.dig(key) } || schema
|
|
215
|
+
rescue
|
|
216
|
+
schema
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def document
|
|
220
|
+
@resolver.respond_to?(:document) ? @resolver.document : {}
|
|
221
|
+
end
|
|
222
|
+
|
|
193
223
|
def read_request_body(request)
|
|
194
224
|
return nil unless request.body
|
|
195
225
|
|
data/lib/openapi_ruby/version.rb
CHANGED
data/lib/tasks/openapi_ruby.rake
CHANGED
|
@@ -8,7 +8,7 @@ namespace :openapi_ruby do
|
|
|
8
8
|
|
|
9
9
|
# Spawn a subprocess so RAILS_ENV defaults to "test" cleanly,
|
|
10
10
|
# just like rswag did with RSpec::Core::RakeTask.
|
|
11
|
-
env = {"RAILS_ENV" => ENV.fetch("RAILS_ENV", "test")}
|
|
11
|
+
env = {"RAILS_ENV" => ENV.fetch("RAILS_ENV", "test"), "OPENAPI_RUBY_GENERATING" => "true"}
|
|
12
12
|
script = generate_script(framework, pattern)
|
|
13
13
|
command = "bundle exec ruby -e #{Shellwords.escape(script)}"
|
|
14
14
|
|