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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +16 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +69 -0
- data/LICENSE +21 -0
- data/README.md +89 -0
- data/Rakefile +10 -0
- data/lib/openapi_parameters/converter.rb +101 -0
- data/lib/openapi_parameters/cookie.rb +61 -0
- data/lib/openapi_parameters/errors.rb +9 -0
- data/lib/openapi_parameters/header.rb +60 -0
- data/lib/openapi_parameters/headers_hash.rb +27 -0
- data/lib/openapi_parameters/parameter.rb +108 -0
- data/lib/openapi_parameters/path.rb +72 -0
- data/lib/openapi_parameters/query.rb +82 -0
- data/lib/openapi_parameters/version.rb +5 -0
- data/lib/openapi_parameters.rb +11 -0
- data/openapi_parameters.gemspec +47 -0
- data/sig/openapi_parameters.rbs +4 -0
- metadata +117 -0
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
data/.rubocop.yml
ADDED
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 3.2.1
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
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,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,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,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
|
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: []
|