openapi_first 0.20.0 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 033e7d24a78ecd8cd0c98bad90e49949612c47a2d0b83791cca3ec7149e91ff6
4
- data.tar.gz: f2092ebe5ee49b86b9745c2a1d27d4a71b798ed1d91f1fad6ea8cc9750f3ddff
3
+ metadata.gz: 0b8b03aaa251de1bdb5cbba71de089bf1ffc6b9dbb96512008f88e783c3cea27
4
+ data.tar.gz: 02eb6cec864e9b5ed272d4392b31fd9e006713d3a8b00df9b26e9a52fa68e555
5
5
  SHA512:
6
- metadata.gz: fc80eff5d2f0c30d1df07d9e7d58cec4dfab41b05467e503dfc4200c11670fe336d1f7ac723b770798708006419ddc1da689ce862b12385d673f03f9a4d4aef4
7
- data.tar.gz: dbf86b5ce8a3b23f12af0311a5bad788b0b9841deff4f8170fe7de17acbad230b9a65806abf7eb73958bd123a76820f54b84f92a2c09a2dd54364c47bf5203f9
6
+ metadata.gz: e37e99e982f0ead9d54587683fa74491e69998f520b1cfb4993c9e1ee81273537dfdf1751c77daef856ab3c3051660589d2f8ac6e58508fd11d3ea130734d2a5
7
+ data.tar.gz: 6944ae2444da29928eeb12e70326505aa154f1c36a09f033083098b1e34be766b075257fd2459b94574370fa8cc417ef488f74dafcec6026eaab07a47f471445
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.6
2
+ TargetRubyVersion: 3.0.5
3
3
  NewCops: enable
4
4
  SuggestExtensions: false
5
5
  Style/Documentation:
data/CHANGELOG.md CHANGED
@@ -2,7 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.21.0
6
+
7
+ - Fix: Query parameter validation does not fail if header parameters are defined (Thanks to [JF Lalonde](https://github.com/JF-Lalonde))
8
+ - Update Ruby dependency to >= 3.0.5
9
+ - Handle simple form-data in request bodies (see https://github.com/ahx/openapi_first/issues/149)
10
+ - Update to hanami-router 2.0.0 stable
11
+
5
12
  ## 0.20.0
13
+
6
14
  - You can pass a filepath to `spec:` now so you no longer have to call `OpenapiFirst.load` anymore.
7
15
  - Router is optional now.
8
16
  You no longer have to add `Router` to your middleware stack. You still can add it to customize behaviour by setting options, but you no longer have to add it.
@@ -49,7 +57,7 @@ Yanked. No useful changes.
49
57
 
50
58
  ## 0.14.1
51
59
 
52
- - Bugfix: Don't mix path- and operation-level parameters for request validation
60
+ - Fix: Don't mix path- and operation-level parameters for request validation
53
61
 
54
62
  ## 0.14.0
55
63
 
data/Gemfile.lock CHANGED
@@ -1,10 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (0.20.0)
4
+ openapi_first (0.21.0)
5
5
  deep_merge (>= 1.2.1)
6
- hanami-router (= 2.0.alpha5)
7
- hanami-utils (= 2.0.alpha3)
6
+ hanami-router (~> 2.0.0)
7
+ hanami-utils (~> 2.0.0)
8
8
  json_refs (~> 0.1, >= 0.1.7)
9
9
  json_schemer (~> 0.2.16)
10
10
  multi_json (~> 1.14)
@@ -15,78 +15,84 @@ GEM
15
15
  specs:
16
16
  ast (2.4.2)
17
17
  coderay (1.1.3)
18
- concurrent-ruby (1.1.10)
18
+ concurrent-ruby (1.2.2)
19
19
  deep_merge (1.2.2)
20
20
  diff-lcs (1.5.0)
21
- dry-transformer (0.1.1)
21
+ dry-core (1.0.0)
22
+ concurrent-ruby (~> 1.0)
23
+ zeitwerk (~> 2.6)
24
+ dry-transformer (1.0.1)
25
+ zeitwerk (~> 2.6)
22
26
  ecma-re-validator (0.4.0)
23
27
  regexp_parser (~> 2.2)
24
28
  hana (1.3.7)
25
- hanami-router (2.0.0.alpha5)
26
- mustermann (~> 1.0)
27
- mustermann-contrib (~> 1.0)
29
+ hanami-router (2.0.2)
30
+ mustermann (~> 3.0)
31
+ mustermann-contrib (~> 3.0)
28
32
  rack (~> 2.0)
29
- hanami-utils (2.0.0.alpha3)
33
+ hanami-utils (2.0.3)
30
34
  concurrent-ruby (~> 1.0)
31
- dry-transformer (~> 0.1)
35
+ dry-core (~> 1.0, < 2)
36
+ dry-transformer (~> 1.0, < 2)
32
37
  hansi (0.2.1)
33
- json (2.6.2)
38
+ json (2.6.3)
34
39
  json_refs (0.1.7)
35
40
  hana
36
- json_schemer (0.2.22)
41
+ json_schemer (0.2.24)
37
42
  ecma-re-validator (~> 0.3)
38
43
  hana (~> 1.3)
39
44
  regexp_parser (~> 2.0)
40
45
  uri_template (~> 0.7)
41
46
  method_source (1.0.0)
42
47
  multi_json (1.15.0)
43
- mustermann (1.1.2)
48
+ mustermann (3.0.0)
44
49
  ruby2_keywords (~> 0.0.1)
45
- mustermann-contrib (1.1.2)
50
+ mustermann-contrib (3.0.0)
46
51
  hansi (~> 0.2.0)
47
- mustermann (= 1.1.2)
52
+ mustermann (= 3.0.0)
48
53
  parallel (1.22.1)
49
- parser (3.1.2.1)
54
+ parser (3.2.1.0)
50
55
  ast (~> 2.4.1)
51
- pry (0.14.1)
56
+ pry (0.14.2)
52
57
  coderay (~> 1.1)
53
58
  method_source (~> 1.0)
54
- rack (2.2.4)
59
+ rack (2.2.6.2)
55
60
  rack-test (1.1.0)
56
61
  rack (>= 1.0, < 3)
57
62
  rainbow (3.1.1)
58
63
  rake (13.0.6)
59
- regexp_parser (2.6.0)
64
+ regexp_parser (2.7.0)
60
65
  rexml (3.2.5)
61
66
  rspec (3.12.0)
62
67
  rspec-core (~> 3.12.0)
63
68
  rspec-expectations (~> 3.12.0)
64
69
  rspec-mocks (~> 3.12.0)
65
- rspec-core (3.12.0)
70
+ rspec-core (3.12.1)
66
71
  rspec-support (~> 3.12.0)
67
- rspec-expectations (3.12.0)
72
+ rspec-expectations (3.12.2)
68
73
  diff-lcs (>= 1.2.0, < 2.0)
69
74
  rspec-support (~> 3.12.0)
70
- rspec-mocks (3.12.0)
75
+ rspec-mocks (3.12.3)
71
76
  diff-lcs (>= 1.2.0, < 2.0)
72
77
  rspec-support (~> 3.12.0)
73
78
  rspec-support (3.12.0)
74
- rubocop (1.37.1)
79
+ rubocop (1.45.1)
75
80
  json (~> 2.3)
76
81
  parallel (~> 1.10)
77
- parser (>= 3.1.2.1)
82
+ parser (>= 3.2.0.0)
78
83
  rainbow (>= 2.2.2, < 4.0)
79
84
  regexp_parser (>= 1.8, < 3.0)
80
85
  rexml (>= 3.2.5, < 4.0)
81
- rubocop-ast (>= 1.23.0, < 2.0)
86
+ rubocop-ast (>= 1.24.1, < 2.0)
82
87
  ruby-progressbar (~> 1.7)
83
- unicode-display_width (>= 1.4.0, < 3.0)
84
- rubocop-ast (1.23.0)
85
- parser (>= 3.1.1.0)
88
+ unicode-display_width (>= 2.4.0, < 3.0)
89
+ rubocop-ast (1.26.0)
90
+ parser (>= 3.2.1.0)
86
91
  ruby-progressbar (1.11.0)
87
92
  ruby2_keywords (0.0.5)
88
- unicode-display_width (2.3.0)
93
+ unicode-display_width (2.4.2)
89
94
  uri_template (0.7.0)
95
+ zeitwerk (2.6.7)
90
96
 
91
97
  PLATFORMS
92
98
  arm64-darwin-21
data/README.md CHANGED
@@ -32,7 +32,7 @@ And these Rack apps:
32
32
  This middleware returns a 400 status code with a body that describes the error if the request is not valid.
33
33
 
34
34
  ```ruby
35
- use OpenapiFirst::RequestValidatio, spec: 'openapi.yaml'
35
+ use OpenapiFirst::RequestValidation, spec: 'openapi.yaml'
36
36
  ```
37
37
 
38
38
  ### Options and defaults
@@ -110,7 +110,7 @@ use OpenapiFirst::ResponseValidation, spec: 'openapi.yaml' if ENV['RACK_ENV'] ==
110
110
 
111
111
  ## OpenapiFirst::Router
112
112
 
113
- This middleware first always used automatically, but you can add it to the top of your middleware stack if you want to change configuration.
113
+ This middleware is used automatically, but you can add it to the top of your middleware stack if you want to change configuration.
114
114
 
115
115
  ```ruby
116
116
  use OpenapiFirst::Router, spec: './openapi/openapi.yaml'
@@ -122,8 +122,7 @@ This middleware adds `env[OpenapiFirst::OPERATION]` which holds an Operation obj
122
122
 
123
123
  | Name | Possible values | Description | Default |
124
124
  | :------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
125
- | `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load`
126
- | |
125
+ | `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` | |
127
126
  | `raise_error:` | `false`, `true` | If set to true the middleware raises `OpenapiFirst::NotFoundError` when a path or method was not found in the API description. This is useful during testing to spot an incomplete API description. | `false` (don't raise an exception) |
128
127
  | `not_found:` | `:continue`, `:halt` | If set to `:continue` the middleware will not return 404 (405, 415), but just pass handling the request to the next middleware or application in the Rack stack. If combined with `raise_error: true` `raise_error` gets preference and an exception is raised. | `:halt` (return 4xx response) |
129
128
 
@@ -269,7 +268,7 @@ validator.validate(last_request, last_response)
269
268
  You can filter the URIs that should be handled by passing `only` to `OpenapiFirst.load`:
270
269
 
271
270
  ```ruby
272
- spec = OpenapiFirst.load('./openapi/openapi.yaml', only: '/pets'.method(:==))
271
+ spec = OpenapiFirst.load('./openapi/openapi.yaml', only: { |path| path.starts_with? '/pets' })
273
272
  run OpenapiFirst.app(spec, namespace: Pets)
274
273
  ```
275
274
 
@@ -1,10 +1,10 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- openapi_first (0.20.0)
4
+ openapi_first (0.21.0)
5
5
  deep_merge (>= 1.2.1)
6
- hanami-router (= 2.0.alpha5)
7
- hanami-utils (= 2.0.alpha3)
6
+ hanami-router (~> 2.0.0)
7
+ hanami-utils (~> 2.0.0)
8
8
  json_refs (~> 0.1, >= 0.1.7)
9
9
  json_schemer (~> 0.2.16)
10
10
  multi_json (~> 1.14)
@@ -13,43 +13,40 @@ PATH
13
13
  GEM
14
14
  remote: https://rubygems.org/
15
15
  specs:
16
- activesupport (7.0.2.3)
16
+ activesupport (7.0.4.2)
17
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
18
  i18n (>= 1.6, < 2)
19
19
  minitest (>= 5.1)
20
20
  tzinfo (~> 2.0)
21
- benchmark-ips (2.10.0)
21
+ benchmark-ips (2.11.0)
22
22
  benchmark-memory (0.2.0)
23
23
  memory_profiler (~> 1)
24
24
  builder (3.2.4)
25
- committee (4.4.0)
25
+ committee (5.0.0)
26
26
  json_schema (~> 0.14, >= 0.14.3)
27
- openapi_parser (>= 0.11.1, < 1.0)
27
+ openapi_parser (~> 1.0)
28
28
  rack (>= 1.5)
29
- concurrent-ruby (1.1.10)
29
+ concurrent-ruby (1.2.0)
30
30
  deep_merge (1.2.2)
31
- dry-configurable (0.14.0)
31
+ dry-core (1.0.0)
32
32
  concurrent-ruby (~> 1.0)
33
- dry-core (~> 0.6)
34
- dry-container (0.9.0)
33
+ zeitwerk (~> 2.6)
34
+ dry-inflector (1.0.0)
35
+ dry-logic (1.5.0)
35
36
  concurrent-ruby (~> 1.0)
36
- dry-configurable (~> 0.13, >= 0.13.0)
37
- dry-core (0.7.1)
37
+ dry-core (~> 1.0, < 2)
38
+ zeitwerk (~> 2.6)
39
+ dry-transformer (1.0.1)
40
+ zeitwerk (~> 2.6)
41
+ dry-types (1.7.0)
38
42
  concurrent-ruby (~> 1.0)
39
- dry-inflector (0.2.1)
40
- dry-logic (1.2.0)
41
- concurrent-ruby (~> 1.0)
42
- dry-core (~> 0.5, >= 0.5)
43
- dry-transformer (0.1.1)
44
- dry-types (1.5.1)
45
- concurrent-ruby (~> 1.0)
46
- dry-container (~> 0.3)
47
- dry-core (~> 0.5, >= 0.5)
48
- dry-inflector (~> 0.1, >= 0.1.2)
49
- dry-logic (~> 1.0, >= 1.0.2)
43
+ dry-core (~> 1.0, < 2)
44
+ dry-inflector (~> 1.0, < 2)
45
+ dry-logic (>= 1.4, < 2)
46
+ zeitwerk (~> 2.6)
50
47
  ecma-re-validator (0.4.0)
51
48
  regexp_parser (~> 2.2)
52
- grape (1.6.2)
49
+ grape (1.7.0)
53
50
  activesupport
54
51
  builder
55
52
  dry-types (>= 1.1)
@@ -57,62 +54,64 @@ GEM
57
54
  rack (>= 1.3.0)
58
55
  rack-accept
59
56
  hana (1.3.7)
60
- hanami-api (0.2.0)
61
- hanami-router (~> 2.0.alpha)
62
- hanami-router (2.0.0.alpha5)
63
- mustermann (~> 1.0)
64
- mustermann-contrib (~> 1.0)
57
+ hanami-api (0.3.0)
58
+ hanami-router (~> 2.0)
59
+ hanami-router (2.0.2)
60
+ mustermann (~> 3.0)
61
+ mustermann-contrib (~> 3.0)
65
62
  rack (~> 2.0)
66
- hanami-utils (2.0.0.alpha3)
63
+ hanami-utils (2.0.3)
67
64
  concurrent-ruby (~> 1.0)
68
- dry-transformer (~> 0.1)
69
- hansi (0.2.0)
70
- i18n (1.10.0)
65
+ dry-core (~> 1.0, < 2)
66
+ dry-transformer (~> 1.0, < 2)
67
+ hansi (0.2.1)
68
+ i18n (1.12.0)
71
69
  concurrent-ruby (~> 1.0)
72
70
  json_refs (0.1.7)
73
71
  hana
74
72
  json_schema (0.21.0)
75
- json_schemer (0.2.22)
73
+ json_schemer (0.2.24)
76
74
  ecma-re-validator (~> 0.3)
77
75
  hana (~> 1.3)
78
76
  regexp_parser (~> 2.0)
79
77
  uri_template (~> 0.7)
80
- memory_profiler (1.0.0)
81
- minitest (5.15.0)
78
+ memory_profiler (1.0.1)
79
+ minitest (5.17.0)
82
80
  multi_json (1.15.0)
83
- mustermann (1.1.1)
81
+ mustermann (3.0.0)
84
82
  ruby2_keywords (~> 0.0.1)
85
- mustermann-contrib (1.1.1)
83
+ mustermann-contrib (3.0.0)
86
84
  hansi (~> 0.2.0)
87
- mustermann (= 1.1.1)
88
- mustermann-grape (1.0.1)
85
+ mustermann (= 3.0.0)
86
+ mustermann-grape (1.0.2)
89
87
  mustermann (>= 1.0.0)
90
88
  nio4r (2.5.8)
91
- openapi_parser (0.15.0)
92
- puma (5.6.5)
89
+ openapi_parser (1.0.0)
90
+ puma (6.1.0)
93
91
  nio4r (~> 2.0)
94
- rack (2.2.3.1)
92
+ rack (2.2.6.2)
95
93
  rack-accept (0.4.5)
96
94
  rack (>= 0.4)
97
- rack-protection (2.2.0)
95
+ rack-protection (3.0.5)
98
96
  rack
99
- regexp_parser (2.6.0)
100
- roda (3.54.0)
97
+ regexp_parser (2.7.0)
98
+ roda (3.65.0)
101
99
  rack
102
100
  ruby2_keywords (0.0.5)
103
101
  seg (1.2.0)
104
- sinatra (2.2.0)
105
- mustermann (~> 1.0)
106
- rack (~> 2.2)
107
- rack-protection (= 2.2.0)
102
+ sinatra (3.0.5)
103
+ mustermann (~> 3.0)
104
+ rack (~> 2.2, >= 2.2.4)
105
+ rack-protection (= 3.0.5)
108
106
  tilt (~> 2.0)
109
107
  syro (3.2.1)
110
108
  rack (>= 1.6.0)
111
109
  seg
112
- tilt (2.0.10)
113
- tzinfo (2.0.4)
110
+ tilt (2.0.11)
111
+ tzinfo (2.0.6)
114
112
  concurrent-ruby (~> 1.0)
115
113
  uri_template (0.7.0)
114
+ zeitwerk (2.6.7)
116
115
 
117
116
  PLATFORMS
118
117
  arm64-darwin-21
@@ -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
@@ -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)
@@ -47,6 +47,13 @@ module OpenapiFirst
47
47
  end
48
48
  end
49
49
 
50
+ def query_parameters_schema
51
+ @query_parameters_schema ||= begin
52
+ query_parameters_json_schema = build_query_parameters_json_schema
53
+ query_parameters_json_schema && SchemaValidation.new(query_parameters_json_schema)
54
+ end
55
+ end
56
+
50
57
  def content_types_for(status)
51
58
  response_for(status)['content']&.keys
52
59
  end
@@ -88,6 +95,13 @@ module OpenapiFirst
88
95
  "#{method.upcase} #{path} (#{operation_id})"
89
96
  end
90
97
 
98
+ def valid_request_content_type?(request_content_type)
99
+ content = operation_object.dig('requestBody', 'content')
100
+ return unless content
101
+
102
+ !!find_content_for_content_type(content, request_content_type)
103
+ end
104
+
91
105
  private
92
106
 
93
107
  def response_by_code(status)
@@ -118,6 +132,16 @@ module OpenapiFirst
118
132
  end
119
133
  end
120
134
 
135
+ def build_query_parameters_json_schema
136
+ query_parameters = all_parameters.reject { |field, _value| field['in'] == 'header' }
137
+ return unless query_parameters&.any?
138
+
139
+ query_parameters.each_with_object(new_node) do |parameter, schema|
140
+ params = Rack::Utils.parse_nested_query(parameter['name'])
141
+ generate_schema(schema, params, parameter)
142
+ end
143
+ end
144
+
121
145
  def all_parameters
122
146
  parameters = @path_item_object['parameters']&.dup || []
123
147
  parameters_on_operation = operation_object['parameters']
@@ -20,57 +20,56 @@ module OpenapiFirst
20
20
  return @app.call(env) unless operation
21
21
 
22
22
  env[INBOX] = {}
23
- catch(:halt) do
24
- validate_query_parameters!(env, operation, env[PARAMETERS])
23
+ error = catch(:error) do
24
+ params = validate_query_parameters!(operation, env[PARAMETERS])
25
+ env[INBOX].merge! env[PARAMETERS] = params if params
25
26
  req = Rack::Request.new(env)
26
- content_type = req.content_type
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
+ validate_request_content_type!(operation, req.content_type)
30
+ parsed_request_body = parse_and_validate_request_body!(operation, req)
31
+ env[REQUEST_BODY] = parsed_request_body
32
+ env[INBOX].merge! parsed_request_body if parsed_request_body.is_a?(Hash)
33
+ nil
34
34
  end
35
+ if error
36
+ raise RequestInvalidError, error[:errors] if @raise
37
+
38
+ return validation_error_response(error[:status], error[:errors])
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
45
+ ROUTER_PARSED_BODY = 'router.parsed_body'
46
+
47
+ def parse_and_validate_request_body!(operation, request)
48
+ env = request.env
49
+
50
+ body = env.delete(ROUTER_PARSED_BODY) if env.key?(ROUTER_PARSED_BODY)
42
51
 
43
- def parse_and_validate_request_body!(env, content_type, body, operation)
44
52
  validate_request_body_presence!(body, operation)
45
- return if body.empty?
53
+ return if body.nil?
46
54
 
47
- schema = operation&.request_body_schema(content_type)
55
+ schema = operation&.request_body_schema(request.content_type)
48
56
  return unless schema
49
57
 
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
58
+ errors = schema.validate(body)
59
+ throw_error(400, serialize_request_body_errors(errors)) if errors.any?
60
+ return Utils.deep_symbolize(body) if body.is_a?(Hash)
55
61
 
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])
62
+ body
62
63
  end
63
64
 
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)
65
+ def validate_request_content_type!(operation, content_type)
66
+ operation.valid_request_content_type?(content_type) || throw_error(415)
68
67
  end
69
68
 
70
69
  def validate_request_body_presence!(body, operation)
71
- return unless operation.request_body['required'] && body.empty?
70
+ return unless operation.request_body['required'] && body.nil?
72
71
 
73
- halt_with_error(415, 'Request body is required')
72
+ throw_error(415, 'Request body is required')
74
73
  end
75
74
 
76
75
  def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
@@ -80,10 +79,15 @@ module OpenapiFirst
80
79
  }
81
80
  end
82
81
 
83
- def halt_with_error(status, errors = [default_error(status)])
84
- raise RequestInvalidError, errors if @raise
82
+ def throw_error(status, errors = [default_error(status)])
83
+ throw :error, {
84
+ status: status,
85
+ errors: errors
86
+ }
87
+ end
85
88
 
86
- halt Rack::Response.new(
89
+ def validation_error_response(status, errors)
90
+ Rack::Response.new(
87
91
  MultiJson.dump(errors: errors),
88
92
  status,
89
93
  Rack::CONTENT_TYPE => 'application/vnd.api+json'
@@ -100,17 +104,15 @@ module OpenapiFirst
100
104
  end
101
105
  end
102
106
 
103
- def validate_query_parameters!(env, operation, params)
104
- schema = operation.parameters_schema
107
+ def validate_query_parameters!(operation, params)
108
+ schema = operation.query_parameters_schema
105
109
  return unless schema
106
110
 
107
111
  params = filtered_params(schema.raw_schema, params)
108
112
  params = Utils.deep_stringify(params)
109
113
  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
114
+ throw_error(400, serialize_query_parameter_errors(errors)) if errors.any?
115
+ Utils.deep_symbolize(params)
114
116
  end
115
117
 
116
118
  def filtered_params(json_schema, params)
@@ -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
@@ -54,26 +56,50 @@ module OpenapiFirst
54
56
  env[ORIGINAL_PATH] = env[Rack::PATH_INFO]
55
57
  env[Rack::PATH_INFO] = Rack::Request.new(env).path
56
58
  @router.call(env)
59
+ rescue BodyParsingError => e
60
+ handle_body_parsing_error(e)
57
61
  ensure
58
62
  env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
59
63
  end
60
64
 
65
+ def handle_body_parsing_error(exception)
66
+ err = { title: 'Failed to parse body as application/json', status: '400' }
67
+ err[:detail] = exception.cause unless ENV['RACK_ENV'] == 'production'
68
+ errors = [err]
69
+ raise RequestInvalidError, errors if @raise
70
+
71
+ Rack::Response.new(
72
+ MultiJson.dump(errors: errors),
73
+ 400,
74
+ Rack::CONTENT_TYPE => 'application/vnd.api+json'
75
+ ).finish
76
+ end
77
+
61
78
  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
- )
79
+ router = Hanami::Router.new.tap do |r|
80
+ operations.each do |operation|
81
+ normalized_path = operation.path.gsub('{', ':').gsub('}', '')
82
+ r.public_send(
83
+ operation.method,
84
+ normalized_path,
85
+ to: build_route(operation)
86
+ )
87
+ end
88
+ end
89
+ raise_error = @raise
90
+ Rack::Builder.app do
91
+ use BodyParserMiddleware, raise_error: raise_error
92
+ run router
93
+ end
94
+ end
95
+
96
+ def build_route(operation)
97
+ lambda do |env|
98
+ env[OPERATION] = operation
99
+ env[PARAMETERS] = env['router.params']
100
+ env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH)
101
+ @app.call(env)
75
102
  end
76
- router
77
103
  end
78
104
  end
79
105
  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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.20.0'
4
+ VERSION = '0.21.0'
5
5
  end
@@ -32,11 +32,11 @@ 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'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi_first
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-10-27 00:00:00.000000000 Z
11
+ date: 2023-03-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deep_merge
@@ -28,30 +28,30 @@ dependencies:
28
28
  name: hanami-router
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '='
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 2.0.alpha5
33
+ version: 2.0.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '='
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 2.0.alpha5
40
+ version: 2.0.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: hanami-utils
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - '='
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 2.0.alpha3
47
+ version: 2.0.0
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - '='
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 2.0.alpha3
54
+ version: 2.0.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: json_refs
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -215,6 +215,7 @@ files:
215
215
  - examples/openapi.yaml
216
216
  - lib/openapi_first.rb
217
217
  - lib/openapi_first/app.rb
218
+ - lib/openapi_first/body_parser_middleware.rb
218
219
  - lib/openapi_first/coverage.rb
219
220
  - lib/openapi_first/default_operation_resolver.rb
220
221
  - lib/openapi_first/definition.rb
@@ -248,7 +249,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
248
249
  requirements:
249
250
  - - ">="
250
251
  - !ruby/object:Gem::Version
251
- version: 2.6.0
252
+ version: 3.0.5
252
253
  required_rubygems_version: !ruby/object:Gem::Requirement
253
254
  requirements:
254
255
  - - ">="