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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f135ed1b9a07f225cf01191cb278cd1eb62188369613e41b86df80dcbd147639
4
- data.tar.gz: '008fe467062f636c4400f2dd65fa40cd09c32d6087d4c1207c9ce2667a9afc85'
3
+ metadata.gz: e392a1235f97dc71aaf0ec1790ee0bd64c4bcbae412063af363af91e95cb608d
4
+ data.tar.gz: 90f056e8bc51aa18d6de20a38ac706c079955567066e034d9408a8bd75b2f966
5
5
  SHA512:
6
- metadata.gz: fddba781e5ca09c19f5766c5b708b0408c4b0c199663f4cba23b8e1959535283f75a1b6e5550bc4defc7bbabc07442c16ad66374b7e93bebd432d29dadfe93f8
7
- data.tar.gz: 67b1ef59d47959496cfb4b33ac76f3e73d961d0645bec2ae2cbc5c693e4fbb4a4d2d9121bef3f39c2840c6bf50500f8a8acead89cd91e7a5893312ccd7798db9
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
- return scope&.to_sym if relative_path.start_with?("#{prefix}/")
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
- # When filtering by scope:
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
- result[type_key][klass.component_name] = klass.to_openapi
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 every operation that doesn't already define one
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
- # Path-level parameters are already copied into each operation (line 27),
38
- # so we don't output them at the path level to avoid duplicates.
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
@@ -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
- components = Components::Registry.instance.to_openapi_hash(scope: scope)
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.map do |template|
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
- schemer = JSONSchemer.schema(schema)
143
- schemer.validate(coerced).each do |err|
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
- return value unless value.is_a?(String)
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
- case schema["type"]
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiRuby
4
- VERSION = "3.0.3"
4
+ VERSION = "3.1.0"
5
5
  end
@@ -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
 
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: 3.0.3
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Morten Hartvig