openapi_first 0.20.0 → 1.0.0.beta1

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.
@@ -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'