openapi_parameters 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 97605d112ff65a874f5fa63dc7ecd13d3ca37468e559dbf7980a1f6c55e37274
4
+ data.tar.gz: 280fe6bc55862118dc65e77a6e29c6a3f27ee3c3d07f1361003c9cbd52fdc660
5
+ SHA512:
6
+ metadata.gz: e1cff00125ffb1ea81548a6e8f5bee73b1cedc29ccb2569296e0f869a9ac3412cc9c683afcba2f48bcc6cca06287a1fba251068b4c831e2d4933497268a58da8
7
+ data.tar.gz: 6177d8425f16f678af64f4059ecc1fecf70e954ce0edd95ce2e38a7b5bbd1efe801951fffb9ca8932d7d4d258100af0070623c5c04672d1e539caee1cbc8204e
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,16 @@
1
+ require:
2
+ - rubocop-rspec
3
+
4
+ Naming/MethodParameterName:
5
+ Enabled: false
6
+
7
+ AllCops:
8
+ NewCops: enable
9
+ SuggestExtensions: false
10
+ TargetRubyVersion: 3.1
11
+
12
+ RSpec/ExampleLength:
13
+ Enabled: false
14
+
15
+ Metrics/MethodLength:
16
+ Max: 20
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 3.2.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2032-03-25
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in OpenapiParameters.gemspec
6
+ gemspec
7
+
8
+ gem 'rake', '~> 13.0'
9
+
10
+ gem 'rspec', '~> 3.0'
11
+ gem 'rubocop'
12
+ gem 'rubocop-rspec'
data/Gemfile.lock ADDED
@@ -0,0 +1,69 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ openapi_parameters (0.1.0)
5
+ rack (>= 2.2)
6
+ uri_template (>= 0.7, < 2.0)
7
+ zeitwerk (~> 2.6)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ ast (2.4.2)
13
+ diff-lcs (1.5.0)
14
+ json (2.6.2)
15
+ parallel (1.22.1)
16
+ parser (3.1.2.1)
17
+ ast (~> 2.4.1)
18
+ rack (3.0.0)
19
+ rainbow (3.1.1)
20
+ rake (13.0.6)
21
+ regexp_parser (2.6.1)
22
+ rexml (3.2.5)
23
+ rspec (3.12.0)
24
+ rspec-core (~> 3.12.0)
25
+ rspec-expectations (~> 3.12.0)
26
+ rspec-mocks (~> 3.12.0)
27
+ rspec-core (3.12.0)
28
+ rspec-support (~> 3.12.0)
29
+ rspec-expectations (3.12.0)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.12.0)
32
+ rspec-mocks (3.12.0)
33
+ diff-lcs (>= 1.2.0, < 2.0)
34
+ rspec-support (~> 3.12.0)
35
+ rspec-support (3.12.0)
36
+ rubocop (1.39.0)
37
+ json (~> 2.3)
38
+ parallel (~> 1.10)
39
+ parser (>= 3.1.2.1)
40
+ rainbow (>= 2.2.2, < 4.0)
41
+ regexp_parser (>= 1.8, < 3.0)
42
+ rexml (>= 3.2.5, < 4.0)
43
+ rubocop-ast (>= 1.23.0, < 2.0)
44
+ ruby-progressbar (~> 1.7)
45
+ unicode-display_width (>= 1.4.0, < 3.0)
46
+ rubocop-ast (1.23.0)
47
+ parser (>= 3.1.1.0)
48
+ rubocop-rspec (2.11.1)
49
+ rubocop (~> 1.19)
50
+ ruby-progressbar (1.11.0)
51
+ unicode-display_width (2.3.0)
52
+ uri_template (0.7.0)
53
+ zeitwerk (2.6.7)
54
+
55
+ PLATFORMS
56
+ arm64-darwin-21
57
+ arm64-darwin-22
58
+ x86_64-darwin-20
59
+ x86_64-linux
60
+
61
+ DEPENDENCIES
62
+ openapi_parameters!
63
+ rake (~> 13.0)
64
+ rspec (~> 3.0)
65
+ rubocop
66
+ rubocop-rspec
67
+
68
+ BUNDLED WITH
69
+ 2.3.10
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Andreas Haller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # OpenapiParameters
2
+
3
+ OpenapiParameters is an an [OpenAPI](https://www.openapis.org/) aware parameter parser.
4
+
5
+ OpenapiParameters unpacks HTTP/Rack (query / header / cookie) parameters exactly as described in an [OpenAPI](https://www.openapis.org/) definition. It supports `style`, `explode` and `schema` definitions according to OpenAPI 3.1 (or 3.0).
6
+
7
+ ## Synopsis
8
+
9
+ Note that OpenAPI supportes parameter definition on path and operation objects. Parameter definitions must use strings as keys.
10
+
11
+ ### Unpack query/path/header/cookie parameters from HTTP requests according to their OpenAPI definition
12
+
13
+ ```ruby
14
+ parameters = [{
15
+ 'name' => 'ids',
16
+ 'required' => true,
17
+ 'in' => 'query', # or 'path', 'header', 'cookie'
18
+ 'schema' => {
19
+ 'type' => 'array',
20
+ 'items' => {
21
+ 'type' => 'integer'
22
+ }
23
+ }
24
+ }]
25
+
26
+ query_parameters = OpenapiParameters::Query.new(parameters)
27
+ query_string = env['QUERY_STRING'] # => 'ids=1&ids=2'
28
+ query_parameters.unpack(query_string) # => { 'ids' => [1, 2] }
29
+ query_parameters.unpack(query_string, convert: false) # => { 'ids' => ['1', '2'] }
30
+
31
+ path_parameters = OpenapiParameters::Path.new(parameters, '/pets/ids')
32
+ path_info = env['PATH_INFO'] # => '/pets/1,2,3'
33
+ path_parameters.unpack(path_info) # => { 'ids' => [1, 2, 3] }
34
+
35
+ header_parameters = OpenapiParameters::Header.new(parameters)
36
+ header_parameters.unpack_env(env)
37
+
38
+ cookie_parameters = OpenapiParameters::Cookie.new(parameters)
39
+ cookie_string = env['HTTP_COOKIE'] # => "ids=3"
40
+ cookie_parameters.unpack(cookie_string) # => { 'ids' => [3] }
41
+ ```
42
+
43
+ Note that this library does not validate the parameter value against it's JSON Schema.
44
+
45
+ ### Inspect parameter definition
46
+
47
+ ```ruby
48
+ parameter = OpenapiParameters::Parameter.new({
49
+ 'name' => 'ids',
50
+ 'required' => true,
51
+ 'in' => 'query', # or 'path', 'header', 'cookie'
52
+ 'schema' => {
53
+ 'type' => 'array',
54
+ 'items' => {
55
+ 'type' => 'integer'
56
+ }
57
+ }
58
+ })
59
+ parameter.name # => 'ids'
60
+ parameter.required? # => true
61
+ parameter.in # => 'query'
62
+ parameter.location # => 'query' (alias for in)
63
+ parameter.schema # => { 'type' => 'array', 'items' => { 'type' => 'integer' } }
64
+ parameter.type # => 'array'
65
+ parameter.deprecated? # => false
66
+ parameter.media_type # => nil
67
+ parameter.allow_reserved? # => false
68
+ # etc.
69
+ ```
70
+
71
+ ## Installation
72
+
73
+ Install the gem and add to the application's Gemfile by executing:
74
+
75
+ $ bundle add OpenapiParameters
76
+
77
+ If bundler is not being used to manage dependencies, install the gem by executing:
78
+
79
+ $ gem install OpenapiParameters
80
+
81
+ ## Development
82
+
83
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
84
+
85
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
86
+
87
+ ## Contributing
88
+
89
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ahx/OpenapiParameters.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[spec rubocop]
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiParameters
4
+ ##
5
+ # Tries to convert a request parameter value (string) to the type specified in the JSON Schema.
6
+ class Converter
7
+ ##
8
+ # @param input [String, Hash, Array] the value to convert
9
+ # @param schema [Hash] the schema to use for conversion.
10
+ def self.call(input, schema)
11
+ new(input, schema).call
12
+ end
13
+
14
+ def initialize(input, schema)
15
+ @input = input
16
+ @root_schema = schema
17
+ end
18
+
19
+ def call
20
+ convert(@input, @root_schema)
21
+ end
22
+
23
+ private
24
+
25
+ require 'json'
26
+
27
+ def convert(value, schema)
28
+ check_supported!(schema)
29
+ return if value.nil?
30
+ return value if schema.nil?
31
+
32
+ case type(schema)
33
+ when 'integer'
34
+ begin
35
+ Integer(value, 10)
36
+ rescue StandardError
37
+ value
38
+ end
39
+ when 'number'
40
+ begin
41
+ Float(value)
42
+ rescue StandardError
43
+ value
44
+ end
45
+ when 'boolean'
46
+ if value == 'true'
47
+ true
48
+ else
49
+ value == 'false' ? false : value
50
+ end
51
+ when 'object'
52
+ convert_object(value, schema)
53
+ when 'array'
54
+ convert_array(value, schema)
55
+ else
56
+ value
57
+ end
58
+ end
59
+
60
+ REF = '$ref'.freeze
61
+ private_constant :REF
62
+
63
+ def check_supported!(schema)
64
+ return unless schema&.key?(REF)
65
+
66
+ raise NotSupportedError,
67
+ "$ref is not supported: #{@root_schema.inspect}"
68
+ end
69
+
70
+ def type(schema)
71
+ schema && schema['type']
72
+ end
73
+
74
+ def convert_object(object, schema)
75
+ object.each_with_object({}) do |(key, value), hsh|
76
+ hsh[key] = convert(value, schema['properties']&.fetch(key))
77
+ end
78
+ end
79
+
80
+ def convert_array(array, schema)
81
+ item_schema = schema['items']
82
+ prefix_schemas = schema['prefixItems']
83
+ return convert_array_with_prefixes(array, prefix_schemas, item_schema) if prefix_schemas
84
+
85
+ array.map { |item| convert(item, item_schema) }
86
+ end
87
+
88
+ def convert_array_with_prefixes(array, prefix_schemas, item_schema)
89
+ prefixes =
90
+ array
91
+ .slice(0, prefix_schemas.size)
92
+ .each_with_index
93
+ .map { |item, index| convert(item, prefix_schemas[index]) }
94
+ array =
95
+ array[prefix_schemas.size..].map! do |item|
96
+ convert(item, item_schema)
97
+ end
98
+ prefixes + array
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+
5
+ module OpenapiParameters
6
+ # Cookie parses OpenAPI cookie parameters from a cookie string.
7
+ class Cookie
8
+ # @param parameters [Array<Hash>] The OpenAPI parameter definitions.
9
+ # @param convert [Boolean] Whether to convert the values to the correct type.
10
+ def initialize(parameters, convert: true)
11
+ @parameters = parameters
12
+ @convert = convert
13
+ end
14
+
15
+ # @param cookie_string [String] The cookie string from the request. Example "foo=bar; baz=qux"
16
+ def unpack(cookie_string)
17
+ cookies = Rack::Utils.parse_cookies_header(cookie_string)
18
+ parameters.each_with_object({}) do |parameter, result|
19
+ parameter = Parameter.new(parameter)
20
+ next unless cookies.key?(parameter.name)
21
+
22
+ result[parameter.name] = catch :skip do
23
+ value = unpack_parameter(parameter, cookies)
24
+ @convert ? Converter.call(value, parameter.schema) : value
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :parameters
32
+
33
+ def unpack_parameter(parameter, cookies)
34
+ value = cookies[parameter.name]
35
+ return if value.nil?
36
+ return unpack_object(parameter, value) if parameter.object?
37
+ return unpack_array(value) if parameter.array?
38
+
39
+ value
40
+ end
41
+
42
+ def unpack_array(value)
43
+ value.split(ARRAY_DELIMER)
44
+ end
45
+
46
+ ARRAY_DELIMER = ','
47
+ OBJECT_EXPLODE_SPLITTER = Regexp.union(',', '=').freeze
48
+
49
+ def unpack_object(parameter, value)
50
+ entries =
51
+ if parameter.explode?
52
+ value.split(OBJECT_EXPLODE_SPLITTER)
53
+ else
54
+ value.split(ARRAY_DELIMER)
55
+ end
56
+ throw :skip, value if entries.length.odd?
57
+
58
+ Hash[*entries]
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiParameters
4
+ class Error < StandardError
5
+ end
6
+
7
+ class NotSupportedError < Error
8
+ end
9
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiParameters
4
+ # Header parses OpenAPI parameters from the request headers.
5
+ class Header
6
+ # @param parameters [Array<Hash>] The OpenAPI parameters
7
+ # @param convert [Boolean] Whether to convert the values to the correct type.
8
+ def initialize(parameters, convert: true)
9
+ @parameters = parameters
10
+ @convert = convert
11
+ end
12
+
13
+ # @param headers [Hash] The headers from the request. Use HeadersHash to convert a Rack env to a Hash.
14
+ def unpack(headers)
15
+ parameters.each_with_object({}) do |parameter, result|
16
+ parameter = Parameter.new(parameter)
17
+ next unless headers.key?(parameter.name)
18
+
19
+ result[parameter.name] = catch :skip do
20
+ value = unpack_parameter(parameter, headers)
21
+ @convert ? Converter.call(value, parameter.schema) : value
22
+ end
23
+ end
24
+ end
25
+
26
+ def unpack_env(env)
27
+ unpack(HeadersHash.new(env))
28
+ end
29
+
30
+ attr_reader :parameters
31
+
32
+ private
33
+
34
+ def unpack_parameter(parameter, headers)
35
+ value = headers[parameter.name]
36
+ return value if parameter.primitive?
37
+ return unpack_object(parameter, value) if parameter.object?
38
+ return unpack_array(value) if parameter.array?
39
+ end
40
+
41
+ def unpack_array(value)
42
+ value.split(ARRAY_DELIMER)
43
+ end
44
+
45
+ ARRAY_DELIMER = ','
46
+ OBJECT_EXPLODE_SPLITTER = Regexp.union(',', '=').freeze
47
+
48
+ def unpack_object(parameter, value)
49
+ entries =
50
+ if parameter.explode?
51
+ value.split(OBJECT_EXPLODE_SPLITTER)
52
+ else
53
+ value.split(ARRAY_DELIMER)
54
+ end
55
+ throw :skip, value if entries.length.odd?
56
+
57
+ Hash[*entries]
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiParameters
4
+ # This is a wrapper around the Rack env hash that allows us to access headers with headers names
5
+ class HeadersHash
6
+ # This was copied from this Rack::Request PR: https://github.com/rack/rack/pull/1881
7
+ # It is not yet released in Rack, so we copied it here.
8
+ def initialize(env)
9
+ @env = env
10
+ end
11
+
12
+ def [](k)
13
+ @env[header_to_env_key(k)]
14
+ end
15
+
16
+ def key?(k)
17
+ @env.key?(header_to_env_key(k))
18
+ end
19
+
20
+ def header_to_env_key(k)
21
+ k = k.upcase
22
+ k.tr!('-', '_')
23
+ k = "HTTP_#{k}" unless %w[CONTENT_LENGTH CONTENT_TYPE].include?(k)
24
+ k
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiParameters
4
+ ##
5
+ # Represents a parameter in an OpenAPI operation.
6
+ class Parameter
7
+ def initialize(definition)
8
+ check_supported!(definition)
9
+ @definition = definition
10
+ end
11
+
12
+ attr_reader :definition
13
+
14
+ def name
15
+ definition['name']
16
+ end
17
+
18
+ ##
19
+ # @return [String] The location of the parameter in the request, "path", "query", "header" or "cookie".
20
+ def location
21
+ definition['in']
22
+ end
23
+
24
+ alias in location
25
+
26
+ def schema
27
+ return definition.dig('content', media_type, 'schema') if media_type
28
+
29
+ definition['schema']
30
+ end
31
+
32
+ def media_type
33
+ definition['content']&.keys&.first
34
+ end
35
+
36
+ def type
37
+ schema && schema['type']
38
+ end
39
+
40
+ def primitive?
41
+ type != 'object' && type != 'array'
42
+ end
43
+
44
+ def array?
45
+ type == 'array'
46
+ end
47
+
48
+ def object?
49
+ type == 'object'
50
+ end
51
+
52
+ def style
53
+ return definition['style'] if definition['style']
54
+
55
+ DEFAULT_STYLE.fetch(location)
56
+ end
57
+
58
+ def required?
59
+ return true if location == 'path'
60
+
61
+ definition['required'] == true
62
+ end
63
+
64
+ def deprecated?
65
+ definition['deprecated'] == true
66
+ end
67
+
68
+ def allow_reserved?
69
+ definition['allowReserved'] == true
70
+ end
71
+
72
+ def explode?
73
+ return definition['explode'] if definition.key?('explode')
74
+ return true if style == 'form'
75
+
76
+ false
77
+ end
78
+
79
+ private
80
+
81
+ DEFAULT_STYLE = {
82
+ 'query' => 'form',
83
+ 'path' => 'simple',
84
+ 'header' => 'simple',
85
+ 'cookie' => 'form'
86
+ }.freeze
87
+ private_constant :DEFAULT_STYLE
88
+
89
+
90
+ VALID_LOCATIONS = Set.new(%w[query header path cookie]).freeze
91
+ private_constant :VALID_LOCATIONS
92
+
93
+ REF = '$ref'.freeze
94
+ private_constant :REF
95
+
96
+ def check_supported!(definition)
97
+ if definition.values.any? { |v| v.is_a?(Hash) && v.key?(REF) }
98
+ raise NotSupportedError,
99
+ "Parameter schema with $ref is not supported: #{definition.inspect}"
100
+ end
101
+ return if VALID_LOCATIONS.include?(definition['in'])
102
+
103
+ raise ArgumentError,
104
+ %(Parameter definition must have an 'in' property defined
105
+ which should be one of #{VALID_LOCATIONS.join(', ')}).freeze
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri_template'
4
+
5
+ module OpenapiParameters
6
+ # Parses OpenAPI path parameters from path template strings and the request path.
7
+ class Path
8
+ # @param parameters [Array<Hash>] The OpenAPI path parameters.
9
+ # @param path [String] The OpenAPI path template string.
10
+ # @param convert [Boolean] Whether to convert the values to the correct type.
11
+ def initialize(parameters, path, convert: true)
12
+ @parameters = parameters
13
+ @path = path
14
+ @convert = convert
15
+ end
16
+
17
+ attr_reader :parameters, :path
18
+
19
+ def unpack(path_info)
20
+ parsed_path = URITemplate.new(url_template).extract(path_info) || {}
21
+ parameters.each_with_object(parsed_path) do |param, result|
22
+ parameter = Parameter.new(param)
23
+ next unless parsed_path.key?(parameter.name)
24
+
25
+ result[parameter.name] = catch :skip do
26
+ value = unpack_parameter(parameter, result)
27
+ @convert ? Converter.call(value, parameter.schema) : value
28
+ end
29
+ end
30
+ end
31
+
32
+ def unpack_parameter(parameter, parsed_path)
33
+ value = parsed_path[parameter.name]
34
+ if parameter.object? && value.is_a?(Array)
35
+ throw :skip, value if value.length.odd?
36
+ return Hash[*value]
37
+ end
38
+ value
39
+ end
40
+
41
+ def url_template
42
+ @url_template ||=
43
+ begin
44
+ path = @path.dup
45
+ parameters.each do |p|
46
+ param = Parameter.new(p)
47
+ next unless param.array? || param.object?
48
+
49
+ path.gsub!(
50
+ "{#{param.name}}",
51
+ "{#{operator(param)}#{param.name}#{modifier(param)}}"
52
+ )
53
+ end
54
+ path
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ LIST_OPS = { 'simple' => nil, 'label' => '.', 'matrix' => ';' }.freeze
61
+ private_constant :LIST_OPS
62
+
63
+ def operator(param)
64
+ LIST_OPS[param.style]
65
+ end
66
+
67
+ def modifier(param)
68
+ return '*' if param.explode?
69
+ return if param.style == 'matrix' && !param.explode?
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+
5
+ module OpenapiParameters
6
+ # Query parses query parameters from a http query strings.
7
+ class Query
8
+ # @param parameters [Array<Hash>] The OpenAPI query parameter definitions.
9
+ # @param convert [Boolean] Whether to convert the values to the correct type.
10
+ def initialize(parameters, convert: true)
11
+ @parameters = parameters
12
+ @convert = convert
13
+ end
14
+
15
+ def unpack(query_string) # rubocop:disable Metrics/AbcSize
16
+ parsed_query = Rack::Utils.parse_query(query_string)
17
+ parameters.each_with_object({}) do |parameter, result|
18
+ parameter = Parameter.new(parameter)
19
+ if parameter.style == 'deepObject' && parameter.object?
20
+ parsed_nested_query = Rack::Utils.parse_nested_query(query_string)
21
+ next unless parsed_nested_query.key?(parameter.name)
22
+
23
+ result[parameter.name] = convert(parsed_nested_query[parameter.name], parameter)
24
+ else
25
+ next unless parsed_query.key?(parameter.name)
26
+
27
+ unpacked = unpack_parameter(parameter, parsed_query)
28
+ result[parameter.name] = convert(unpacked, parameter)
29
+ end
30
+ end
31
+ end
32
+
33
+ attr_reader :parameters
34
+
35
+ private
36
+
37
+ def convert(value, parameter)
38
+ return value unless @convert
39
+ return value if value == ''
40
+
41
+ Converter.call(value, parameter.schema)
42
+ end
43
+
44
+ QUERY_PARAMETER_DELIMETER = '&'
45
+ ARRAY_DELIMER = ','
46
+
47
+ def unpack_parameter(parameter, parsed_query)
48
+ value = parsed_query[parameter.name]
49
+ return value if parameter.primitive? || value.nil?
50
+ return unpack_array(parameter, parsed_query) if parameter.array?
51
+ return unpack_object(parameter, parsed_query) if parameter.object?
52
+ end
53
+
54
+ def unpack_array(parameter, parsed_query)
55
+ value = parsed_query[parameter.name]
56
+ return value if value.empty?
57
+ return Array(value) if parameter.explode?
58
+
59
+ value.split(array_delimiter(parameter.style))
60
+ end
61
+
62
+ def unpack_object(parameter, parsed_query)
63
+ return parsed_query[parameter.name] if parameter.explode?
64
+
65
+ array = parsed_query[parameter.name]&.split(ARRAY_DELIMER)
66
+ return array if array.length.odd?
67
+
68
+ Hash[*array]
69
+ end
70
+
71
+ DELIMERS = {
72
+ 'pipeDelimited' => '|',
73
+ 'spaceDelimited' => ' ',
74
+ 'form' => ',',
75
+ 'simple' => ','
76
+ }.freeze
77
+
78
+ def array_delimiter(style)
79
+ DELIMERS.fetch(style, ARRAY_DELIMER)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiParameters
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+ loader = Zeitwerk::Loader.for_gem
5
+ loader.setup
6
+
7
+ # OpenapiParameters is a gem that parses OpenAPI parameters from Rack
8
+ module OpenapiParameters
9
+ end
10
+
11
+ require_relative 'openapi_parameters/errors'
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/openapi_parameters/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'openapi_parameters'
7
+ spec.version = OpenapiParameters::VERSION
8
+ spec.authors = ['Andreas Haller']
9
+ spec.email = ['andreas.haller@posteo.de']
10
+
11
+ spec.summary = 'OpenapiParameters is an OpenAPI aware parameter parser'
12
+ spec.description =
13
+ 'OpenapiParameters parses HTTP query parameters exactly as described in an OpenAPI API description.'
14
+ spec.homepage = 'https://github.com/ahx/OpenapiParameters'
15
+ spec.required_ruby_version = '>= 3.1.0'
16
+ spec.licenses = ['MIT']
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/ahx/OpenapiParameters'
20
+ spec.metadata[
21
+ 'changelog_uri'
22
+ ] = 'https://github.com/ahx/openapi_parameters/blob/main/CHANGELOG.md'
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
26
+ spec.files =
27
+ Dir.chdir(__dir__) do
28
+ `git ls-files -z`.split("\x0")
29
+ .reject do |f|
30
+ (f == __FILE__) ||
31
+ f.match(
32
+ %r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}
33
+ )
34
+ end
35
+ end
36
+ spec.bindir = 'exe'
37
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
38
+ spec.require_paths = ['lib']
39
+
40
+ spec.add_dependency 'rack', '>= 2.2'
41
+ spec.add_dependency 'uri_template', '>= 0.7', '< 2.0'
42
+ spec.add_dependency 'zeitwerk', '~> 2.6'
43
+
44
+ # For more information and examples about making a new gem, check out our
45
+ # guide at: https://bundler.io/guides/creating_gem.html
46
+ spec.metadata['rubygems_mfa_required'] = 'true'
47
+ end
@@ -0,0 +1,4 @@
1
+ module OpenapiParameters
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: openapi_parameters
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andreas Haller
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-03-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '2.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: uri_template
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0.7'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '2.0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0.7'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: zeitwerk
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.6'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.6'
61
+ description: OpenapiParameters parses HTTP query parameters exactly as described in
62
+ an OpenAPI API description.
63
+ email:
64
+ - andreas.haller@posteo.de
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - ".rspec"
70
+ - ".rubocop.yml"
71
+ - ".tool-versions"
72
+ - CHANGELOG.md
73
+ - Gemfile
74
+ - Gemfile.lock
75
+ - LICENSE
76
+ - README.md
77
+ - Rakefile
78
+ - lib/openapi_parameters.rb
79
+ - lib/openapi_parameters/converter.rb
80
+ - lib/openapi_parameters/cookie.rb
81
+ - lib/openapi_parameters/errors.rb
82
+ - lib/openapi_parameters/header.rb
83
+ - lib/openapi_parameters/headers_hash.rb
84
+ - lib/openapi_parameters/parameter.rb
85
+ - lib/openapi_parameters/path.rb
86
+ - lib/openapi_parameters/query.rb
87
+ - lib/openapi_parameters/version.rb
88
+ - openapi_parameters.gemspec
89
+ - sig/openapi_parameters.rbs
90
+ homepage: https://github.com/ahx/OpenapiParameters
91
+ licenses:
92
+ - MIT
93
+ metadata:
94
+ homepage_uri: https://github.com/ahx/OpenapiParameters
95
+ source_code_uri: https://github.com/ahx/OpenapiParameters
96
+ changelog_uri: https://github.com/ahx/openapi_parameters/blob/main/CHANGELOG.md
97
+ rubygems_mfa_required: 'true'
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 3.1.0
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubygems_version: 3.4.6
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: OpenapiParameters is an OpenAPI aware parameter parser
117
+ test_files: []