openapi_parameters 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []