openapi_parameters 0.10.0 → 0.12.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: 52bca4aebd3d66fa0a3edc68d3ce71a24a223a31be135677a8fb9ea09a4055bc
4
- data.tar.gz: '03230596b824ee0a59ca68048bcfc68e884af2cbbb3b14ea3952c1f917a7d87b'
3
+ metadata.gz: 69f716831414a1780a62e1d78b4405f3e9bc293f8dea9dcd9d56330c56180322
4
+ data.tar.gz: 4621c4705857aa76cdf67c3f0275d4e515b63fe8429fd9de406174dd04625f23
5
5
  SHA512:
6
- metadata.gz: e5bf0752ef3be5d397cd5f5cee010a5e89fc4c1a1821ec0b86411fb1a97bfae8e81be46f5fa94d5bd47940c921f0a0dab9641f591f6ceddecfc856dd881f34d7
7
- data.tar.gz: 8244a7c1c93ef4c0c2ee88374c4bb72f3f6295a11637fff60b02ad96e9015e702b284251eff003309ebdc0bb8f6ccf38f85334a9be7d6801bcd0b6f48cc5e808
6
+ metadata.gz: cc2fb07a22b21031c311271166a204929170fd678385e0f69e00af18c98109550709da12bc9cd625a456cff8cd93a887c2d4b0eabb8c7850765e6095b4cf53b1
7
+ data.tar.gz: f0d679df5b595877fc2033e6ee68014ef108fe13b1c9c128f423220397d79e9aba9f1cf2a5feac4bb63e6ba5afe7b59ca46c97373fcba0102e2e455f94cad960
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.12.0] - 2026-05-18
4
+
5
+ - Add support for parameters defined with a `content` field. Values for media types matching `application/json` or `*+json` are decoded with `JSON.parse` before conversion. Works for query, path, header, and cookie parameters.
6
+
7
+ ## [0.11.0] - 2025-11-30
8
+
9
+ - Changed: Passing a plain query string like `filter=ada` to a`deepObject` parameter instead of using the brackets syntax `filter[name]=ada` no longer results in an empty object, but in the plain string value (like `{ filter: 'ada'} `), which will make request validation fail.
10
+
3
11
  ## [0.10.0] - 2025-11-23
4
12
 
5
13
  - remove `OpenapiParameters::Parameter#definition` to clean up interface
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module OpenapiParameters
6
+ # Registry of content parsers keyed by media type.
7
+ #
8
+ # A parser is a callable that takes a raw string and returns the parsed value.
9
+ # It should `throw :skip, value` if the input cannot be parsed, so the
10
+ # parameter falls through gracefully at the location class level.
11
+ #
12
+ # Built-in: `application/json` and any `*+json` subtype.
13
+ #
14
+ # To register your own:
15
+ #
16
+ # OpenapiParameters::ContentParsers.register('application/xml', ->(v) { ... })
17
+ module ContentParsers
18
+ @parsers = []
19
+
20
+ class << self
21
+ attr_reader :parsers
22
+
23
+ # @param matcher [String, Regexp] exact media type or a pattern.
24
+ # @param parser [#call] callable that takes the raw string and returns the parsed value.
25
+ def register(matcher, parser)
26
+ parsers.reject! { |existing, _| existing == matcher }
27
+ parsers << [matcher, parser]
28
+ end
29
+
30
+ # @param media_type [String, nil]
31
+ # @return [#call, nil] the parser, or nil if none is registered.
32
+ def [](media_type)
33
+ return nil if media_type.nil?
34
+
35
+ parsers.each do |matcher, parser|
36
+ return parser if match?(matcher, media_type)
37
+ end
38
+ nil
39
+ end
40
+
41
+ private
42
+
43
+ def match?(matcher, media_type)
44
+ case matcher
45
+ when Regexp then matcher.match?(media_type)
46
+ else matcher == media_type
47
+ end
48
+ end
49
+ end
50
+
51
+ register(%r{\A[\w.+-]+/(?:[\w.-]+\+)?json\z}i, lambda do |value|
52
+ JSON.parse(value)
53
+ rescue JSON::ParserError
54
+ throw :skip, value
55
+ end)
56
+ end
57
+ end
@@ -17,7 +17,7 @@ module OpenapiParameters
17
17
  next unless cookies.key?(parameter.name)
18
18
 
19
19
  result[parameter.name] = catch :skip do
20
- value = Unpacker.unpack_value(parameter, cookies[parameter.name])
20
+ value = parameter.unpack(cookies[parameter.name])
21
21
  @convert ? parameter.convert(value) : value
22
22
  end
23
23
  end
@@ -16,7 +16,7 @@ module OpenapiParameters
16
16
  next unless headers.key?(parameter.name)
17
17
 
18
18
  result[parameter.name] = catch :skip do
19
- value = Unpacker.unpack_value(parameter, headers[parameter.name])
19
+ value = parameter.unpack(headers[parameter.name])
20
20
  @convert ? parameter.convert(value) : value
21
21
  end
22
22
  end
@@ -11,10 +11,18 @@ module OpenapiParameters
11
11
  @is_deep_object = style == 'deepObject'
12
12
  @converter = Converters[schema]
13
13
  check_supported!
14
+ @unpacker = Unpackers.find(self)
15
+ @bracket_array = (array? && @name&.end_with?(EMPTY_BRACKETS)) || false
14
16
  end
15
17
 
16
18
  attr_reader :name
17
- private attr_reader :definition
19
+ private attr_reader :definition, :unpacker
20
+
21
+ def unpack(value)
22
+ return value if value.nil?
23
+
24
+ unpacker.call(value)
25
+ end
18
26
 
19
27
  def convert(value)
20
28
  @converter.call(value)
@@ -57,7 +65,7 @@ module OpenapiParameters
57
65
  private_constant :EMPTY_BRACKETS
58
66
 
59
67
  def bracket_array?
60
- @bracket_array ||= array? && name.end_with?(EMPTY_BRACKETS)
68
+ @bracket_array
61
69
  end
62
70
 
63
71
  def object?
@@ -20,7 +20,7 @@ module OpenapiParameters
20
20
  next unless path_params.key?(parameter.name)
21
21
 
22
22
  result[parameter.name] = catch :skip do
23
- value = Unpacker.unpack_value(parameter, path_params[parameter.name])
23
+ value = parameter.unpack(path_params[parameter.name])
24
24
  @convert ? parameter.convert(value) : value
25
25
  end
26
26
  end
@@ -12,18 +12,26 @@ module OpenapiParameters
12
12
  @parameters = parameters.map { Parameter.new(_1) }
13
13
  @convert = convert
14
14
  @remove_array_brackets = rack_array_compat
15
+ @deep_object_properties = @parameters.each_with_object({}) do |param, hsh|
16
+ hsh[param.name] = ObjectConverter.get_properties(param.schema) if param.deep_object?
17
+ end
15
18
  end
16
19
 
17
- def unpack(query_string) # rubocop:disable Metrics/AbcSize
20
+ def unpack(query_string) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
18
21
  parsed_query = parse_query(query_string)
19
22
  parameters.each_with_object({}) do |parameter, result|
20
23
  if parameter.deep_object?
21
- value = parse_deep_object(parameter, parsed_query)
22
- next if value.empty?
24
+ if parsed_query.key?(parameter.name)
25
+ value = parsed_query[parameter.name]
26
+ else
27
+ value = parse_deep_object(parameter, parsed_query)
28
+ next if value.empty?
29
+ end
23
30
  else
24
31
  next unless parsed_query.key?(parameter.name)
25
32
 
26
- value = Unpacker.unpack_value(parameter, parsed_query[parameter.name])
33
+ raw = parsed_query[parameter.name]
34
+ value = catch(:skip) { parameter.unpack(raw) }
27
35
  end
28
36
  key = if remove_array_brackets && parameter.bracket_array?
29
37
  parameter.name.delete_suffix('[]')
@@ -65,18 +73,13 @@ module OpenapiParameters
65
73
  end
66
74
  end
67
75
 
68
- def build_properties_schema(parameter)
69
- schema = parameter.schema
70
- ObjectConverter.get_properties(schema) if schema
71
- end
72
-
73
76
  DEEP_PROP = '\[([\w-]+)\]$'
74
77
  private_constant :DEEP_PROP
75
78
 
76
79
  def parse_deep_object(parameter, parsed_query)
77
80
  name = parameter.name
78
81
  prop_regx = /^#{name}#{DEEP_PROP}/
79
- properties_schema = build_properties_schema(parameter)
82
+ properties_schema = @deep_object_properties[name]
80
83
 
81
84
  parsed_query.each.with_object({}) do |(key, value), result|
82
85
  next unless parsed_query.key?(key)
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiParameters
4
+ # Resolves the per-parameter unpacker once at Parameter construction time.
5
+ # Each unpacker is a callable that takes a raw string (or already-parsed
6
+ # value) and returns the unpacked Ruby value, throwing :skip on unrecoverable
7
+ # parse errors.
8
+ # @visibility private
9
+ module Unpackers
10
+ ARRAY_DELIMITER = {
11
+ 'label' => '.',
12
+ 'simple' => ',',
13
+ 'form' => ',',
14
+ 'pipeDelimited' => '|',
15
+ 'spaceDelimited' => ' '
16
+ }.freeze
17
+ private_constant :ARRAY_DELIMITER
18
+
19
+ PREFIXED_STYLES = %w[label matrix].freeze
20
+ private_constant :PREFIXED_STYLES
21
+
22
+ OBJECT_EXPLODE_SPLITTER = Regexp.union(',', '=').freeze
23
+ private_constant :OBJECT_EXPLODE_SPLITTER
24
+
25
+ PassThrough = ->(value) { value }
26
+
27
+ DelimitedArray = Data.define(:delimiter, :strip_prefix) do
28
+ def call(value)
29
+ return value if value.is_a?(::Array)
30
+ return value if value.empty?
31
+
32
+ value = value[1..] if strip_prefix
33
+ value.split(delimiter)
34
+ end
35
+ end
36
+
37
+ MatrixArray = Data.define(:name, :explode) do
38
+ def call(value)
39
+ return value if value.is_a?(::Array)
40
+ return value if value.empty?
41
+
42
+ result = Rack::Utils.parse_query(value, ';')[name]
43
+ explode ? result : result.split(',')
44
+ end
45
+ end
46
+
47
+ ExplodeFormObject = lambda do |value|
48
+ entries = value.split(OBJECT_EXPLODE_SPLITTER)
49
+ throw :skip, value if entries.length.odd?
50
+
51
+ Hash[*entries]
52
+ end
53
+
54
+ DelimitedObject = Data.define(:delimiter) do
55
+ def call(value)
56
+ entries = value.split(delimiter)
57
+ throw :skip, value if entries.length.odd?
58
+
59
+ Hash[*entries]
60
+ end
61
+ end
62
+
63
+ ExplodePathObject = ->(value) { Rack::Utils.parse_query(value, ',') }
64
+
65
+ NonExplodePathObject = Data.define(:array_unpacker) do
66
+ def call(value)
67
+ array = array_unpacker.call(value)
68
+ throw :skip, value if array.length.odd?
69
+
70
+ Hash[*array]
71
+ end
72
+ end
73
+
74
+ class << self
75
+ def find(parameter)
76
+ return find_media_type(parameter) if parameter.media_type
77
+ return find_array(parameter) if parameter.array?
78
+ return find_object(parameter) if parameter.object?
79
+
80
+ PassThrough
81
+ end
82
+
83
+ private
84
+
85
+ def find_media_type(parameter)
86
+ ContentParsers[parameter.media_type] || PassThrough
87
+ end
88
+
89
+ def find_array(parameter)
90
+ style = parameter.style
91
+ return MatrixArray.new(name: parameter.name, explode: parameter.explode?) if style == 'matrix'
92
+
93
+ DelimitedArray.new(
94
+ delimiter: ARRAY_DELIMITER[style],
95
+ strip_prefix: PREFIXED_STYLES.include?(style)
96
+ )
97
+ end
98
+
99
+ def find_object(parameter)
100
+ return find_path_object(parameter) if parameter.location == 'path'
101
+ return ExplodeFormObject if parameter.explode?
102
+
103
+ DelimitedObject.new(delimiter: ARRAY_DELIMITER[parameter.style])
104
+ end
105
+
106
+ def find_path_object(parameter)
107
+ return ExplodePathObject if parameter.explode?
108
+
109
+ NonExplodePathObject.new(array_unpacker: find_array(parameter))
110
+ end
111
+ end
112
+ end
113
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiParameters
4
- VERSION = '0.10.0'
4
+ VERSION = '0.12.0'
5
5
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
3
4
  require 'rack'
4
5
  require_relative 'openapi_parameters/converters'
5
6
  require_relative 'openapi_parameters/converter'
@@ -8,10 +9,11 @@ require_relative 'openapi_parameters/error'
8
9
  require_relative 'openapi_parameters/header'
9
10
  require_relative 'openapi_parameters/headers_hash'
10
11
  require_relative 'openapi_parameters/not_supported_error'
12
+ require_relative 'openapi_parameters/content_parsers'
13
+ require_relative 'openapi_parameters/unpackers'
11
14
  require_relative 'openapi_parameters/parameter'
12
15
  require_relative 'openapi_parameters/path'
13
16
  require_relative 'openapi_parameters/query'
14
- require_relative 'openapi_parameters/unpacker'
15
17
 
16
18
  # OpenapiParameters is a gem that parses OpenAPI parameters from Rack
17
19
  module OpenapiParameters
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi_parameters
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
@@ -35,6 +35,7 @@ files:
35
35
  - README.md
36
36
  - lib/openapi_parameters.rb
37
37
  - lib/openapi_parameters/array_converter.rb
38
+ - lib/openapi_parameters/content_parsers.rb
38
39
  - lib/openapi_parameters/converter.rb
39
40
  - lib/openapi_parameters/converters.rb
40
41
  - lib/openapi_parameters/cookie.rb
@@ -46,7 +47,7 @@ files:
46
47
  - lib/openapi_parameters/parameter.rb
47
48
  - lib/openapi_parameters/path.rb
48
49
  - lib/openapi_parameters/query.rb
49
- - lib/openapi_parameters/unpacker.rb
50
+ - lib/openapi_parameters/unpackers.rb
50
51
  - lib/openapi_parameters/version.rb
51
52
  homepage: https://github.com/ahx/openapi_parameters
52
53
  licenses:
@@ -1,73 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OpenapiParameters
4
- # Unpacks a parameter value from a string as defined in the OpenAPI specification.
5
- # @visibility private
6
- module Unpacker
7
- class << self
8
- def unpack_value(parameter, value)
9
- return value if value.nil?
10
- return unpack_array(parameter, value) if parameter.array?
11
- return unpack_object(parameter, value) if parameter.object?
12
-
13
- value
14
- end
15
-
16
- def unpack_array(parameter, value)
17
- return value if value.is_a?(Array)
18
- return value if value.empty?
19
- return unpack_matrix(parameter, value) if parameter.style == 'matrix'
20
-
21
- value = value[1..] if PREFIXED.key?(parameter.style)
22
- value.split(ARRAY_DELIMITER[parameter.style])
23
- end
24
-
25
- def unpack_matrix(parameter, value)
26
- result = Rack::Utils.parse_query(value, ';')[parameter.name]
27
- return result if parameter.explode?
28
-
29
- result.split(',')
30
- end
31
-
32
- OBJECT_EXPLODE_SPLITTER = Regexp.union(',', '=').freeze
33
- private_constant :OBJECT_EXPLODE_SPLITTER
34
-
35
- def unpack_object(parameter, value)
36
- return unpack_object_path(parameter, value) if parameter.location == 'path'
37
-
38
- entries =
39
- if parameter.explode?
40
- value.split(OBJECT_EXPLODE_SPLITTER)
41
- else
42
- value.split(ARRAY_DELIMITER[parameter.style])
43
- end
44
- throw :skip, value if entries.length.odd?
45
-
46
- Hash[*entries]
47
- end
48
-
49
- def unpack_object_path(parameter, value)
50
- return Rack::Utils.parse_query(value, ',') if parameter.explode?
51
-
52
- array = unpack_array(parameter, value)
53
- throw :skip, value if array.length.odd?
54
-
55
- Hash[*array]
56
- end
57
-
58
- PREFIXED = {
59
- 'label' => '.',
60
- 'matrix' => ';'
61
- }.freeze
62
-
63
- ARRAY_DELIMITER = {
64
- 'label' => '.',
65
- 'simple' => ',',
66
- 'form' => ',',
67
- 'pipeDelimited' => '|',
68
- 'spaceDelimited' => ' '
69
- }.freeze
70
- private_constant :ARRAY_DELIMITER
71
- end
72
- end
73
- end