openapi-ruby 4.2.0 → 5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f7b5b27e899e80be2470549b5a347b3e3cd9a3d3974f8d5de3842510741e252
4
- data.tar.gz: d322e9f88d6164fe4f6f10203266efa88eb99a6f7b27cf0983376bc3587d2e77
3
+ metadata.gz: f937b3be2ac21a6d99687ca9ae2e0a625f89747c597fca146a7f04398d55cedc
4
+ data.tar.gz: e185d294c49caf0f33b1ad4cfc4a0512968f94ecb8f0f8184affcdd69230c94c
5
5
  SHA512:
6
- metadata.gz: 1a1948211044728807f9ff02fa7a0d81af64c4d52619bbcf43895481d8309e4c949c8e40d8daff4c897d694afc53ac02fd48194fa88203bc49d5d7f102200927
7
- data.tar.gz: 55a9e333eff2da6db2a8032ad3777219b8df7957f3fc49b5dafc9fc3f0cf586d35836df8439b3b40dcb09e0c2ed56b7aff6fbe7ebc67b75264bf62c1cede3cc2
6
+ metadata.gz: 7509404bf36980da8c58fde64c22b868e8216aaba20a13849904eedda75245f5015fece24a757e4d7c9c5b40c420554c9538afa30ab591e6b93086cce0c85928
7
+ data.tar.gz: 1a00f2de46c1d363686952541948bc20930bd27397a66381923c1a01262e833eedcc0e3298c3601d0c84ae288ead7a754f73825fca0d292711588823be3469bc
data/README.md CHANGED
@@ -182,6 +182,37 @@ class Schemas::User
182
182
  end
183
183
  ```
184
184
 
185
+ #### Parameter names
186
+
187
+ The same setting covers parameter names, so parameters match the component keys instead of contradicting them. Declare them in snake_case — that keeps `let(:page_size)` and `params: {page_size: 20}` idiomatic Ruby — and the camelCased name is what reaches both the document and the request:
188
+
189
+ ```ruby
190
+ api_path "/users/{user_id}" do
191
+ parameter name: :user_id, in: :path, schema: { type: :integer }
192
+
193
+ get "List posts" do
194
+ parameter name: :page_size, in: :query, schema: { type: :integer }
195
+ # ...
196
+ end
197
+ end
198
+ ```
199
+
200
+ ```yaml
201
+ paths:
202
+ /users/{userId}: # template variables travel with the parameter names
203
+ parameters:
204
+ - name: userId
205
+ in: path
206
+ get:
207
+ parameters:
208
+ - name: pageSize
209
+ in: query
210
+ ```
211
+
212
+ The test DSL sends what the document promises: `GET /users/7?pageSize=20`. Header parameters keep their declared spelling — HTTP header names are conventionally hyphenated and matched case-insensitively, so camelizing them would rename headers nobody asked to rename.
213
+
214
+ If your API takes snake_case parameters, set `config.camelize_keys = false` (or keep the parameter names camelCased in the declaration, which passes through unchanged either way).
215
+
185
216
  ### Scopes
186
217
 
187
218
  Assign components to scopes for multiple API specs:
@@ -253,6 +284,8 @@ rails generate openapi_ruby:component User schemas
253
284
  rails generate openapi_ruby:component BearerAuth security_schemes
254
285
  ```
255
286
 
287
+ Files land in the first entry of `config.component_paths`, so the generator follows whatever the loader reads.
288
+
256
289
  ## Testing with RSpec
257
290
 
258
291
  ```ruby
@@ -621,7 +654,7 @@ Rails picks up its wiring from the engine. Every other host makes the same calls
621
654
  | `openapi_ruby:install` / `:component` generators | ✅ | — | — |
622
655
  | `openapi_permit` strong params | ✅ | — | — |
623
656
 
624
- Versions covered by CI: Rails 7.0–8.0, Hanami 2.3 and 3.0, Sinatra 3.2 and 4.2. Working reference apps live in [`spec/dummy`](spec/dummy), [`spec/hanami_dummy`](spec/hanami_dummy), and [`spec/sinatra_dummy`](spec/sinatra_dummy).
657
+ Versions covered by CI: Ruby 3.2–4.0, Rails 7.0–8.0, Hanami 2.3 and 3.0, Sinatra 3.2 and 4.2. Working reference apps live in [`spec/dummy`](spec/dummy), [`spec/hanami_dummy`](spec/hanami_dummy), and [`spec/sinatra_dummy`](spec/sinatra_dummy).
625
658
 
626
659
  ### Hanami
627
660
 
@@ -12,11 +12,31 @@ module OpenapiRuby
12
12
 
13
13
  def create_component_file
14
14
  template "component.rb.tt",
15
- File.join("app/api_components", component_type, "#{file_name}.rb")
15
+ File.join(component_path, component_type, "#{file_name}.rb")
16
16
  end
17
17
 
18
18
  private
19
19
 
20
+ # Components::Loader only looks under the configured paths, so anything
21
+ # written to a hardcoded app/api_components is invisible on hosts that
22
+ # default elsewhere (Hanami) or configure their own.
23
+ def component_path
24
+ path = OpenapiRuby.configuration.component_paths.first ||
25
+ Configuration.default_component_paths.first
26
+ relativize(path)
27
+ end
28
+
29
+ # An initializer may hold an absolute Rails.root.join(...) path; Thor
30
+ # reports the destination verbatim, and an absolute one reads as noise.
31
+ def relativize(path)
32
+ pathname = Pathname.new(path)
33
+ return path unless pathname.absolute?
34
+
35
+ pathname.relative_path_from(Pathname.new(destination_root)).to_s
36
+ rescue ArgumentError
37
+ path
38
+ end
39
+
20
40
  def class_name
21
41
  name.camelize
22
42
  end
@@ -69,7 +69,7 @@ module OpenapiRuby
69
69
  def accounted_keys(context, method)
70
70
  declared = context.path_parameters + (context.operations[method.to_s]&.parameters || [])
71
71
 
72
- template_params(context) | declared.filter_map { |param| param["name"]&.to_s }
72
+ template_params(context) | declared.flat_map { |param| OpenapiRuby::ParameterNames.lookup_names(param) }
73
73
  end
74
74
 
75
75
  def template_params(context)
@@ -90,6 +90,7 @@ module OpenapiRuby
90
90
 
91
91
  # Build query params (exclude path params)
92
92
  query_params = params.reject { |k, _| path_param_names(context).include?(k.to_s) }
93
+ query_params = ParameterNames.rename_keys(query_params, operation.parameters)
93
94
 
94
95
  # Execute the request
95
96
  if body
@@ -180,7 +181,7 @@ module OpenapiRuby
180
181
  end
181
182
 
182
183
  def path_param_names(context)
183
- context.path_parameters.map { |p| p["name"] }
184
+ context.path_parameters.flat_map { |p| ParameterNames.lookup_names(p) }
184
185
  end
185
186
 
186
187
  def resolve_base_path(schema_name)
@@ -156,8 +156,9 @@ module OpenapiRuby
156
156
 
157
157
  headers["Accept"] ||= "application/json"
158
158
 
159
- path_param_names = context.path_parameters.map { |p| p["name"] }
159
+ path_param_names = context.path_parameters.flat_map { |p| ParameterNames.lookup_names(p) }
160
160
  query_params = params.reject { |k, _| path_param_names.include?(k.to_s) }
161
+ query_params = ParameterNames.rename_keys(query_params, operation.parameters)
161
162
 
162
163
  if body
163
164
  content_type = operation.request_body_definition&.dig("content")&.keys&.first || "application/json"
@@ -248,7 +249,7 @@ module OpenapiRuby
248
249
  next if val.nil?
249
250
 
250
251
  case param["in"]
251
- when "query" then params[name] = val
252
+ when "query" then params[ParameterNames.wire_name(param)] = val
252
253
  when "header" then headers[name] = val
253
254
  end
254
255
  end
@@ -17,6 +17,7 @@ module OpenapiRuby
17
17
  end
18
18
 
19
19
  def add_path(template, operations)
20
+ template = ParameterNames.in_template(template)
20
21
  @paths[template] ||= {}
21
22
  @paths[template].deep_merge!(operations)
22
23
  end
@@ -32,7 +32,7 @@ module OpenapiRuby
32
32
  def to_openapi
33
33
  result = {}
34
34
 
35
- result["parameters"] = @path_parameters if @path_parameters.any?
35
+ result["parameters"] = ParameterNames.parameters_for_document(@path_parameters) if @path_parameters.any?
36
36
 
37
37
  @operations.each do |verb, op|
38
38
  result[verb] = op.to_openapi
@@ -93,7 +93,7 @@ module OpenapiRuby
93
93
  result["summary"] = @summary if @summary
94
94
  result["tags"] = @tags_list if @tags_list.any?
95
95
  result.merge!(@metadata)
96
- result["parameters"] = @parameters if @parameters.any?
96
+ result["parameters"] = ParameterNames.parameters_for_document(@parameters) if @parameters.any?
97
97
  result["security"] = @security_list if @security_list
98
98
 
99
99
  result["requestBody"] = build_request_body if @request_body_definition
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiRuby
4
+ # Parameters are declared in snake_case so `let(:page_size)` and
5
+ # `params: {page_size: 1}` stay idiomatic Ruby. With `camelize_keys` on, the
6
+ # name that reaches the document *and* the wire is camelCased, so parameters
7
+ # match the component keys instead of contradicting them.
8
+ #
9
+ # Header parameters keep their declared spelling: HTTP header names are
10
+ # conventionally hyphenated and matched case-insensitively, so camelizing
11
+ # them would rename headers nobody asked to rename.
12
+ module ParameterNames
13
+ module_function
14
+
15
+ def camelize?
16
+ OpenapiRuby.configuration.camelize_keys
17
+ end
18
+
19
+ def camelize(name)
20
+ return name.to_s unless camelize?
21
+
22
+ Components::KeyTransformer.camelize(name)
23
+ end
24
+
25
+ # The name a declared parameter carries in the document and on the wire.
26
+ def wire_name(param)
27
+ name = param["name"] || param[:name]
28
+ return nil if name.nil?
29
+ return name.to_s if header?(param)
30
+
31
+ camelize(name)
32
+ end
33
+
34
+ # Both spellings of a declared parameter. Value lookups go through this so a
35
+ # caller that passes the declared name still resolves after the wire name
36
+ # diverged from it.
37
+ def lookup_names(param)
38
+ declared = (param["name"] || param[:name])&.to_s
39
+ return [] unless declared
40
+
41
+ [declared, wire_name(param)].compact.uniq
42
+ end
43
+
44
+ def parameters_for_document(parameters)
45
+ return parameters unless camelize?
46
+
47
+ parameters.map do |param|
48
+ wire = wire_name(param)
49
+ next param if wire.nil? || wire == param["name"].to_s
50
+
51
+ param.merge("name" => wire)
52
+ end
53
+ end
54
+
55
+ # `/users/{user_id}` -> `/users/{userId}`. The template variables have to
56
+ # travel with the parameter names: OpenAPI requires each one to be backed by
57
+ # a path parameter of the same name.
58
+ def in_template(template)
59
+ return template unless camelize?
60
+
61
+ template.gsub(/\{(\w+)\}/) { "{#{camelize(::Regexp.last_match(1))}}" }
62
+ end
63
+
64
+ # Rename the keys of a request value hash from their declared spelling to
65
+ # the wire spelling. Keys that match no declared parameter are left alone —
66
+ # undeclared params are passed through as the caller wrote them.
67
+ def rename_keys(values, parameters)
68
+ return values unless camelize?
69
+
70
+ mapping = parameters.each_with_object({}) do |param, acc|
71
+ declared = (param["name"] || param[:name])&.to_s
72
+ next unless declared
73
+
74
+ wire = wire_name(param)
75
+ acc[declared] = wire if wire && wire != declared
76
+ end
77
+ return values if mapping.empty?
78
+
79
+ values.each_with_object({}) do |(key, value), acc|
80
+ renamed = mapping[key.to_s]
81
+ acc[renamed.nil? ? key : renamed] = value
82
+ end
83
+ end
84
+
85
+ def header?(param)
86
+ (param["in"] || param[:in]).to_s == "header"
87
+ end
88
+ end
89
+ end
@@ -37,7 +37,7 @@ module OpenapiRuby
37
37
  result = {}
38
38
  query_params.each do |param|
39
39
  name = param["name"]
40
- result[name] = @param_values[name] if @param_values.key?(name)
40
+ result[ParameterNames.wire_name(param)] = @param_values[name] if @param_values.key?(name)
41
41
  end
42
42
  result
43
43
  end
@@ -39,19 +39,30 @@ module OpenapiRuby
39
39
 
40
40
  private
41
41
 
42
+ # A value may arrive under the declared name or the camelized wire name,
43
+ # depending on whether the caller wrote it or an adapter already renamed
44
+ # it for the request. Both spellings resolve.
42
45
  def extract_param_value(param, params, headers, path_params)
43
- name = param["name"]
46
+ names = ParameterNames.lookup_names(param)
44
47
 
45
48
  case param["in"]
46
49
  when "query"
47
- params[name.to_sym] || params[name.to_s]
50
+ fetch_any(params, names)
48
51
  when "path"
49
- path_params[name.to_sym] || path_params[name.to_s]
52
+ fetch_any(path_params, names)
50
53
  when "header"
51
- headers[name] || headers[name.downcase]
54
+ fetch_any(headers, names.flat_map { |name| [name, name.downcase] })
52
55
  end
53
56
  end
54
57
 
58
+ def fetch_any(store, names)
59
+ names.each do |name|
60
+ value = store[name.to_sym] || store[name.to_s]
61
+ return value if value
62
+ end
63
+ nil
64
+ end
65
+
55
66
  def validate_request_body(operation, body)
56
67
  errors = []
57
68
  rb_spec = operation.request_body_definition
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiRuby
4
- VERSION = "4.2.0"
4
+ VERSION = "5.0.0"
5
5
  end
data/lib/openapi_ruby.rb CHANGED
@@ -54,6 +54,7 @@ require_relative "openapi_ruby/components/key_transformer"
54
54
  require_relative "openapi_ruby/components/registry"
55
55
  require_relative "openapi_ruby/components/base"
56
56
  require_relative "openapi_ruby/components/loader"
57
+ require_relative "openapi_ruby/parameter_names"
57
58
  require_relative "openapi_ruby/dsl/response_context"
58
59
  require_relative "openapi_ruby/dsl/operation_context"
59
60
  require_relative "openapi_ruby/dsl/context"
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.2.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Morten Hartvig
@@ -106,6 +106,7 @@ files:
106
106
  - lib/openapi_ruby/middleware/response_validation.rb
107
107
  - lib/openapi_ruby/middleware/schema_resolver.rb
108
108
  - lib/openapi_ruby/minitest.rb
109
+ - lib/openapi_ruby/parameter_names.rb
109
110
  - lib/openapi_ruby/rack_app.rb
110
111
  - lib/openapi_ruby/rake_tasks.rb
111
112
  - lib/openapi_ruby/rspec.rb