openapi_first 0.6.4 → 0.6.5

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: 587fc0801a17d65a320a6eb56afbcbda34a3861b7098435a6f1f3bdbd83bee65
4
- data.tar.gz: 78d32f17e40e2a489729989c6008e079d29942ca9a440bf90d5463fc4b0829a7
3
+ metadata.gz: 035e8a92e6ae6b8817bc3660141c33807d2b813ea79ccdaf045056947b37ed2b
4
+ data.tar.gz: 7e776d7b8d66a5c36a2d70fc73043b648c1d8fb034563064b35963e95aa8732d
5
5
  SHA512:
6
- metadata.gz: 45cc9e17bb4876e263ef37bc9f7b0023289da1638b989777dbff09ba1ef88f8521541a4a4a29a81c0e6b7ee46ba4f247a9bdff92d0ca86f767995af37462cffa
7
- data.tar.gz: 397633c21c2c24a86193ee25e7732fe839d74b318a0e76094aad787eb37e9232a4c4495f73a1b006ca42d95d3ddc76eca74b5a9623af9d533070c4b7a08e7f90
6
+ metadata.gz: 8d196918a2091123c0ad50da1a631da01a2b4923117400dedf01047cb12094375595bde9297de6887cd188258f6b2cb1acdea6a5d2b6af2551077a5788fc9ee2
7
+ data.tar.gz: 8289e602befcfa2d7e8aef2adf0c4d3663d624b68d002b2d3fd8ceb9266b20213cfeeca8191f8c84b76ce8d2b3dfe9fefe911d3673ad5b25a3263d5ddd377b5f
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.5
6
+
7
+ - Merge QueryParameterValidation and ReqestBodyValidation middlewares into RequestValidation
8
+ - Rename option to `allow_unknown_query_paramerters`
5
9
 
6
10
  ## 0.6.4
7
11
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (0.6.4)
4
+ openapi_first (0.6.5)
5
5
  json_schemer (~> 0.2)
6
6
  multi_json (~> 1.13)
7
7
  oas_parser (~> 0.19)
@@ -32,7 +32,7 @@ GEM
32
32
  i18n (1.7.0)
33
33
  concurrent-ruby (~> 1.0)
34
34
  jaro_winkler (1.5.3)
35
- json_schemer (0.2.7)
35
+ json_schemer (0.2.8)
36
36
  ecma-re-validator (~> 0.2)
37
37
  hana (~> 1.3)
38
38
  regexp_parser (~> 1.5)
data/README.md CHANGED
@@ -77,8 +77,7 @@ OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
77
77
 
78
78
  OpenapiFirst offers Rack middlewares to auto-implement different aspects for request handling:
79
79
 
80
- - Query parameter validation
81
- - Request body validation
80
+ - Request validation
82
81
  - Mapping request to a function call
83
82
 
84
83
  It starts by adding a router middleware:
@@ -109,40 +108,30 @@ content-type: "application/vnd.api+json"
109
108
  ]
110
109
  }
111
110
  ```
112
-
113
- ## Query parameter validation
111
+ ## Request validation
114
112
 
115
113
  ```ruby
116
- use OpenapiFirst::QueryParameterValidation
114
+ # Add the middleware:
115
+ use OpenapiFirst::RequestValidation
117
116
  ```
118
117
 
119
- By default OpenapiFirst does not allow additional query parameters and will respond with 400 if additional parameters are sent. You can allow additional parameters with `additional_properties: true`:
118
+ ## Query parameter validation
120
119
 
121
- ```ruby
122
- use OpenapiFirst::QueryParameterValidation,
123
- allow_additional_parameters: true
124
- ```
120
+ By default OpenapiFirst does not allow additional query parameters and will respond with 400 if additional parameters are sent. You can allow additional parameters with `allow_allow_unknown_query_parameters: true`:
125
121
 
126
122
  The middleware filteres all top-level query parameters and adds these to the Rack env: `env[OpenapiFirst::QUERY_PARAMS]`.
127
- If you want to forbid nested query parameters you will need to use `additionalProperties: false` in your query parameter json schema.
123
+ If you want to forbid _nested_ query parameters you will need to use [`additionalProperties: false`](https://json-schema.org/understanding-json-schema/reference/object.html#properties) in your query parameter JSON schema.
128
124
 
129
- OpenapiFirst does not support parameters set to `explode: false` and treats nested query parameters (`filter[foo]=bar`) like [`style: deepObject`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#style-values).
125
+ _OpenapiFirst always treats query parameters like [`style: deepObject`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#style-values), **but** it just works with nested objects (`filter[foo][bar]=baz`) (see [this discussion](https://github.com/OAI/OpenAPI-Specification/issues/1706))._
130
126
 
131
- ## Header, Cookie, Path parameter validation
132
-
133
- tbd.
127
+ ### Request body validation
134
128
 
135
- ## Request Body validation
136
-
137
- ```ruby
138
- # Add the middleware:
139
- use OpenapiFirst::RequestBodyValidation
140
- ```
141
-
142
- This will return a `415` if the requests content type does not match or `400` if the request body is invalid.
129
+ The middleware will return a `415` if the requests content type does not match or `400` if the request body is invalid.
143
130
  This will add the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
144
131
 
145
- OpenAPI request (and response) body validation is based on [JSON Schema](http://json-schema.org/).
132
+ ### Header, Cookie, Path parameter validation
133
+
134
+ tbd.
146
135
 
147
136
  ## Mapping the request to a method call
148
137
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- openapi_first (0.6.3)
4
+ openapi_first (0.6.4)
5
5
  json_schemer (~> 0.2)
6
6
  multi_json (~> 1.13)
7
7
  oas_parser (~> 0.19)
@@ -25,7 +25,6 @@ end
25
25
 
26
26
  spec = OpenapiFirst.load(File.absolute_path('./openapi.yaml', __dir__))
27
27
  use OpenapiFirst::Router, spec: spec
28
- use OpenapiFirst::QueryParameterValidation
29
- use OpenapiFirst::RequestBodyValidation
28
+ use OpenapiFirst::RequestValidation
30
29
 
31
30
  run app
@@ -5,8 +5,7 @@ require 'oas_parser'
5
5
  require 'openapi_first/definition'
6
6
  require 'openapi_first/version'
7
7
  require 'openapi_first/router'
8
- require 'openapi_first/query_parameter_validation'
9
- require 'openapi_first/request_body_validation'
8
+ require 'openapi_first/request_validation'
10
9
  require 'openapi_first/response_validator'
11
10
  require 'openapi_first/operation_resolver'
12
11
  require 'openapi_first/app'
@@ -14,8 +14,7 @@ module OpenapiFirst
14
14
  use OpenapiFirst::Router,
15
15
  spec: spec,
16
16
  allow_unknown_operation: allow_unknown_operation
17
- use OpenapiFirst::QueryParameterValidation
18
- use OpenapiFirst::RequestBodyValidation
17
+ use OpenapiFirst::RequestValidation
19
18
  run OpenapiFirst::OperationResolver.new(app, namespace: namespace)
20
19
  end
21
20
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ class QueryParameters
5
+ def initialize(operation:, allow_unknown_parameters: false)
6
+ @operation = operation
7
+ @allow_unknown_parameters = allow_unknown_parameters
8
+ end
9
+
10
+ def to_json_schema
11
+ return unless @operation&.query_parameters&.any?
12
+
13
+ @operation.query_parameters.each_with_object(
14
+ 'type' => 'object',
15
+ 'required' => [],
16
+ 'additionalProperties' => @allow_unknown_parameters,
17
+ 'properties' => {}
18
+ ) do |parameter, schema|
19
+ schema['required'] << parameter.name if parameter.required
20
+ schema['properties'][parameter.name] = parameter.schema
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ require 'json_schemer'
5
+ require 'multi_json'
6
+ require_relative 'query_parameters'
7
+ require_relative 'validation_format'
8
+
9
+ module OpenapiFirst
10
+ class RequestValidation
11
+ def initialize(app, allow_unknown_query_parameters: false)
12
+ @app = app
13
+ @allow_unknown_query_parameters = allow_unknown_query_parameters
14
+ end
15
+
16
+ def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
17
+ operation = env[OpenapiFirst::OPERATION]
18
+ return @app.call(env) unless operation
19
+
20
+ req = Rack::Request.new(env)
21
+ catch(:halt) do
22
+ validate_query_parameters!(env, operation, req.params)
23
+ content_type = req.content_type
24
+ return @app.call(env) unless operation.request_body
25
+
26
+ validate_request_content_type!(content_type, operation)
27
+ body = req.body.read
28
+ req.body.rewind
29
+ parse_and_validate_request_body!(env, content_type, body, operation)
30
+ @app.call(env)
31
+ end
32
+ end
33
+
34
+ def halt(response)
35
+ throw :halt, response
36
+ end
37
+
38
+ def parse_and_validate_request_body!(env, content_type, body, operation)
39
+ validate_request_body_presence!(body, operation)
40
+ return if body.empty?
41
+
42
+ schema = request_body_schema(content_type, operation)
43
+ return unless schema
44
+
45
+ parsed_request_body = MultiJson.load(body)
46
+ errors = validate_json_schema(schema, parsed_request_body)
47
+ if errors.any?
48
+ halt(error_response(400, serialize_request_body_errors(errors)))
49
+ end
50
+ env[OpenapiFirst::REQUEST_BODY] = parsed_request_body
51
+ end
52
+
53
+ def validate_request_content_type!(content_type, operation)
54
+ return if operation.request_body.content[content_type]
55
+
56
+ halt(error_response(415))
57
+ end
58
+
59
+ def validate_request_body_presence!(body, operation)
60
+ return unless operation.request_body.required && body.empty?
61
+
62
+ halt(error_response(415, 'Request body is required'))
63
+ end
64
+
65
+ def validate_json_schema(schema, object)
66
+ JSONSchemer.schema(schema).validate(object)
67
+ end
68
+
69
+ def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
70
+ {
71
+ status: status.to_s,
72
+ title: title
73
+ }
74
+ end
75
+
76
+ def error_response(status, errors = [default_error(status)])
77
+ Rack::Response.new(
78
+ MultiJson.dump(errors: errors),
79
+ status,
80
+ Rack::CONTENT_TYPE => 'application/vnd.api+json'
81
+ ).finish
82
+ end
83
+
84
+ def request_body_schema(content_type, endpoint)
85
+ return unless endpoint
86
+
87
+ endpoint.request_body.content[content_type]&.fetch('schema')
88
+ end
89
+
90
+ def serialize_request_body_errors(validation_errors)
91
+ validation_errors.map do |error|
92
+ {
93
+ source: {
94
+ pointer: error['data_pointer']
95
+ }
96
+ }.update(ValidationFormat.error_details(error))
97
+ end
98
+ end
99
+
100
+ def validate_query_parameters!(env, operation, params)
101
+ json_schema = QueryParameters.new(
102
+ operation: operation,
103
+ allow_unknown_parameters: @allow_unknown_query_parameters
104
+ ).to_json_schema
105
+
106
+ return unless json_schema
107
+
108
+ errors = JSONSchemer.schema(json_schema).validate(params)
109
+ if errors.any?
110
+ halt error_response(400, serialize_query_parameter_errors(errors))
111
+ end
112
+ env[QUERY_PARAMS] = allowed_params(json_schema, params)
113
+ end
114
+
115
+ def allowed_params(json_schema, params)
116
+ json_schema['properties']
117
+ .keys
118
+ .each_with_object({}) do |parameter_name, filtered|
119
+ next unless params.key?(parameter_name)
120
+
121
+ filtered[parameter_name] = params[parameter_name]
122
+ end
123
+ end
124
+
125
+ def serialize_query_parameter_errors(validation_errors)
126
+ validation_errors.map do |error|
127
+ {
128
+ source: {
129
+ parameter: File.basename(error['data_pointer'])
130
+ }
131
+ }.update(ValidationFormat.error_details(error))
132
+ end
133
+ end
134
+ end
135
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.6.4'
4
+ VERSION = '0.6.5'
5
5
  end
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.6.4
4
+ version: 0.6.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-10-18 00:00:00.000000000 Z
11
+ date: 2019-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json_schemer
@@ -161,11 +161,9 @@ files:
161
161
  - lib/openapi_first/app.rb
162
162
  - lib/openapi_first/coverage.rb
163
163
  - lib/openapi_first/definition.rb
164
- - lib/openapi_first/error_response_method.rb
165
164
  - lib/openapi_first/operation_resolver.rb
166
- - lib/openapi_first/query_parameter_schemas.rb
167
- - lib/openapi_first/query_parameter_validation.rb
168
- - lib/openapi_first/request_body_validation.rb
165
+ - lib/openapi_first/query_parameters.rb
166
+ - lib/openapi_first/request_validation.rb
169
167
  - lib/openapi_first/response_validator.rb
170
168
  - lib/openapi_first/router.rb
171
169
  - lib/openapi_first/validation.rb
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OpenapiFirst
4
- module ErrorResponseMethod
5
- def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
6
- {
7
- status: status.to_s,
8
- title: title
9
- }
10
- end
11
-
12
- def error_response(status, errors = [default_error(status)])
13
- Rack::Response.new(
14
- MultiJson.dump(errors: errors),
15
- status,
16
- Rack::CONTENT_TYPE => 'application/vnd.api+json'
17
- ).finish
18
- end
19
- end
20
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OpenapiFirst
4
- class QueryParameterSchemas
5
- def initialize(allow_additional_parameters:)
6
- @additional_properties = allow_additional_parameters
7
- end
8
-
9
- def find(operation)
10
- build_parameter_schema(operation)
11
- end
12
-
13
- private
14
-
15
- def build_parameter_schema(operation)
16
- return unless operation&.query_parameters&.any?
17
-
18
- operation.query_parameters.each_with_object(
19
- 'type' => 'object',
20
- 'required' => [],
21
- 'additionalProperties' => @additional_properties,
22
- 'properties' => {}
23
- ) do |parameter, schema|
24
- schema['required'] << parameter.name if parameter.required
25
- schema['properties'][parameter.name] = parameter.schema
26
- end
27
- end
28
- end
29
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rack'
4
- require 'json_schemer'
5
- require 'multi_json'
6
- require_relative 'validation_format'
7
- require_relative 'error_response_method'
8
- require_relative 'query_parameter_schemas'
9
-
10
- module OpenapiFirst
11
- class QueryParameterValidation
12
- include ErrorResponseMethod
13
-
14
- def initialize(app, allow_additional_parameters: false)
15
- @app = app
16
- @schemas = QueryParameterSchemas.new(
17
- allow_additional_parameters: allow_additional_parameters
18
- )
19
- @additional_properties = allow_additional_parameters
20
- end
21
-
22
- def call(env)
23
- req = Rack::Request.new(env)
24
- operation = env[OpenapiFirst::OPERATION]
25
- schema = operation && @schemas.find(operation)
26
- if schema
27
- params = req.params
28
- errors = schema && JSONSchemer.schema(schema).validate(params)
29
- return error_response(400, serialize_errors(errors)) if errors&.any?
30
-
31
- req.env[QUERY_PARAMS] = allowed_query_parameters(schema, params)
32
- end
33
-
34
- @app.call(env)
35
- end
36
-
37
- def allowed_query_parameters(params_schema, query_params)
38
- params_schema['properties']
39
- .keys
40
- .each_with_object({}) do |parameter_name, filtered|
41
- value = query_params[parameter_name]
42
- filtered[parameter_name] = value if value
43
- end
44
- end
45
-
46
- def serialize_errors(validation_errors)
47
- validation_errors.map do |error|
48
- {
49
- source: {
50
- parameter: File.basename(error['data_pointer'])
51
- }
52
- }.update(ValidationFormat.error_details(error))
53
- end
54
- end
55
- end
56
- end
@@ -1,93 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rack'
4
- require 'json_schemer'
5
- require 'multi_json'
6
- require_relative 'error_response_method'
7
- require_relative 'validation_format'
8
-
9
- module OpenapiFirst
10
- class RequestBodyValidation
11
- include ErrorResponseMethod
12
-
13
- def initialize(app)
14
- @app = app
15
- end
16
-
17
- def call(env) # rubocop:disable Metrics/MethodLength
18
- operation = env[OpenapiFirst::OPERATION]
19
- return @app.call(env) unless operation&.request_body
20
-
21
- req = Rack::Request.new(env)
22
- body = req.body.read
23
- req.body.rewind
24
- content_type = req.content_type
25
- catch(:halt) do
26
- validate_request_content_type!(content_type, operation)
27
- validate_request_body_presence!(env, body, operation)
28
- parse_and_validate_request_body!(env, content_type, body, operation)
29
- @app.call(env)
30
- end
31
- end
32
-
33
- def halt(response)
34
- throw :halt, response
35
- end
36
-
37
- def validate_request_content_type!(content_type, operation)
38
- return if content_type_valid?(content_type, operation)
39
-
40
- halt(error_response(415))
41
- end
42
-
43
- def validate_request_body_presence!(env, body, operation)
44
- return unless body.size.zero?
45
-
46
- if operation.request_body.required
47
- halt(error_response(415, 'Request body is required'))
48
- end
49
- halt(@app.call(env))
50
- end
51
-
52
- def parse_and_validate_request_body!(env, content_type, body, operation)
53
- schema = request_body_schema(content_type, operation)
54
- return unless schema
55
-
56
- parsed_request_body = MultiJson.load(body)
57
- errors = validate_json_schema(schema, parsed_request_body)
58
- halt(error_response(400, serialize_errors(errors))) if errors&.any?
59
- env[OpenapiFirst::REQUEST_BODY] = parsed_request_body
60
- end
61
-
62
- def validate_json_schema(schema, object)
63
- JSONSchemer.schema(schema).validate(object)
64
- end
65
-
66
- def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
67
- {
68
- status: status.to_s,
69
- title: title
70
- }
71
- end
72
-
73
- def content_type_valid?(content_type, endpoint)
74
- endpoint.request_body.content[content_type]
75
- end
76
-
77
- def request_body_schema(content_type, endpoint)
78
- return unless endpoint
79
-
80
- endpoint.request_body.content[content_type]&.fetch('schema')
81
- end
82
-
83
- def serialize_errors(validation_errors)
84
- validation_errors.map do |error|
85
- {
86
- source: {
87
- pointer: error['data_pointer']
88
- }
89
- }.update(ValidationFormat.error_details(error))
90
- end
91
- end
92
- end
93
- end