openapi_first 0.20.0 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,7 +18,8 @@ examples = [
18
18
  [Rack::MockRequest.env_for('/hello?filter[id]=1,2'), 200]
19
19
  ]
20
20
 
21
- apps = Dir['./apps/*.ru'].each_with_object({}) do |config, hash|
21
+ glob = ARGV[0] || './apps/*.ru'
22
+ apps = Dir[glob].each_with_object({}) do |config, hash|
22
23
  hash[config] = Rack::Builder.parse_file(config).first
23
24
  end
24
25
  apps.freeze
data/examples/app.rb CHANGED
@@ -1,22 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'openapi_first'
4
+ require 'rack'
4
5
 
5
- module Web
6
- module Things
7
- class Index
8
- def call(_params, _response)
9
- { hello: 'world' }
10
- end
11
- end
12
- end
13
- end
6
+ # This example is a bit contrived, but it shows what you could do with the middlewares
7
+
8
+ App = Rack::Builder.new do
9
+ use OpenapiFirst::RequestValidation, raise_error: true, spec: File.expand_path('./openapi.yaml', __dir__)
10
+ use OpenapiFirst::ResponseValidation
14
11
 
15
- oas_path = File.absolute_path('./openapi.yaml', __dir__)
12
+ handlers = {
13
+ 'things#index' => ->(_env) { [200, { 'Content-Type' => 'application/json' }, ['{"hello": "world"}']] }
14
+ }
15
+ not_found = ->(_env) { [404, {}, []] }
16
16
 
17
- App = OpenapiFirst.app(
18
- oas_path,
19
- namespace: Web,
20
- router_raise_error: OpenapiFirst.env == 'test',
21
- response_validation: OpenapiFirst.env == 'test'
22
- )
17
+ run ->(env) { handlers.fetch(env[OpenapiFirst::OPERATION].operation_id, not_found).call(env) }
18
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'multi_json'
4
+
5
+ module OpenapiFirst
6
+ class BodyParserMiddleware
7
+ def initialize(app, options = {})
8
+ @app = app
9
+ @raise = options.fetch(:raise_error, false)
10
+ end
11
+
12
+ RACK_INPUT = 'rack.input'
13
+ ROUTER_PARSED_BODY = 'router.parsed_body'
14
+
15
+ def call(env)
16
+ env[ROUTER_PARSED_BODY] = parse_body(env)
17
+ @app.call(env)
18
+ rescue BodyParsingError => e
19
+ raise if @raise
20
+
21
+ err = { title: "Failed to parse body as #{env['CONTENT_TYPE']}", status: '400' }
22
+ err[:detail] = e.cause unless ENV['RACK_ENV'] == 'production'
23
+ errors = [err]
24
+
25
+ Rack::Response.new(
26
+ MultiJson.dump(errors: errors),
27
+ 400,
28
+ Rack::CONTENT_TYPE => 'application/vnd.api+json'
29
+ ).finish
30
+ end
31
+
32
+ private
33
+
34
+ def parse_body(env)
35
+ request = Rack::Request.new(env)
36
+ body = read_body(request)
37
+ return if body.empty?
38
+
39
+ return MultiJson.load(body) if request.media_type =~ (/json/i) && (request.media_type =~ /json/i)
40
+ return request.POST if request.form_data?
41
+
42
+ body
43
+ rescue MultiJson::ParseError => e
44
+ raise BodyParsingError, e
45
+ end
46
+
47
+ def read_body(request)
48
+ body = request.body.read
49
+ request.body.rewind
50
+ body
51
+ end
52
+ end
53
+ end
@@ -22,6 +22,8 @@ module OpenapiFirst
22
22
 
23
23
  class ResponseBodyInvalidError < ResponseInvalid; end
24
24
 
25
+ class BodyParsingError < Error; end
26
+
25
27
  class RequestInvalidError < Error
26
28
  def initialize(serialized_errors)
27
29
  message = error_message(serialized_errors)
@@ -4,10 +4,9 @@ require 'forwardable'
4
4
  require 'set'
5
5
  require_relative 'schema_validation'
6
6
  require_relative 'utils'
7
- require_relative 'response_object'
8
7
 
9
8
  module OpenapiFirst
10
- class Operation # rubocop:disable Metrics/ClassLength
9
+ class Operation
11
10
  extend Forwardable
12
11
  def_delegators :operation_object,
13
12
  :[],
@@ -40,17 +39,6 @@ module OpenapiFirst
40
39
  operation_object['requestBody']
41
40
  end
42
41
 
43
- def parameters_schema
44
- @parameters_schema ||= begin
45
- parameters_json_schema = build_parameters_json_schema
46
- parameters_json_schema && SchemaValidation.new(parameters_json_schema)
47
- end
48
- end
49
-
50
- def content_types_for(status)
51
- response_for(status)['content']&.keys
52
- end
53
-
54
42
  def response_schema_for(status, content_type)
55
43
  content = response_for(status)['content']
56
44
  return if content.nil? || content.empty?
@@ -88,6 +76,30 @@ module OpenapiFirst
88
76
  "#{method.upcase} #{path} (#{operation_id})"
89
77
  end
90
78
 
79
+ def valid_request_content_type?(request_content_type)
80
+ content = operation_object.dig('requestBody', 'content')
81
+ return unless content
82
+
83
+ !!find_content_for_content_type(content, request_content_type)
84
+ end
85
+
86
+ def query_parameters
87
+ @query_parameters ||= all_parameters.filter { |p| p['in'] == 'query' }
88
+ end
89
+
90
+ def path_parameters
91
+ @path_parameters ||= all_parameters.filter { |p| p['in'] == 'path' }
92
+ end
93
+
94
+ def all_parameters
95
+ @all_parameters ||= begin
96
+ parameters = @path_item_object['parameters']&.dup || []
97
+ parameters_on_operation = operation_object['parameters']
98
+ parameters.concat(parameters_on_operation) if parameters_on_operation
99
+ parameters
100
+ end
101
+ end
102
+
91
103
  private
92
104
 
93
105
  def response_by_code(status)
@@ -107,45 +119,5 @@ module OpenapiFirst
107
119
  content[type] || content["#{type.split('/')[0]}/*"] || content['*/*']
108
120
  end
109
121
  end
110
-
111
- def build_parameters_json_schema
112
- parameters = all_parameters
113
- return unless parameters&.any?
114
-
115
- parameters.each_with_object(new_node) do |parameter, schema|
116
- params = Rack::Utils.parse_nested_query(parameter['name'])
117
- generate_schema(schema, params, parameter)
118
- end
119
- end
120
-
121
- def all_parameters
122
- parameters = @path_item_object['parameters']&.dup || []
123
- parameters_on_operation = operation_object['parameters']
124
- parameters.concat(parameters_on_operation) if parameters_on_operation
125
- parameters
126
- end
127
-
128
- def generate_schema(schema, params, parameter)
129
- required = Set.new(schema['required'])
130
- params.each do |key, value|
131
- required << key if parameter['required']
132
- if value.is_a? Hash
133
- property_schema = new_node
134
- generate_schema(property_schema, value, parameter)
135
- Utils.deep_merge!(schema['properties'], { key => property_schema })
136
- else
137
- schema['properties'][key] = parameter['schema']
138
- end
139
- end
140
- schema['required'] = required.to_a
141
- end
142
-
143
- def new_node
144
- {
145
- 'type' => 'object',
146
- 'required' => [],
147
- 'properties' => {}
148
- }
149
- end
150
122
  end
151
123
  end
@@ -2,12 +2,12 @@
2
2
 
3
3
  require 'rack'
4
4
  require 'multi_json'
5
- require_relative 'inbox'
6
5
  require_relative 'use_router'
7
6
  require_relative 'validation_format'
7
+ require 'openapi_parameters'
8
8
 
9
9
  module OpenapiFirst
10
- class RequestValidation # rubocop:disable Metrics/ClassLength
10
+ class RequestValidation
11
11
  prepend UseRouter
12
12
 
13
13
  def initialize(app, options = {})
@@ -19,58 +19,49 @@ module OpenapiFirst
19
19
  operation = env[OPERATION]
20
20
  return @app.call(env) unless operation
21
21
 
22
- env[INBOX] = {}
23
- catch(:halt) do
24
- validate_query_parameters!(env, operation, env[PARAMETERS])
25
- req = Rack::Request.new(env)
26
- content_type = req.content_type
22
+ error = catch(:error) do
23
+ query_params = OpenapiParameters::Query.new(operation.query_parameters).unpack(env['QUERY_STRING'])
24
+ validate_query_parameters!(operation, query_params)
25
+ env[PARAMS].merge!(query_params)
26
+
27
27
  return @app.call(env) unless operation.request_body
28
28
 
29
- validate_request_content_type!(content_type, operation)
30
- body = req.body.read
31
- req.body.rewind
32
- parse_and_validate_request_body!(env, content_type, body, operation)
33
- @app.call(env)
29
+ content_type = Rack::Request.new(env).content_type
30
+ validate_request_content_type!(operation, content_type)
31
+ parsed_request_body = env[REQUEST_BODY]
32
+ validate_request_body!(operation, parsed_request_body, content_type)
33
+ nil
34
+ end
35
+ if error
36
+ raise RequestInvalidError, error[:errors] if @raise
37
+
38
+ return validation_error_response(error[:status], error[:errors])
34
39
  end
40
+ @app.call(env)
35
41
  end
36
42
 
37
43
  private
38
44
 
39
- def halt(response)
40
- throw :halt, response
41
- end
42
-
43
- def parse_and_validate_request_body!(env, content_type, body, operation)
45
+ def validate_request_body!(operation, body, content_type)
44
46
  validate_request_body_presence!(body, operation)
45
- return if body.empty?
47
+ return if content_type.nil?
46
48
 
47
49
  schema = operation&.request_body_schema(content_type)
48
50
  return unless schema
49
51
 
50
- parsed_request_body = parse_request_body!(body)
51
- errors = schema.validate(parsed_request_body)
52
- halt_with_error(400, serialize_request_body_errors(errors)) if errors.any?
53
- env[INBOX].merge! env[REQUEST_BODY] = Utils.deep_symbolize(parsed_request_body)
54
- end
55
-
56
- def parse_request_body!(body)
57
- MultiJson.load(body)
58
- rescue MultiJson::ParseError => e
59
- err = { title: 'Failed to parse body as JSON' }
60
- err[:detail] = e.cause unless ENV['RACK_ENV'] == 'production'
61
- halt_with_error(400, [err])
52
+ errors = schema.validate(body)
53
+ throw_error(400, serialize_request_body_errors(errors)) if errors.any?
54
+ body
62
55
  end
63
56
 
64
- def validate_request_content_type!(content_type, operation)
65
- return if operation.request_body.dig('content', content_type)
66
-
67
- halt_with_error(415)
57
+ def validate_request_content_type!(operation, content_type)
58
+ operation.valid_request_content_type?(content_type) || throw_error(415)
68
59
  end
69
60
 
70
61
  def validate_request_body_presence!(body, operation)
71
- return unless operation.request_body['required'] && body.empty?
62
+ return unless operation.request_body['required'] && body.nil?
72
63
 
73
- halt_with_error(415, 'Request body is required')
64
+ throw_error(415, 'Request body is required')
74
65
  end
75
66
 
76
67
  def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
@@ -80,10 +71,15 @@ module OpenapiFirst
80
71
  }
81
72
  end
82
73
 
83
- def halt_with_error(status, errors = [default_error(status)])
84
- raise RequestInvalidError, errors if @raise
74
+ def throw_error(status, errors = [default_error(status)])
75
+ throw :error, {
76
+ status: status,
77
+ errors: errors
78
+ }
79
+ end
85
80
 
86
- halt Rack::Response.new(
81
+ def validation_error_response(status, errors)
82
+ Rack::Response.new(
87
83
  MultiJson.dump(errors: errors),
88
84
  status,
89
85
  Rack::CONTENT_TYPE => 'application/vnd.api+json'
@@ -100,32 +96,29 @@ module OpenapiFirst
100
96
  end
101
97
  end
102
98
 
103
- def validate_query_parameters!(env, operation, params)
104
- schema = operation.parameters_schema
105
- return unless schema
106
-
107
- params = filtered_params(schema.raw_schema, params)
108
- params = Utils.deep_stringify(params)
109
- errors = schema.validate(params)
110
- halt_with_error(400, serialize_query_parameter_errors(errors)) if errors.any?
111
- params = Utils.deep_symbolize(params)
112
- env[PARAMETERS] = params
113
- env[INBOX].merge! params
99
+ def build_json_schema(parameter_defs)
100
+ init_schema = {
101
+ 'type' => 'object',
102
+ 'properties' => {},
103
+ 'required' => []
104
+ }
105
+ parameter_defs.each_with_object(init_schema) do |parameter_def, schema|
106
+ parameter = OpenapiParameters::Parameter.new(parameter_def)
107
+ schema['properties'][parameter.name] = parameter.schema if parameter.schema
108
+ schema['required'] << parameter.name if parameter.required?
109
+ end
114
110
  end
115
111
 
116
- def filtered_params(json_schema, params)
117
- json_schema['properties']
118
- .each_with_object({}) do |key_value, result|
119
- parameter_name = key_value[0].to_sym
120
- schema = key_value[1]
121
- next unless params.key?(parameter_name)
112
+ def validate_query_parameters!(operation, params)
113
+ parameter_defs = operation.query_parameters
114
+ return unless parameter_defs&.any?
122
115
 
123
- value = params[parameter_name]
124
- result[parameter_name] = parse_parameter(value, schema)
125
- end
116
+ json_schema = build_json_schema(parameter_defs)
117
+ errors = SchemaValidation.new(json_schema).validate(params)
118
+ throw_error(400, serialize_parameter_errors(errors)) if errors.any?
126
119
  end
127
120
 
128
- def serialize_query_parameter_errors(validation_errors)
121
+ def serialize_parameter_errors(validation_errors)
129
122
  validation_errors.map do |error|
130
123
  pointer = error['data_pointer'][1..].to_s
131
124
  {
@@ -133,41 +126,5 @@ module OpenapiFirst
133
126
  }.update(ValidationFormat.error_details(error))
134
127
  end
135
128
  end
136
-
137
- def parse_parameter(value, schema)
138
- return filtered_params(schema, value) if schema['properties']
139
-
140
- return parse_array_parameter(value, schema) if schema['type'] == 'array'
141
-
142
- parse_simple_value(value, schema)
143
- end
144
-
145
- def parse_array_parameter(value, schema)
146
- return value if value.nil? || value.empty?
147
-
148
- array = value.is_a?(Array) ? value : value.split(',')
149
- return array unless schema['items']
150
-
151
- array.map! { |e| parse_simple_value(e, schema['items']) }
152
- end
153
-
154
- def parse_simple_value(value, schema)
155
- return to_boolean(value) if schema['type'] == 'boolean'
156
-
157
- begin
158
- return Integer(value, 10) if schema['type'] == 'integer'
159
- return Float(value) if schema['type'] == 'number'
160
- rescue ArgumentError
161
- value
162
- end
163
- value
164
- end
165
-
166
- def to_boolean(value)
167
- return true if value == 'true'
168
- return false if value == 'false'
169
-
170
- value
171
- end
172
129
  end
173
130
  end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rack'
4
+ require 'multi_json'
4
5
  require 'hanami/router'
6
+ require_relative 'body_parser_middleware'
5
7
 
6
8
  module OpenapiFirst
7
9
  class Router
@@ -10,7 +12,6 @@ module OpenapiFirst
10
12
  options
11
13
  )
12
14
  @app = app
13
- @parent_app = options.fetch(:parent_app, nil)
14
15
  @raise = options.fetch(:raise_error, false)
15
16
  @not_found = options.fetch(:not_found, :halt)
16
17
  spec = options.fetch(:spec)
@@ -26,8 +27,6 @@ module OpenapiFirst
26
27
  env[OPERATION] = nil
27
28
  response = call_router(env)
28
29
  if env[OPERATION].nil?
29
- return @parent_app.call(env) if @parent_app # This should only happen if used via OpenapiFirst.middleware
30
-
31
30
  raise_error(env) if @raise
32
31
 
33
32
  return @app.call(env) if @not_found == :continue
@@ -37,6 +36,10 @@ module OpenapiFirst
37
36
  end
38
37
 
39
38
  ORIGINAL_PATH = 'openapi_first.path_info'
39
+ private_constant :ORIGINAL_PATH
40
+
41
+ ROUTER_PARSED_BODY = 'router.parsed_body'
42
+ private_constant :ROUTER_PARSED_BODY
40
43
 
41
44
  private
42
45
 
@@ -54,26 +57,53 @@ module OpenapiFirst
54
57
  env[ORIGINAL_PATH] = env[Rack::PATH_INFO]
55
58
  env[Rack::PATH_INFO] = Rack::Request.new(env).path
56
59
  @router.call(env)
60
+ rescue BodyParsingError => e
61
+ handle_body_parsing_error(e)
57
62
  ensure
58
63
  env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
59
64
  end
60
65
 
66
+ def handle_body_parsing_error(exception)
67
+ err = { title: 'Failed to parse body as application/json', status: '400' }
68
+ err[:detail] = exception.cause unless ENV['RACK_ENV'] == 'production'
69
+ errors = [err]
70
+ raise RequestInvalidError, errors if @raise
71
+
72
+ Rack::Response.new(
73
+ MultiJson.dump(errors: errors),
74
+ 400,
75
+ Rack::CONTENT_TYPE => 'application/vnd.api+json'
76
+ ).finish
77
+ end
78
+
61
79
  def build_router(operations)
62
- router = Hanami::Router.new
63
- operations.each do |operation|
64
- normalized_path = operation.path.gsub('{', ':').gsub('}', '')
65
- router.public_send(
66
- operation.method,
67
- normalized_path,
68
- to: lambda do |env|
69
- env[OPERATION] = operation
70
- env[PARAMETERS] = env['router.params']
71
- env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH)
72
- @app.call(env)
73
- end
74
- )
80
+ router = Hanami::Router.new.tap do |r|
81
+ operations.each do |operation|
82
+ normalized_path = operation.path.gsub('{', ':').gsub('}', '')
83
+ r.public_send(
84
+ operation.method,
85
+ normalized_path,
86
+ to: build_route(operation)
87
+ )
88
+ end
89
+ end
90
+ raise_error = @raise
91
+ Rack::Builder.app do
92
+ use BodyParserMiddleware, raise_error: raise_error
93
+ run router
94
+ end
95
+ end
96
+
97
+ def build_route(operation)
98
+ lambda do |env|
99
+ env[OPERATION] = operation
100
+ path_info = env.delete(ORIGINAL_PATH)
101
+ env[REQUEST_BODY] = env.delete(ROUTER_PARSED_BODY) if env.key?(ROUTER_PARSED_BODY)
102
+ route_params = Utils::StringKeyedHash.new(env['router.params'])
103
+ env[PARAMS] = OpenapiParameters::Path.new(operation.path_parameters).unpack(route_params)
104
+ env[Rack::PATH_INFO] = path_info
105
+ @app.call(env)
75
106
  end
76
- router
77
107
  end
78
108
  end
79
109
  end
@@ -17,6 +17,7 @@ module OpenapiFirst
17
17
  insert_property_defaults: true,
18
18
  before_property_validation: proc do |data, property, property_schema, parent|
19
19
  convert_nullable(data, property, property_schema, parent)
20
+ binary_format(data, property, property_schema, parent)
20
21
  end
21
22
  )
22
23
  end
@@ -27,6 +28,14 @@ module OpenapiFirst
27
28
 
28
29
  private
29
30
 
31
+ def binary_format(data, property, property_schema, _parent)
32
+ return unless property_schema.is_a?(Hash) && property_schema['format'] == 'binary'
33
+
34
+ property_schema['type'] = 'object'
35
+ property_schema.delete('format')
36
+ data[property].transform_keys!(&:to_s)
37
+ end
38
+
30
39
  def convert_nullable(_data, _property, property_schema, _parent)
31
40
  return unless property_schema.is_a?(Hash) && property_schema['nullable'] && property_schema['type']
32
41
 
@@ -11,9 +11,7 @@ module OpenapiFirst
11
11
  def call(env)
12
12
  return super if env.key?(OPERATION)
13
13
 
14
- @router ||= Router.new(lambda { |e|
15
- super(e)
16
- }, spec: @options.fetch(:spec), raise_error: @options.fetch(:raise_error, false))
14
+ @router ||= Router.new(->(e) { super(e) }, @options)
17
15
  @router.call(env)
18
16
  end
19
17
  end
@@ -18,12 +18,18 @@ module OpenapiFirst
18
18
  Hanami::Utils::String.classify(string)
19
19
  end
20
20
 
21
- def self.deep_symbolize(hash)
22
- Hanami::Utils::Hash.deep_symbolize(hash)
23
- end
21
+ class StringKeyedHash
22
+ def initialize(original)
23
+ @orig = original
24
+ end
25
+
26
+ def key?(key)
27
+ @orig.key?(key.to_sym)
28
+ end
24
29
 
25
- def self.deep_stringify(hash)
26
- Hanami::Utils::Hash.deep_stringify(hash)
30
+ def [](key)
31
+ @orig[key.to_sym]
32
+ end
27
33
  end
28
34
  end
29
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.20.0'
4
+ VERSION = '1.0.0.beta1'
5
5
  end
data/lib/openapi_first.rb CHANGED
@@ -5,19 +5,15 @@ require 'json_refs'
5
5
  require_relative 'openapi_first/definition'
6
6
  require_relative 'openapi_first/version'
7
7
  require_relative 'openapi_first/errors'
8
- require_relative 'openapi_first/inbox'
9
8
  require_relative 'openapi_first/router'
10
9
  require_relative 'openapi_first/request_validation'
11
10
  require_relative 'openapi_first/response_validator'
12
11
  require_relative 'openapi_first/response_validation'
13
- require_relative 'openapi_first/responder'
14
- require_relative 'openapi_first/app'
15
12
 
16
13
  module OpenapiFirst
17
- OPERATION = 'openapi_first.operation'
18
- PARAMETERS = 'openapi_first.parameters'
19
- REQUEST_BODY = 'openapi_first.parsed_request_body'
20
- INBOX = 'openapi_first.inbox'
14
+ OPERATION = 'openapi.operation'
15
+ PARAMS = 'openapi.params'
16
+ REQUEST_BODY = 'openapi.parsed_request_body'
21
17
  HANDLER = 'openapi_first.handler'
22
18
 
23
19
  def self.env
@@ -50,32 +46,4 @@ module OpenapiFirst
50
46
  response_validation: response_validation
51
47
  )
52
48
  end
53
-
54
- def self.middleware(
55
- spec,
56
- namespace:,
57
- router_raise_error: false,
58
- request_validation_raise_error: false,
59
- response_validation: false
60
- )
61
- spec = OpenapiFirst.load(spec) unless spec.is_a?(Definition)
62
- AppWithOptions.new(
63
- spec,
64
- namespace: namespace,
65
- router_raise_error: router_raise_error,
66
- request_validation_raise_error: request_validation_raise_error,
67
- response_validation: response_validation
68
- )
69
- end
70
-
71
- class AppWithOptions
72
- def initialize(spec, options)
73
- @spec = spec
74
- @options = options
75
- end
76
-
77
- def new(app)
78
- App.new(app, @spec, **@options)
79
- end
80
- end
81
49
  end
@@ -32,17 +32,19 @@ Gem::Specification.new do |spec|
32
32
  spec.bindir = 'exe'
33
33
  spec.require_paths = ['lib']
34
34
 
35
- spec.required_ruby_version = '>= 2.6.0'
35
+ spec.required_ruby_version = '>= 3.0.5'
36
36
 
37
37
  spec.add_runtime_dependency 'deep_merge', '>= 1.2.1'
38
- spec.add_runtime_dependency 'hanami-router', '2.0.alpha5'
39
- spec.add_runtime_dependency 'hanami-utils', '2.0.alpha3'
38
+ spec.add_runtime_dependency 'hanami-router', '~> 2.0.0'
39
+ spec.add_runtime_dependency 'hanami-utils', '~> 2.0.0'
40
40
  spec.add_runtime_dependency 'json_refs', '~> 0.1', '>= 0.1.7'
41
41
  spec.add_runtime_dependency 'json_schemer', '~> 0.2.16'
42
42
  spec.add_runtime_dependency 'multi_json', '~> 1.14'
43
- spec.add_runtime_dependency 'rack', '~> 2.2'
43
+ spec.add_runtime_dependency 'mustermann-contrib', '~> 3.0.0'
44
+ spec.add_runtime_dependency 'rack', '>= 2.2', '< 4.0'
44
45
 
45
46
  spec.add_development_dependency 'bundler', '~> 2'
47
+ spec.add_development_dependency 'openapi_parameters', '~> 0.2', '<= 2.0.0'
46
48
  spec.add_development_dependency 'rack-test', '~> 1'
47
49
  spec.add_development_dependency 'rake', '~> 13'
48
50
  spec.add_development_dependency 'rspec', '~> 3'