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