openapi_first 0.20.0 → 0.21.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 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
  - - ">="