openapi_first 3.4.2 → 4.0.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 +4 -4
- data/CHANGELOG.md +70 -0
- data/README.md +95 -42
- data/lib/openapi_first/builder.rb +87 -52
- data/lib/openapi_first/child_configuration.rb +0 -2
- data/lib/openapi_first/configuration.rb +0 -23
- data/lib/openapi_first/definition.rb +35 -2
- data/lib/openapi_first/failure.rb +5 -1
- data/lib/openapi_first/middlewares/request_validation.rb +1 -1
- data/lib/openapi_first/middlewares/response_validation.rb +1 -1
- data/lib/openapi_first/parameter/converter/array_converter.rb +42 -0
- data/lib/openapi_first/parameter/converter/object_converter.rb +60 -0
- data/lib/openapi_first/parameter/converter.rb +69 -0
- data/lib/openapi_first/parameter/unpackers.rb +132 -0
- data/lib/openapi_first/parameter.rb +70 -0
- data/lib/openapi_first/parameter_content_parsers.rb +55 -0
- data/lib/openapi_first/parameters_parser.rb +23 -0
- data/lib/openapi_first/query_string_parser.rb +93 -0
- data/lib/openapi_first/ref_resolver.rb +56 -3
- data/lib/openapi_first/request.rb +12 -18
- data/lib/openapi_first/request_body_parsers.rb +19 -16
- data/lib/openapi_first/request_headers.rb +27 -0
- data/lib/openapi_first/request_validator.rb +4 -1
- data/lib/openapi_first/response_header.rb +9 -0
- data/lib/openapi_first/response_parser.rb +3 -10
- data/lib/openapi_first/router.rb +27 -12
- data/lib/openapi_first/schema/hash.rb +0 -1
- data/lib/openapi_first/sinatra.rb +217 -0
- data/lib/openapi_first/test/configuration.rb +0 -34
- data/lib/openapi_first/test/coverage/html_reporter/context.rb +24 -17
- data/lib/openapi_first/test/coverage/html_reporter.css +214 -67
- data/lib/openapi_first/test/coverage/html_reporter.html.erb +39 -11
- data/lib/openapi_first/test/coverage/html_reporter.rb +11 -1
- data/lib/openapi_first/test/coverage/plan.rb +30 -10
- data/lib/openapi_first/test/coverage/request_task.rb +7 -2
- data/lib/openapi_first/test/coverage/response_task.rb +6 -1
- data/lib/openapi_first/test/coverage/route_task.rb +23 -1
- data/lib/openapi_first/test/coverage/skipped_summary.rb +22 -0
- data/lib/openapi_first/test/coverage/terminal_reporter.rb +23 -11
- data/lib/openapi_first/test/coverage.rb +9 -3
- data/lib/openapi_first/test.rb +69 -11
- data/lib/openapi_first/validators/multipart_request_body.rb +57 -0
- data/lib/openapi_first/validators/request_body.rb +20 -7
- data/lib/openapi_first/validators/request_parameters.rb +5 -4
- data/lib/openapi_first/version.rb +1 -1
- metadata +15 -23
- data/lib/openapi_first/header.rb +0 -9
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module OpenapiFirst
|
|
4
4
|
# A failure object returned when validation or parsing of a request or response has failed.
|
|
5
5
|
# This returned in ValidatedRequest#error and ValidatedResponse#error.
|
|
6
|
-
class Failure < Data.define(:type, :message, :errors)
|
|
6
|
+
class Failure < Data.define(:type, :message, :errors)
|
|
7
7
|
TYPES = {
|
|
8
8
|
not_found: [NotFoundError, 'Not found.'],
|
|
9
9
|
method_not_allowed: [RequestInvalidError, 'Request method is not defined.'],
|
|
@@ -49,6 +49,10 @@ module OpenapiFirst
|
|
|
49
49
|
alias original_message message
|
|
50
50
|
private :original_message
|
|
51
51
|
|
|
52
|
+
def inspect
|
|
53
|
+
"#<OpenapiFirst::Failure:#{object_id} type: #{type}, message: #{message}>"
|
|
54
|
+
end
|
|
55
|
+
|
|
52
56
|
# A generic error message
|
|
53
57
|
def message
|
|
54
58
|
original_message || exception_message
|
|
@@ -36,7 +36,7 @@ module OpenapiFirst
|
|
|
36
36
|
options = spec
|
|
37
37
|
spec = options[:spec]
|
|
38
38
|
end
|
|
39
|
-
@raise = options.fetch(:raise_error,
|
|
39
|
+
@raise = options.fetch(:raise_error, false)
|
|
40
40
|
@error_response_class = error_response_option(options[:error_response])
|
|
41
41
|
|
|
42
42
|
spec ||= :default
|
|
@@ -20,7 +20,7 @@ module OpenapiFirst
|
|
|
20
20
|
options = spec
|
|
21
21
|
spec = options[:spec]
|
|
22
22
|
end
|
|
23
|
-
@raise = options.fetch(:raise_error,
|
|
23
|
+
@raise = options.fetch(:raise_error, true)
|
|
24
24
|
|
|
25
25
|
spec ||= :default
|
|
26
26
|
spec = OpenapiFirst[spec] if spec.is_a?(Symbol)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiFirst
|
|
4
|
+
class Parameter
|
|
5
|
+
module Converter
|
|
6
|
+
# Converts the items of an array parameter value
|
|
7
|
+
# @visibility private
|
|
8
|
+
ArrayConverter = Data.define(:schema) do
|
|
9
|
+
def call(value)
|
|
10
|
+
return [] if value.nil? || value.empty?
|
|
11
|
+
|
|
12
|
+
convert_array(value)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def convert_array(array)
|
|
18
|
+
return array unless array.is_a?(Array)
|
|
19
|
+
|
|
20
|
+
item_schema = schema['items']
|
|
21
|
+
prefix_schemas = schema['prefixItems']
|
|
22
|
+
return convert_array_with_prefixes(array, prefix_schemas, item_schema) if prefix_schemas
|
|
23
|
+
|
|
24
|
+
array.map { Converter.convert(_1, item_schema) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def convert_array_with_prefixes(array, prefix_schemas, item_schema)
|
|
28
|
+
prefixes =
|
|
29
|
+
array
|
|
30
|
+
.slice(0, prefix_schemas.size)
|
|
31
|
+
.each_with_index
|
|
32
|
+
.map { |item, index| Converter.convert(item, prefix_schemas[index]) }
|
|
33
|
+
array =
|
|
34
|
+
array[prefix_schemas.size..].map! do |item|
|
|
35
|
+
Converter.convert(item, item_schema)
|
|
36
|
+
end
|
|
37
|
+
prefixes + array
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiFirst
|
|
4
|
+
class Parameter
|
|
5
|
+
module Converter
|
|
6
|
+
# Converts the properties of an object parameter value
|
|
7
|
+
# @visibility private
|
|
8
|
+
ObjectConverter = Data.define(:schema) do
|
|
9
|
+
def self.get_properties(schema) # rubocop:disable Metrics
|
|
10
|
+
return nil if schema.nil? || schema.empty?
|
|
11
|
+
|
|
12
|
+
direct_props = schema['properties']
|
|
13
|
+
additional_props = schema['additionalProperties']
|
|
14
|
+
|
|
15
|
+
composition_props = []
|
|
16
|
+
|
|
17
|
+
%w[allOf oneOf anyOf].each do |keyword|
|
|
18
|
+
next unless (array = schema[keyword])
|
|
19
|
+
|
|
20
|
+
array.each do |sub_schema|
|
|
21
|
+
if (props = sub_schema['properties'])
|
|
22
|
+
composition_props << props
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
%w[then else].each do |keyword|
|
|
28
|
+
next unless (sub_schema = schema[keyword])
|
|
29
|
+
|
|
30
|
+
if (props = sub_schema['properties'])
|
|
31
|
+
composition_props << props
|
|
32
|
+
end
|
|
33
|
+
if (add_props = sub_schema['additionalProperties']) && add_props.is_a?(Hash) && !add_props.empty?
|
|
34
|
+
composition_props << add_props
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
composition_props << additional_props if additional_props.is_a?(Hash) && !additional_props.empty?
|
|
39
|
+
|
|
40
|
+
return direct_props if composition_props.empty? && direct_props
|
|
41
|
+
return nil if direct_props.nil? && composition_props.empty?
|
|
42
|
+
|
|
43
|
+
result = direct_props ? direct_props.dup : {}
|
|
44
|
+
composition_props.each { result.merge!(_1) }
|
|
45
|
+
result
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def call(value)
|
|
49
|
+
return value unless value.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
properties = self.class.get_properties(schema)
|
|
52
|
+
|
|
53
|
+
value.each_with_object({}) do |(key, val), hsh|
|
|
54
|
+
hsh[key] = Converter.convert(val, properties&.fetch(key, nil))
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'converter/array_converter'
|
|
4
|
+
require_relative 'converter/object_converter'
|
|
5
|
+
|
|
6
|
+
module OpenapiFirst
|
|
7
|
+
class Parameter
|
|
8
|
+
# Converts a parameter value (string) to the type specified in the JSON Schema.
|
|
9
|
+
# @visibility private
|
|
10
|
+
module Converter
|
|
11
|
+
PASS_THROUGH = ->(value) { value }
|
|
12
|
+
|
|
13
|
+
INTEGER = lambda do |value|
|
|
14
|
+
Integer(value, 10)
|
|
15
|
+
rescue StandardError
|
|
16
|
+
value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
NUMBER = lambda do |value|
|
|
20
|
+
Float(value)
|
|
21
|
+
rescue StandardError
|
|
22
|
+
value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
BOOLEAN = lambda do |value|
|
|
26
|
+
if value == 'true'
|
|
27
|
+
true
|
|
28
|
+
else
|
|
29
|
+
value == 'false' ? false : value
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# Returns a callable that converts a value as described in the schema
|
|
35
|
+
# @param schema [Hash, nil]
|
|
36
|
+
def [](schema)
|
|
37
|
+
case schema && schema['type']
|
|
38
|
+
when 'integer' then INTEGER
|
|
39
|
+
when 'number' then NUMBER
|
|
40
|
+
when 'boolean' then BOOLEAN
|
|
41
|
+
when 'object' then ObjectConverter.new(schema)
|
|
42
|
+
when 'array' then ArrayConverter.new(schema)
|
|
43
|
+
else
|
|
44
|
+
return ObjectConverter.new(schema) if object_like?(schema)
|
|
45
|
+
|
|
46
|
+
PASS_THROUGH
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Converts a nested value, like an array item or an object property
|
|
51
|
+
def convert(value, schema)
|
|
52
|
+
return if value.nil?
|
|
53
|
+
return value if schema.nil?
|
|
54
|
+
|
|
55
|
+
self[schema].call(value)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
OBJECT_KEYWORDS = %w[properties oneOf allOf anyOf].freeze
|
|
61
|
+
private_constant :OBJECT_KEYWORDS
|
|
62
|
+
|
|
63
|
+
def object_like?(schema)
|
|
64
|
+
schema && OBJECT_KEYWORDS.any? { schema[_1] }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack'
|
|
4
|
+
require_relative '../parameter_content_parsers'
|
|
5
|
+
|
|
6
|
+
module OpenapiFirst
|
|
7
|
+
class Parameter
|
|
8
|
+
# Resolves the unpacker of a parameter once, at Parameter construction time.
|
|
9
|
+
# Each unpacker is a callable that takes a raw string (or already-parsed
|
|
10
|
+
# value) and returns the unpacked Ruby value, throwing :skip on unrecoverable
|
|
11
|
+
# parse errors.
|
|
12
|
+
# @visibility private
|
|
13
|
+
module Unpackers
|
|
14
|
+
ARRAY_DELIMITER = {
|
|
15
|
+
'label' => '.',
|
|
16
|
+
'simple' => ',',
|
|
17
|
+
'form' => ',',
|
|
18
|
+
'pipeDelimited' => '|',
|
|
19
|
+
'spaceDelimited' => ' '
|
|
20
|
+
}.freeze
|
|
21
|
+
private_constant :ARRAY_DELIMITER
|
|
22
|
+
|
|
23
|
+
PREFIXED_STYLES = %w[label matrix].freeze
|
|
24
|
+
private_constant :PREFIXED_STYLES
|
|
25
|
+
|
|
26
|
+
OBJECT_EXPLODE_SPLITTER = Regexp.union(',', '=').freeze
|
|
27
|
+
private_constant :OBJECT_EXPLODE_SPLITTER
|
|
28
|
+
|
|
29
|
+
PassThrough = ->(value) { value }
|
|
30
|
+
|
|
31
|
+
DelimitedArray = Data.define(:delimiter, :strip_prefix) do
|
|
32
|
+
def call(value)
|
|
33
|
+
return value if value.is_a?(::Array)
|
|
34
|
+
return value if value.empty?
|
|
35
|
+
|
|
36
|
+
value = value[1..] if strip_prefix
|
|
37
|
+
value.split(delimiter)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
MatrixArray = Data.define(:name, :explode) do
|
|
42
|
+
def call(value)
|
|
43
|
+
return value if value.is_a?(::Array)
|
|
44
|
+
return value if value.empty?
|
|
45
|
+
|
|
46
|
+
result = Unpackers.parse_query(value, ';')[name]
|
|
47
|
+
return result if explode
|
|
48
|
+
return result unless result.is_a?(::String)
|
|
49
|
+
|
|
50
|
+
result.split(',')
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
ExplodeFormObject = lambda do |value|
|
|
55
|
+
throw :skip, value unless value.is_a?(::String)
|
|
56
|
+
|
|
57
|
+
entries = value.split(OBJECT_EXPLODE_SPLITTER)
|
|
58
|
+
throw :skip, value if entries.length.odd?
|
|
59
|
+
|
|
60
|
+
Hash[*entries]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
DelimitedObject = Data.define(:delimiter) do
|
|
64
|
+
def call(value)
|
|
65
|
+
throw :skip, value unless value.is_a?(::String)
|
|
66
|
+
|
|
67
|
+
entries = value.split(delimiter)
|
|
68
|
+
throw :skip, value if entries.length.odd?
|
|
69
|
+
|
|
70
|
+
Hash[*entries]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
ExplodePathObject = ->(value) { Unpackers.parse_query(value, ',') }
|
|
75
|
+
|
|
76
|
+
NonExplodePathObject = Data.define(:array_unpacker) do
|
|
77
|
+
def call(value)
|
|
78
|
+
array = array_unpacker.call(value)
|
|
79
|
+
throw :skip, value if array.length.odd?
|
|
80
|
+
|
|
81
|
+
Hash[*array]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class << self
|
|
86
|
+
# Values that are not encoded as described are left to schema validation
|
|
87
|
+
def parse_query(value, delimiter)
|
|
88
|
+
Rack::Utils.parse_query(value, delimiter)
|
|
89
|
+
rescue ArgumentError
|
|
90
|
+
throw :skip, value
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def find(parameter)
|
|
94
|
+
return find_media_type(parameter) if parameter.media_type
|
|
95
|
+
return find_array(parameter) if parameter.array?
|
|
96
|
+
return find_object(parameter) if parameter.object?
|
|
97
|
+
|
|
98
|
+
PassThrough
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def find_media_type(parameter)
|
|
104
|
+
ParameterContentParsers[parameter.media_type] || PassThrough
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def find_array(parameter)
|
|
108
|
+
style = parameter.style
|
|
109
|
+
return MatrixArray.new(name: parameter.name, explode: parameter.explode?) if style == 'matrix'
|
|
110
|
+
|
|
111
|
+
DelimitedArray.new(
|
|
112
|
+
delimiter: ARRAY_DELIMITER[style],
|
|
113
|
+
strip_prefix: PREFIXED_STYLES.include?(style)
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def find_object(parameter)
|
|
118
|
+
return find_path_object(parameter) if parameter.location == 'path'
|
|
119
|
+
return ExplodeFormObject if parameter.explode?
|
|
120
|
+
|
|
121
|
+
DelimitedObject.new(delimiter: ARRAY_DELIMITER[parameter.style])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def find_path_object(parameter)
|
|
125
|
+
return ExplodePathObject if parameter.explode?
|
|
126
|
+
|
|
127
|
+
NonExplodePathObject.new(array_unpacker: find_array(parameter))
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'parameter/converter'
|
|
4
|
+
require_relative 'parameter/unpackers'
|
|
5
|
+
|
|
6
|
+
module OpenapiFirst
|
|
7
|
+
# A parameter of a request, or a header of a response.
|
|
8
|
+
class Parameter
|
|
9
|
+
DEFAULT_STYLE = {
|
|
10
|
+
'query' => 'form',
|
|
11
|
+
'path' => 'simple',
|
|
12
|
+
'header' => 'simple',
|
|
13
|
+
'cookie' => 'form'
|
|
14
|
+
}.freeze
|
|
15
|
+
private_constant :DEFAULT_STYLE
|
|
16
|
+
|
|
17
|
+
# @param definition [Hash] The OpenAPI Parameter Object. A string keyed Hash.
|
|
18
|
+
# @param schema [Hash, nil] The resolved JSON Schema of this parameter.
|
|
19
|
+
def initialize(definition, schema:)
|
|
20
|
+
@name = definition['name']
|
|
21
|
+
@schema = schema
|
|
22
|
+
@location = definition['in']
|
|
23
|
+
@media_type = definition['content']&.keys&.first
|
|
24
|
+
@style = definition['style'] || DEFAULT_STYLE.fetch(@location)
|
|
25
|
+
@explode = definition.fetch('explode') { @style == 'form' }
|
|
26
|
+
@deep_object = @style == 'deepObject'
|
|
27
|
+
@required = @location == 'path' || definition['required'] == true
|
|
28
|
+
@deprecated = definition['deprecated'] == true
|
|
29
|
+
@converter = Converter[schema]
|
|
30
|
+
@unpacker = Unpackers.find(self)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
attr_reader :name, :schema, :location, :media_type, :style
|
|
34
|
+
|
|
35
|
+
def unpack(value)
|
|
36
|
+
return value if value.nil?
|
|
37
|
+
|
|
38
|
+
@unpacker.call(value)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def convert(value) = @converter.call(value)
|
|
42
|
+
|
|
43
|
+
# Unpacks and converts a raw value.
|
|
44
|
+
# For a query parameter, a value that cannot be unpacked is still converted.
|
|
45
|
+
# For any other location, such a value is returned as it is, unconverted.
|
|
46
|
+
def unpack_and_convert(value)
|
|
47
|
+
return convert(catch(:skip) { unpack(value) }) if location == 'query'
|
|
48
|
+
|
|
49
|
+
catch(:skip) { convert(unpack(value)) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def explode? = @explode
|
|
53
|
+
|
|
54
|
+
def required? = @required
|
|
55
|
+
|
|
56
|
+
def deprecated? = @deprecated
|
|
57
|
+
|
|
58
|
+
def deep_object? = @deep_object
|
|
59
|
+
|
|
60
|
+
def type = schema && schema['type']
|
|
61
|
+
|
|
62
|
+
def array? = type == 'array'
|
|
63
|
+
|
|
64
|
+
def object? = type == 'object' || deep_object? || schema&.key?('properties')
|
|
65
|
+
|
|
66
|
+
# The properties of an object parameter, merged across composition keywords.
|
|
67
|
+
# @return [Hash, nil]
|
|
68
|
+
def object_properties = Converter::ObjectConverter.get_properties(schema)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module OpenapiFirst
|
|
6
|
+
# Registry of parsers for parameters that use a `content` field, keyed by media type.
|
|
7
|
+
#
|
|
8
|
+
# A parser is a callable that takes a raw string and returns the parsed value.
|
|
9
|
+
# It should `throw :skip, value` if the input cannot be parsed, so the
|
|
10
|
+
# parameter value is used as is.
|
|
11
|
+
#
|
|
12
|
+
# OpenapiFirst::ParameterContentParsers.register('application/xml', ->(value) { ... })
|
|
13
|
+
module ParameterContentParsers
|
|
14
|
+
@parsers = []
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
attr_reader :parsers
|
|
18
|
+
|
|
19
|
+
# @param matcher [String, Regexp] exact media type or a pattern.
|
|
20
|
+
# @param parser [#call] callable that takes the raw string and returns the parsed value.
|
|
21
|
+
def register(matcher, parser)
|
|
22
|
+
parsers.reject! { |existing, _| existing == matcher }
|
|
23
|
+
parsers << [matcher, parser]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param media_type [String, nil]
|
|
27
|
+
# @return [#call, nil] the parser, or nil if none is registered.
|
|
28
|
+
def [](media_type)
|
|
29
|
+
return nil if media_type.nil?
|
|
30
|
+
|
|
31
|
+
parsers.each do |matcher, parser|
|
|
32
|
+
return parser if match?(matcher, media_type)
|
|
33
|
+
end
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def match?(matcher, media_type)
|
|
40
|
+
case matcher
|
|
41
|
+
when Regexp then matcher.match?(media_type)
|
|
42
|
+
else matcher == media_type
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
register(%r{\A[\w.+-]+/(?:[\w.-]+\+)?json\z}i, lambda do |value|
|
|
48
|
+
throw :skip, value unless value.is_a?(String)
|
|
49
|
+
|
|
50
|
+
JSON.parse(value)
|
|
51
|
+
rescue JSON::ParserError
|
|
52
|
+
throw :skip, value
|
|
53
|
+
end)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiFirst
|
|
4
|
+
# Unpacks parameters from a Hash of raw values, like path parameters, headers or cookies.
|
|
5
|
+
# @visibility private
|
|
6
|
+
class ParametersParser
|
|
7
|
+
# @param parameters [Array<Parameter>]
|
|
8
|
+
def initialize(parameters)
|
|
9
|
+
@parameters = parameters
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :parameters
|
|
13
|
+
|
|
14
|
+
# @param parameters_hash [Hash] The raw values, keyed by parameter name.
|
|
15
|
+
def unpack(parameters_hash)
|
|
16
|
+
parameters.each_with_object({}) do |parameter, result|
|
|
17
|
+
next unless parameters_hash.key?(parameter.name)
|
|
18
|
+
|
|
19
|
+
result[parameter.name] = parameter.unpack_and_convert(parameters_hash[parameter.name])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack'
|
|
4
|
+
require_relative 'parameters_parser'
|
|
5
|
+
|
|
6
|
+
module OpenapiFirst
|
|
7
|
+
# Unpacks query parameters from a query string.
|
|
8
|
+
# @visibility private
|
|
9
|
+
class QueryStringParser
|
|
10
|
+
DEEP_PROP = '\[([\w-]+)\]$'
|
|
11
|
+
private_constant :DEEP_PROP
|
|
12
|
+
|
|
13
|
+
# @param parameters [Array<Parameter>]
|
|
14
|
+
def initialize(parameters)
|
|
15
|
+
@parameters = parameters
|
|
16
|
+
@deep_object_parameters, flat_parameters = parameters.partition(&:deep_object?)
|
|
17
|
+
@flat_parser = ParametersParser.new(flat_parameters)
|
|
18
|
+
@deep_object_properties = {}
|
|
19
|
+
@deep_object_regex = {}
|
|
20
|
+
@deep_object_parameters.each do |parameter|
|
|
21
|
+
@deep_object_properties[parameter.name] = parameter.object_properties
|
|
22
|
+
@deep_object_regex[parameter.name] = /^#{Regexp.escape(parameter.name)}#{DEEP_PROP}/
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_reader :parameters
|
|
27
|
+
|
|
28
|
+
def unpack(query_string)
|
|
29
|
+
parsed_query = parse_query(query_string)
|
|
30
|
+
result = @flat_parser.unpack(parsed_query)
|
|
31
|
+
@deep_object_parameters.each do |parameter|
|
|
32
|
+
name = parameter.name
|
|
33
|
+
if parsed_query.key?(name)
|
|
34
|
+
result[name] = parameter.convert(parsed_query[name])
|
|
35
|
+
else
|
|
36
|
+
value = parse_deep_object(parameter, parsed_query)
|
|
37
|
+
result[name] = parameter.convert(value) unless value.empty?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns query parameters that are not defined in the API description
|
|
44
|
+
def unknown_values(query_string)
|
|
45
|
+
parsed_query = parse_query(query_string)
|
|
46
|
+
known_parameter_names = parameters.to_set(&:name)
|
|
47
|
+
|
|
48
|
+
unknown = parsed_query.each_with_object({}) do |(key, value), result|
|
|
49
|
+
next if known_parameter_names.include?(key)
|
|
50
|
+
next if @deep_object_parameters.any? { key.start_with?("#{_1.name}[") }
|
|
51
|
+
|
|
52
|
+
result[key] = value
|
|
53
|
+
end
|
|
54
|
+
return if unknown.empty?
|
|
55
|
+
|
|
56
|
+
unknown
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def parse_query(query_string)
|
|
62
|
+
Rack::Utils.parse_query(query_string) do |string|
|
|
63
|
+
Rack::Utils.unescape(string)
|
|
64
|
+
rescue ArgumentError => e
|
|
65
|
+
raise Rack::Utils::InvalidParameterError, e.message
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_deep_object(parameter, parsed_query)
|
|
70
|
+
name = parameter.name
|
|
71
|
+
prop_regx = @deep_object_regex[name]
|
|
72
|
+
properties_schema = @deep_object_properties[name]
|
|
73
|
+
|
|
74
|
+
parsed_query.each.with_object({}) do |(key, value), result|
|
|
75
|
+
prop_key = key.match(prop_regx)&.[](1)
|
|
76
|
+
next if prop_key.nil?
|
|
77
|
+
|
|
78
|
+
is_array = properties_schema&.dig(prop_key, 'type') == 'array'
|
|
79
|
+
result[prop_key] = explode_value(value, parameter, is_array)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def explode_value(value, parameter, is_array)
|
|
84
|
+
value = Array(value)
|
|
85
|
+
if is_array
|
|
86
|
+
return value if parameter.explode?
|
|
87
|
+
|
|
88
|
+
return [value.last]
|
|
89
|
+
end
|
|
90
|
+
value.last
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|