openapi_first 0.12.2 → 0.13.2

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: 689e572886b7ca556ffb00c89368195e129d954a03a3899f3d7d8401bd724196
4
- data.tar.gz: e2669f12188188ac232d0c52b8a371e221ce65e589f2bd06dc6b7547fd0cafc2
3
+ metadata.gz: 579d36678fe6b41e050a370017165899c82be5e64b352e45410107e7302d26af
4
+ data.tar.gz: 0ddd3ab2f5ed7bb9aadc41cba158ea3fc9f2e59a93054c4f76f3766109e03192
5
5
  SHA512:
6
- metadata.gz: c3409b992b56a09eb9e4bc75d20e6711a4ebda0bd93c0422316343ea48b4f8816394f1506b675c56e42f3dad339a0b7aa9c4834f0372045815a81888724ad45e
7
- data.tar.gz: 2893535646d84f3ca6db6efc63c5334961929b9f6e561b8d36f771ca4ad0b9e25af5c5b06292ae9745610fa6dd229516c4aa4b42c01c52fc176294a1713b8767
6
+ metadata.gz: fb1ca083cce9a636bf42f33c308dcd5c1e19b14b31bebe0534646baf0af8c43e6ce1d5b1e20b0657872a12c90e2446122a33c015ce42ee96ee16c8b1f4cc8b79
7
+ data.tar.gz: 8a2f1af143f23f21381691d158d0991b1e055d726c8e0c46579bd1d88383f3c4f66ca3a8b1554a47bdab5e6278340b6e6e8ace1d983e95b01b84c1f822a5249b
@@ -14,6 +14,8 @@ Layout/SpaceAroundMethodCallOperator:
14
14
  Enabled: true
15
15
  Lint/DeprecatedOpenSSLConstant:
16
16
  Enabled: true
17
+ Lint/DuplicateElsifCondition:
18
+ Enabled: true
17
19
  Lint/RaiseException:
18
20
  Enabled: true
19
21
  Lint/MixedRegexpCaptureTypes:
@@ -28,7 +30,25 @@ Lint/StructNewOverride:
28
30
  Enabled: true
29
31
  Style/HashEachMethods:
30
32
  Enabled: false
33
+ Style/AccessorGrouping:
34
+ Enabled: true
35
+ Style/ArrayCoercion:
36
+ Enabled: true
37
+ Style/BisectedAttrAccessor:
38
+ Enabled: true
39
+ Style/CaseLikeIf:
40
+ Enabled: true
41
+ Style/HashAsLastArrayItem:
42
+ Enabled: true
43
+ Style/HashLikeCase:
44
+ Enabled: true
31
45
  Style/HashTransformKeys:
32
46
  Enabled: true
33
47
  Style/HashTransformValues:
34
48
  Enabled: true
49
+ Style/RedundantAssignment:
50
+ Enabled: true
51
+ Style/RedundantFetchBlock:
52
+ Enabled: true
53
+ Style/RedundantFileExtensionInRequire:
54
+ Enabled: true
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.2
4
+ - Return indicator (`source: { parameter: 'list/1' }`) in error response body when array item in query parameter is invalid
5
+
6
+ ## 0.13.0
7
+ - Add support for arrays in query parameters (style: form, explode: false)
8
+ - Remove warning when handler is not implemented
9
+
10
+ ## 0.12.5
11
+ - Add `not_found: :continue` option to Router to make it do nothing if request is unknown
12
+
13
+ ## 0.12.4
14
+ - content-type is found while ignoring additional content-type parameters (`application/json` is found when request/response content-type is `application/json; charset=UTF8`)
15
+ - Support wildcard mime-types when finding the content-type
16
+
17
+ ## 0.12.3
18
+ - Add `response_validation:`, `router_raise_error` options to standalone mode.
19
+
3
20
  ## 0.12.2
4
21
  - Allow response to have no media type object specified
5
22
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (0.12.2)
4
+ openapi_first (0.13.2)
5
5
  deep_merge (>= 1.2.1)
6
6
  hanami-router (~> 2.0.alpha3)
7
7
  hanami-utils (~> 2.0.alpha1)
@@ -24,9 +24,9 @@ GEM
24
24
  ast (2.4.1)
25
25
  builder (3.2.4)
26
26
  coderay (1.1.3)
27
- concurrent-ruby (1.1.6)
27
+ concurrent-ruby (1.1.7)
28
28
  deep_merge (1.2.1)
29
- diff-lcs (1.4.2)
29
+ diff-lcs (1.4.4)
30
30
  ecma-re-validator (0.2.1)
31
31
  regexp_parser (~> 1.2)
32
32
  hana (1.3.6)
@@ -39,9 +39,9 @@ GEM
39
39
  transproc (~> 1.0)
40
40
  hansi (0.2.0)
41
41
  hash-deep-merge (0.1.1)
42
- i18n (1.8.3)
42
+ i18n (1.8.5)
43
43
  concurrent-ruby (~> 1.0)
44
- json_schemer (0.2.11)
44
+ json_schemer (0.2.13)
45
45
  ecma-re-validator (~> 0.2)
46
46
  hana (~> 1.3)
47
47
  regexp_parser (~> 1.5)
@@ -49,15 +49,15 @@ GEM
49
49
  method_source (1.0.0)
50
50
  mini_portile2 (2.4.0)
51
51
  minitest (5.14.1)
52
- multi_json (1.14.1)
52
+ multi_json (1.15.0)
53
53
  mustermann (1.1.1)
54
54
  ruby2_keywords (~> 0.0.1)
55
55
  mustermann-contrib (1.1.1)
56
56
  hansi (~> 0.2.0)
57
57
  mustermann (= 1.1.1)
58
- nokogiri (1.10.9)
58
+ nokogiri (1.10.10)
59
59
  mini_portile2 (~> 2.4.0)
60
- oas_parser (0.25.2)
60
+ oas_parser (0.25.1)
61
61
  activesupport (>= 4.0.0)
62
62
  addressable (~> 2.3)
63
63
  builder (~> 3.2.3)
@@ -92,16 +92,16 @@ GEM
92
92
  diff-lcs (>= 1.2.0, < 2.0)
93
93
  rspec-support (~> 3.9.0)
94
94
  rspec-support (3.9.3)
95
- rubocop (0.86.0)
95
+ rubocop (0.88.0)
96
96
  parallel (~> 1.10)
97
- parser (>= 2.7.0.1)
97
+ parser (>= 2.7.1.1)
98
98
  rainbow (>= 2.2.2, < 4.0)
99
99
  regexp_parser (>= 1.7)
100
100
  rexml
101
- rubocop-ast (>= 0.0.3, < 1.0)
101
+ rubocop-ast (>= 0.1.0, < 1.0)
102
102
  ruby-progressbar (~> 1.7)
103
103
  unicode-display_width (>= 1.4.0, < 2.0)
104
- rubocop-ast (0.0.3)
104
+ rubocop-ast (0.2.0)
105
105
  parser (>= 2.7.0.1)
106
106
  ruby-progressbar (1.10.1)
107
107
  ruby2_keywords (0.0.2)
@@ -111,7 +111,7 @@ GEM
111
111
  thread_safe (~> 0.1)
112
112
  unicode-display_width (1.7.0)
113
113
  uri_template (0.7.0)
114
- zeitwerk (2.3.0)
114
+ zeitwerk (2.4.0)
115
115
 
116
116
  PLATFORMS
117
117
  ruby
data/README.md CHANGED
@@ -37,6 +37,7 @@ Options and their defaults:
37
37
  |:---|---|---|---|
38
38
  |`spec:`| | The spec loaded via `OpenapiFirst.load` ||
39
39
  | `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)
40
+ | `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)
40
41
 
41
42
  ## OpenapiFirst::RequestValidation
42
43
 
@@ -78,7 +79,12 @@ This middleware adds `env[OpenapiFirst::INBOX]` which holds the (filtered) path
78
79
  ### Parameter validation
79
80
 
80
81
  The middleware filteres all top-level query parameters and paths parameters and tries to convert numeric values. Meaning, if you have an `:something_id` path with `type: integer`, it will try convert the value to an integer.
81
- Note that is currently does not convert date, date-time or time formats and that conversion is currently done only for path and query parameters, but not for the request body. It just works with a parameter with `name: filter[age]`.
82
+
83
+ It just works with a parameter with `name: filter[age]`.
84
+
85
+ OpenapiFirst also supports `type: array` for query parameters and will convert `items` just as described above. [`style`](http://spec.openapis.org/oas/v3.0.3#style-values) and `explode` attributes are not supported for query parameters. It will always act as if `style: form` and `explode: false` were used for query parameters.
86
+
87
+ Conversion is currently done only for path and query parameters, but not for the request body. OpenapiFirst currently does not convert date, date-time or time formats.
82
88
 
83
89
  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.
84
90
 
@@ -150,7 +156,12 @@ end
150
156
 
151
157
  # In config.ru:
152
158
  require 'openapi_first'
153
- run OpenapiFirst.app('./openapi/openapi.yaml', namespace: Pets)
159
+ run OpenapiFirst.app(
160
+ './openapi/openapi.yaml',
161
+ namespace: Pets,
162
+ response_validation: ENV['RACK_ENV'] == 'test',
163
+ router_raise_error: ENV['RACK_ENV'] == 'test'
164
+ )
154
165
  ```
155
166
 
156
167
  The above will use the mentioned Rack middlewares to:
@@ -159,6 +170,17 @@ The above will use the mentioned Rack middlewares to:
159
170
  - Map the request to a method call `Pets.find_pet` based on the `operationId` in the API description
160
171
  - Set the response content type according to your spec (here with the default status code `200`)
161
172
 
173
+ ### Options and their defaults:
174
+
175
+ | Name | Possible values | Description | Default
176
+ |:---|---|---|---|
177
+ | `spec_path` || A filepath to an OpenAPI definition file. |
178
+ | `namespace:` || A class or module where to find the handler methods.|
179
+ | `response_validation:` | `true`, `false` | If set to true it raises an exception if the response is invalid. This is useful during testing. | `false`
180
+ | `router_raise_error:` | `true`, `false` | If set to true it raises an exception (subclass of `OpenapiFirst::Error` when a request path/method is not specified. This is useful during testing. | `false`
181
+ | `request_validation_raise_error:` | `true`, `false` | If set to true it raises an exception (subclass of `OpenapiFirst::Error` when a request is not valid. | `false`
182
+
183
+
162
184
  Handler functions (`find_pet`) are called with two arguments:
163
185
 
164
186
  - `params` - Holds the parsed request body, filtered query params and path parameters
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- openapi_first (0.12.2)
4
+ openapi_first (0.13.2)
5
5
  deep_merge (>= 1.2.1)
6
6
  hanami-router (~> 2.0.alpha3)
7
7
  hanami-utils (~> 2.0.alpha1)
@@ -25,11 +25,11 @@ GEM
25
25
  benchmark-memory (0.1.2)
26
26
  memory_profiler (~> 0.9)
27
27
  builder (3.2.4)
28
- committee (4.0.0)
28
+ committee (4.2.0)
29
29
  json_schema (~> 0.14, >= 0.14.3)
30
30
  openapi_parser (>= 0.11.1)
31
31
  rack (>= 1.5)
32
- concurrent-ruby (1.1.6)
32
+ concurrent-ruby (1.1.7)
33
33
  deep_merge (1.2.1)
34
34
  dry-configurable (0.11.6)
35
35
  concurrent-ruby (~> 1.0)
@@ -42,7 +42,7 @@ GEM
42
42
  concurrent-ruby (~> 1.0)
43
43
  dry-equalizer (0.3.0)
44
44
  dry-inflector (0.2.0)
45
- dry-logic (1.0.6)
45
+ dry-logic (1.0.7)
46
46
  concurrent-ruby (~> 1.0)
47
47
  dry-core (~> 0.2)
48
48
  dry-equalizer (~> 0.2)
@@ -55,7 +55,7 @@ GEM
55
55
  dry-logic (~> 1.0, >= 1.0.2)
56
56
  ecma-re-validator (0.2.1)
57
57
  regexp_parser (~> 1.2)
58
- grape (1.3.3)
58
+ grape (1.4.0)
59
59
  activesupport
60
60
  builder
61
61
  dry-types (>= 1.1)
@@ -72,10 +72,10 @@ GEM
72
72
  transproc (~> 1.0)
73
73
  hansi (0.2.0)
74
74
  hash-deep-merge (0.1.1)
75
- i18n (1.8.3)
75
+ i18n (1.8.5)
76
76
  concurrent-ruby (~> 1.0)
77
77
  json_schema (0.20.9)
78
- json_schemer (0.2.11)
78
+ json_schemer (0.2.13)
79
79
  ecma-re-validator (~> 0.2)
80
80
  hana (~> 1.3)
81
81
  regexp_parser (~> 1.5)
@@ -83,7 +83,7 @@ GEM
83
83
  memory_profiler (0.9.14)
84
84
  mini_portile2 (2.4.0)
85
85
  minitest (5.14.1)
86
- multi_json (1.14.1)
86
+ multi_json (1.15.0)
87
87
  mustermann (1.1.1)
88
88
  ruby2_keywords (~> 0.0.1)
89
89
  mustermann-contrib (1.1.1)
@@ -91,9 +91,9 @@ GEM
91
91
  mustermann (= 1.1.1)
92
92
  mustermann-grape (1.0.1)
93
93
  mustermann (>= 1.0.0)
94
- nokogiri (1.10.9)
94
+ nokogiri (1.10.10)
95
95
  mini_portile2 (~> 2.4.0)
96
- oas_parser (0.25.2)
96
+ oas_parser (0.25.1)
97
97
  activesupport (>= 4.0.0)
98
98
  addressable (~> 2.3)
99
99
  builder (~> 3.2.3)
@@ -101,7 +101,7 @@ GEM
101
101
  hash-deep-merge
102
102
  mustermann-contrib (~> 1.1.1)
103
103
  nokogiri
104
- openapi_parser (0.11.2)
104
+ openapi_parser (0.12.1)
105
105
  public_suffix (4.0.5)
106
106
  rack (2.2.3)
107
107
  rack-accept (0.4.5)
@@ -125,7 +125,7 @@ GEM
125
125
  tzinfo (1.2.7)
126
126
  thread_safe (~> 0.1)
127
127
  uri_template (0.7.0)
128
- zeitwerk (2.3.0)
128
+ zeitwerk (2.4.0)
129
129
 
130
130
  PLATFORMS
131
131
  ruby
@@ -17,5 +17,6 @@ oas_path = File.absolute_path('./openapi.yaml', __dir__)
17
17
  App = OpenapiFirst.app(
18
18
  oas_path,
19
19
  namespace: Web,
20
- raise_error: OpenapiFirst.env == 'test'
20
+ router_raise_error: OpenapiFirst.env == 'test',
21
+ response_validation: OpenapiFirst.env == 'test'
21
22
  )
@@ -31,14 +31,39 @@ module OpenapiFirst
31
31
  Definition.new(parsed)
32
32
  end
33
33
 
34
- def self.app(spec, namespace:, raise_error: false)
34
+ def self.app(
35
+ spec,
36
+ namespace:,
37
+ router_raise_error: false,
38
+ request_validation_raise_error: false,
39
+ response_validation: false
40
+ )
35
41
  spec = OpenapiFirst.load(spec) if spec.is_a?(String)
36
- App.new(nil, spec, namespace: namespace, raise_error: raise_error)
42
+ App.new(
43
+ nil,
44
+ spec,
45
+ namespace: namespace,
46
+ router_raise_error: router_raise_error,
47
+ request_validation_raise_error: request_validation_raise_error,
48
+ response_validation: response_validation
49
+ )
37
50
  end
38
51
 
39
- def self.middleware(spec, namespace:, raise_error: false)
52
+ def self.middleware(
53
+ spec,
54
+ namespace:,
55
+ router_raise_error: false,
56
+ request_validation_raise_error: false,
57
+ response_validation: false
58
+ )
40
59
  spec = OpenapiFirst.load(spec) if spec.is_a?(String)
41
- AppWithOptions.new(spec, namespace: namespace, raise_error: raise_error)
60
+ AppWithOptions.new(
61
+ spec,
62
+ namespace: namespace,
63
+ router_raise_error: router_raise_error,
64
+ request_validation_raise_error: request_validation_raise_error,
65
+ response_validation: response_validation
66
+ )
42
67
  end
43
68
 
44
69
  class AppWithOptions
@@ -5,12 +5,19 @@ require 'logger'
5
5
 
6
6
  module OpenapiFirst
7
7
  class App
8
- def initialize(parent_app, spec, namespace:, raise_error:)
8
+ def initialize( # rubocop:disable Metrics/ParameterLists
9
+ parent_app,
10
+ spec,
11
+ namespace:,
12
+ router_raise_error: false,
13
+ request_validation_raise_error: false,
14
+ response_validation: false
15
+ )
9
16
  @stack = Rack::Builder.app do
10
17
  freeze_app
11
- use OpenapiFirst::Router, spec: spec, raise_error: raise_error, parent_app: parent_app
12
- use OpenapiFirst::RequestValidation, raise_error: raise_error
13
- use OpenapiFirst::ResponseValidation if raise_error
18
+ use OpenapiFirst::Router, spec: spec, raise_error: router_raise_error, parent_app: parent_app
19
+ use OpenapiFirst::RequestValidation, raise_error: request_validation_raise_error
20
+ use OpenapiFirst::ResponseValidation if response_validation
14
21
  run OpenapiFirst::Responder.new(
15
22
  spec: spec,
16
23
  namespace: namespace
@@ -9,10 +9,8 @@ module OpenapiFirst
9
9
  @handlers = spec.operations.each_with_object({}) do |operation, hash|
10
10
  operation_id = operation.operation_id
11
11
  handler = find_handler(operation_id)
12
- if handler.nil?
13
- warn "#{self.class.name} cannot not find handler for '#{operation.operation_id}' (#{operation.method} #{operation.path}). This operation will be ignored." # rubocop:disable Layout/LineLength
14
- next
15
- end
12
+ next if handler.nil?
13
+
16
14
  hash[operation_id] = handler
17
15
  end
18
16
  end
@@ -41,7 +41,7 @@ module OpenapiFirst
41
41
 
42
42
  raise ResponseInvalid, "Response has no content-type for '#{name}'" unless content_type
43
43
 
44
- media_type = content[content_type]
44
+ media_type = find_content_for_content_type(content, content_type)
45
45
  unless media_type
46
46
  message = "Response content type not found '#{content_type}' for '#{name}'"
47
47
  raise ResponseContentTypeNotFoundError, message
@@ -49,6 +49,12 @@ module OpenapiFirst
49
49
  media_type['schema']
50
50
  end
51
51
 
52
+ def request_body_schema_for(request_content_type)
53
+ content = @operation.request_body.content
54
+ media_type = find_content_for_content_type(content, request_content_type)
55
+ media_type&.fetch('schema', nil)
56
+ end
57
+
52
58
  def response_for(status)
53
59
  @operation.response_by_code(status.to_s, use_default: true).raw
54
60
  rescue OasParser::ResponseCodeNotFound
@@ -62,6 +68,13 @@ module OpenapiFirst
62
68
 
63
69
  private
64
70
 
71
+ def find_content_for_content_type(content, request_content_type)
72
+ content.fetch(request_content_type) do |_|
73
+ type = request_content_type.split(';')[0]
74
+ content[type] || content["#{type.split('/')[0]}/*"] || content['*/*']
75
+ end
76
+ end
77
+
65
78
  def build_parameters_json_schema
66
79
  return unless @operation.parameters&.any?
67
80
 
@@ -98,7 +98,8 @@ module OpenapiFirst
98
98
  def request_body_schema(content_type, operation)
99
99
  return unless operation
100
100
 
101
- schema = operation.request_body.content[content_type]&.fetch('schema')
101
+ schema = operation.request_body_schema_for(content_type)
102
+
102
103
  JSONSchemer.schema(schema) if schema
103
104
  end
104
105
 
@@ -140,8 +141,9 @@ module OpenapiFirst
140
141
 
141
142
  def serialize_query_parameter_errors(validation_errors)
142
143
  validation_errors.map do |error|
144
+ pointer = error['data_pointer'][1..].to_s
143
145
  {
144
- source: { parameter: File.basename(error['data_pointer']) }
146
+ source: { parameter: pointer }
145
147
  }.update(ValidationFormat.error_details(error))
146
148
  end
147
149
  end
@@ -149,14 +151,27 @@ module OpenapiFirst
149
151
  def parse_parameter(value, schema)
150
152
  return filtered_params(schema, value) if schema['properties']
151
153
 
154
+ return parse_array_parameter(value, schema) if schema['type'] == 'array'
155
+
156
+ parse_simple_value(value, schema)
157
+ end
158
+
159
+ def parse_array_parameter(value, schema)
160
+ array = value.is_a?(Array) ? value : value.split(',')
161
+ return array unless schema['items']
162
+
163
+ array.map! { |e| parse_simple_value(e, schema['items']) }
164
+ end
165
+
166
+ def parse_simple_value(value, schema)
167
+ return to_boolean(value) if schema['type'] == 'boolean'
168
+
152
169
  begin
153
170
  return Integer(value, 10) if schema['type'] == 'integer'
154
171
  return Float(value) if schema['type'] == 'number'
155
172
  rescue ArgumentError
156
173
  value
157
174
  end
158
- return to_boolean(value) if schema['type'] == 'boolean'
159
-
160
175
  value
161
176
  end
162
177
 
@@ -10,11 +10,13 @@ module OpenapiFirst
10
10
  app,
11
11
  spec:,
12
12
  raise_error: false,
13
+ not_found: :halt,
13
14
  parent_app: nil
14
15
  )
15
16
  @app = app
16
17
  @parent_app = parent_app
17
18
  @raise = raise_error
19
+ @not_found = not_found
18
20
  @filepath = spec.filepath
19
21
  @router = build_router(spec.operations)
20
22
  end
@@ -23,9 +25,11 @@ module OpenapiFirst
23
25
  env[OPERATION] = nil
24
26
  response = call_router(env)
25
27
  if env[OPERATION].nil?
26
- return @parent_app.call(env) if @parent_app # This should only happen if used via OpenapiFirst.middlware
28
+ return @parent_app.call(env) if @parent_app # This should only happen if used via OpenapiFirst.middleware
27
29
 
28
30
  raise_error(env) if @raise
31
+
32
+ return @app.call(env) if @not_found == :continue
29
33
  end
30
34
  response
31
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.12.2'
4
+ VERSION = '0.13.2'
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.12.2
4
+ version: 0.13.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-24 00:00:00.000000000 Z
11
+ date: 2020-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deep_merge