openapi_first 0.12.3 → 0.13.3

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: 8f5e77e342d4dba0c6ac4d535511148333efac5bc4b2cea62ee6d8b75219233e
4
- data.tar.gz: 9b3341115e77bd4abc4b14d89464093d78483e251cf69e2b2508461c98aa1ccf
3
+ metadata.gz: 94cbe97158c8482c40dd1521e1f83cef287ad2e7e32f82746f24adcf45f60c4e
4
+ data.tar.gz: 1aff02e97e51b67334c34bc6ee967559def9218c4e81dc6295204bb5447f6715
5
5
  SHA512:
6
- metadata.gz: 86f19dc76569748825731014124b689b7bddf5cce6a6fe38b5d95adb4643eeeeaa3ae6024a290aaf2aed92127147cc948a55faa062b5d97b99b7d2fe65df98cd
7
- data.tar.gz: 27cef03d9ef7e0b49af33154e44ca68d3e0e860dcd94cbf6d2cdc4661ff90a4be590b92365aabfcdffe330f2aa2e96c12f7277ee2fa572fe02a2cc2c263e34b5
6
+ metadata.gz: 4ca9c77f8b70024083b831a02d824c2fd0321ff3598234f085133b161ba46a2177e9279e80e22f2ca56da466b087c0c7ac3a62c6dec9a7f906bf03fd44f2eb47
7
+ data.tar.gz: bcffa9f70194081780a0348f17e695b05f879eb029e4932baa224c3ca1d03e7fb531931f111bfdbcf452f772c0729e725cd3101779aa363408f859e63fd989f8
@@ -1,5 +1,6 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.6
3
+ NewCops: enable
3
4
  Style/Documentation:
4
5
  Enabled: false
5
6
  Style/ExponentialNotation:
@@ -8,29 +9,5 @@ Metrics/BlockLength:
8
9
  Exclude:
9
10
  - 'spec/**/*.rb'
10
11
  - '*.gemspec'
11
- Layout/EmptyLinesAroundAttributeAccessor:
12
- Enabled: true
13
- Layout/SpaceAroundMethodCallOperator:
14
- Enabled: true
15
- Lint/DeprecatedOpenSSLConstant:
16
- Enabled: true
17
- Lint/RaiseException:
18
- Enabled: true
19
- Lint/MixedRegexpCaptureTypes:
20
- Enabled: true
21
- Style/RedundantRegexpCharacterClass:
22
- Enabled: true
23
- Style/RedundantRegexpEscape:
24
- Enabled: true
25
- Style/SlicingWithRange:
26
- Enabled: true
27
- Lint/StructNewOverride:
28
- Enabled: true
29
- Style/HashEachMethods:
30
- Enabled: false
31
- Style/HashTransformKeys:
32
- Enabled: true
33
- Style/HashTransformValues:
34
- Enabled: true
35
- Style/RedundantFetchBlock:
36
- Enabled: true
12
+ Metrics/MethodLength:
13
+ Max: 20
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.3
4
+ - Better error message if string does not match format
5
+
6
+ ## 0.13.2
7
+ - Return indicator (`source: { parameter: 'list/1' }`) in error response body when array item in query parameter is invalid
8
+
9
+ ## 0.13.0
10
+ - Add support for arrays in query parameters (style: form, explode: false)
11
+ - Remove warning when handler is not implemented
12
+
13
+ ## 0.12.5
14
+ - Add `not_found: :continue` option to Router to make it do nothing if request is unknown
15
+
16
+ ## 0.12.4
17
+ - content-type is found while ignoring additional content-type parameters (`application/json` is found when request/response content-type is `application/json; charset=UTF8`)
18
+ - Support wildcard mime-types when finding the content-type
19
+
3
20
  ## 0.12.3
4
21
  - Add `response_validation:`, `router_raise_error` options to standalone mode.
5
22
 
@@ -8,7 +25,7 @@
8
25
 
9
26
  ## 0.12.1
10
27
  - Fix response when handler returns 404 or 405
11
- - Don't validate the response content if status is 205 (no content)
28
+ - Don't validate the response content if status is 204 (no content)
12
29
 
13
30
  ## 0.12.0
14
31
  - Change `ResponseValidator` to raise an exception if it found a problem
@@ -1,11 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (0.12.3)
4
+ openapi_first (0.13.3)
5
5
  deep_merge (>= 1.2.1)
6
6
  hanami-router (~> 2.0.alpha3)
7
7
  hanami-utils (~> 2.0.alpha1)
8
- json_schemer (~> 0.2)
8
+ json_schemer (~> 0.2.16)
9
9
  multi_json (~> 1.14)
10
10
  oas_parser (~> 0.25.1)
11
11
  rack (~> 2.2)
@@ -13,7 +13,7 @@ PATH
13
13
  GEM
14
14
  remote: https://rubygems.org/
15
15
  specs:
16
- activesupport (6.0.3.2)
16
+ activesupport (6.0.3.4)
17
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
18
  i18n (>= 0.7, < 2)
19
19
  minitest (~> 5.1)
@@ -24,7 +24,7 @@ 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
29
  diff-lcs (1.4.4)
30
30
  ecma-re-validator (0.2.1)
@@ -39,16 +39,16 @@ 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.16)
45
45
  ecma-re-validator (~> 0.2)
46
46
  hana (~> 1.3)
47
47
  regexp_parser (~> 1.5)
48
48
  uri_template (~> 0.7)
49
49
  method_source (1.0.0)
50
50
  mini_portile2 (2.4.0)
51
- minitest (5.14.1)
51
+ minitest (5.14.2)
52
52
  multi_json (1.15.0)
53
53
  mustermann (1.1.1)
54
54
  ruby2_keywords (~> 0.0.1)
@@ -66,24 +66,24 @@ GEM
66
66
  mustermann-contrib (~> 1.1.1)
67
67
  nokogiri
68
68
  parallel (1.19.2)
69
- parser (2.7.1.4)
69
+ parser (2.7.2.0)
70
70
  ast (~> 2.4.1)
71
71
  pry (0.13.1)
72
72
  coderay (~> 1.1)
73
73
  method_source (~> 1.0)
74
- public_suffix (4.0.5)
74
+ public_suffix (4.0.6)
75
75
  rack (2.2.3)
76
76
  rack-test (1.1.0)
77
77
  rack (>= 1.0, < 3)
78
78
  rainbow (3.0.0)
79
79
  rake (13.0.1)
80
- regexp_parser (1.7.1)
80
+ regexp_parser (1.8.2)
81
81
  rexml (3.2.4)
82
82
  rspec (3.9.0)
83
83
  rspec-core (~> 3.9.0)
84
84
  rspec-expectations (~> 3.9.0)
85
85
  rspec-mocks (~> 3.9.0)
86
- rspec-core (3.9.2)
86
+ rspec-core (3.9.3)
87
87
  rspec-support (~> 3.9.3)
88
88
  rspec-expectations (3.9.2)
89
89
  diff-lcs (>= 1.2.0, < 2.0)
@@ -92,26 +92,26 @@ 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.87.1)
95
+ rubocop (0.93.1)
96
96
  parallel (~> 1.10)
97
- parser (>= 2.7.1.1)
97
+ parser (>= 2.7.1.5)
98
98
  rainbow (>= 2.2.2, < 4.0)
99
- regexp_parser (>= 1.7)
99
+ regexp_parser (>= 1.8)
100
100
  rexml
101
- rubocop-ast (>= 0.1.0, < 1.0)
101
+ rubocop-ast (>= 0.6.0)
102
102
  ruby-progressbar (~> 1.7)
103
103
  unicode-display_width (>= 1.4.0, < 2.0)
104
- rubocop-ast (0.1.0)
105
- parser (>= 2.7.0.1)
104
+ rubocop-ast (0.8.0)
105
+ parser (>= 2.7.1.5)
106
106
  ruby-progressbar (1.10.1)
107
107
  ruby2_keywords (0.0.2)
108
108
  thread_safe (0.3.6)
109
109
  transproc (1.1.1)
110
- tzinfo (1.2.7)
110
+ tzinfo (1.2.8)
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.1)
114
+ zeitwerk (2.4.1)
115
115
 
116
116
  PLATFORMS
117
117
  ruby
data/README.md CHANGED
@@ -19,7 +19,7 @@ OpenapiFirst consists of these Rack middlewares:
19
19
 
20
20
  - [`OpenapiFirst::Router`](#OpenapiFirst::Router) – Finds the OpenAPI operation for the current request or returns 404 if no operation was found. This can be customized.
21
21
  - [`OpenapiFirst::RequestValidation`](#OpenapiFirst::RequestValidation) – Validates the request against the API description and returns 400 if the request is invalid.
22
- - [`OpenapiFirst::Responder`](#OpenapiFirst::Responder) calls the [handler](#handlers) found for the operation.
22
+ - [`OpenapiFirst::Responder`](#OpenapiFirst::Responder) calls the [handler](#handlers) found for the operation, sets the correct content-type and serialized the response body to json if needed.
23
23
  - [`OpenapiFirst::ResponseValidation`](#OpenapiFirst::ResponseValidation) Validates the response and raises an exception if the response body is invalid.
24
24
 
25
25
  ## OpenapiFirst::Router
@@ -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
 
@@ -93,6 +99,12 @@ This will also add the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
93
99
 
94
100
  tbd.
95
101
 
102
+ ### readOnly / writeOnly properties
103
+
104
+ Request validation fails if request includes a property with `readOnly: true`.
105
+
106
+ Response validation fails if response body includes a property with `writeOnly: true`.
107
+
96
108
  ## OpenapiFirst::Responder
97
109
 
98
110
  This Rack endpoint maps the HTTP request to a method call based on the [operationId](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operation-object) in your API description and calls it. Responder also adds a content-type to the response.
@@ -6,7 +6,8 @@ gem 'benchmark-ips'
6
6
  gem 'benchmark-memory'
7
7
  gem 'committee'
8
8
  gem 'grape'
9
- gem 'hanami-router', '~> 2.0.0.alpha2'
9
+ gem 'hanami-api'
10
+ gem 'hanami-router', '~> 2.0.0.alpha3'
10
11
  gem 'multi_json'
11
12
  gem 'openapi_first', path: '../'
12
13
  gem 'sinatra'
@@ -1,11 +1,11 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- openapi_first (0.12.3)
4
+ openapi_first (0.13.3)
5
5
  deep_merge (>= 1.2.1)
6
6
  hanami-router (~> 2.0.alpha3)
7
7
  hanami-utils (~> 2.0.alpha1)
8
- json_schemer (~> 0.2)
8
+ json_schemer (~> 0.2.16)
9
9
  multi_json (~> 1.14)
10
10
  oas_parser (~> 0.25.1)
11
11
  rack (~> 2.2)
@@ -13,7 +13,7 @@ PATH
13
13
  GEM
14
14
  remote: https://rubygems.org/
15
15
  specs:
16
- activesupport (6.0.3.2)
16
+ activesupport (6.0.3.4)
17
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
18
  i18n (>= 0.7, < 2)
19
19
  minitest (~> 5.1)
@@ -21,15 +21,15 @@ GEM
21
21
  zeitwerk (~> 2.2, >= 2.2.2)
22
22
  addressable (2.7.0)
23
23
  public_suffix (>= 2.0.2, < 5.0)
24
- benchmark-ips (2.8.2)
24
+ benchmark-ips (2.8.3)
25
25
  benchmark-memory (0.1.2)
26
26
  memory_profiler (~> 0.9)
27
27
  builder (3.2.4)
28
- committee (4.1.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.8)
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.5.0)
59
59
  activesupport
60
60
  builder
61
61
  dry-types (>= 1.1)
@@ -63,6 +63,8 @@ GEM
63
63
  rack (>= 1.3.0)
64
64
  rack-accept
65
65
  hana (1.3.6)
66
+ hanami-api (0.1.1)
67
+ hanami-router (~> 2.0.alpha)
66
68
  hanami-router (2.0.0.alpha3)
67
69
  mustermann (~> 1.0)
68
70
  mustermann-contrib (~> 1.0)
@@ -72,17 +74,17 @@ GEM
72
74
  transproc (~> 1.0)
73
75
  hansi (0.2.0)
74
76
  hash-deep-merge (0.1.1)
75
- i18n (1.8.3)
77
+ i18n (1.8.5)
76
78
  concurrent-ruby (~> 1.0)
77
79
  json_schema (0.20.9)
78
- json_schemer (0.2.11)
80
+ json_schemer (0.2.16)
79
81
  ecma-re-validator (~> 0.2)
80
82
  hana (~> 1.3)
81
83
  regexp_parser (~> 1.5)
82
84
  uri_template (~> 0.7)
83
85
  memory_profiler (0.9.14)
84
86
  mini_portile2 (2.4.0)
85
- minitest (5.14.1)
87
+ minitest (5.14.2)
86
88
  multi_json (1.15.0)
87
89
  mustermann (1.1.1)
88
90
  ruby2_keywords (~> 0.0.1)
@@ -93,7 +95,7 @@ GEM
93
95
  mustermann (>= 1.0.0)
94
96
  nokogiri (1.10.10)
95
97
  mini_portile2 (~> 2.4.0)
96
- oas_parser (0.25.1)
98
+ oas_parser (0.25.2)
97
99
  activesupport (>= 4.0.0)
98
100
  addressable (~> 2.3)
99
101
  builder (~> 3.2.3)
@@ -101,20 +103,20 @@ GEM
101
103
  hash-deep-merge
102
104
  mustermann-contrib (~> 1.1.1)
103
105
  nokogiri
104
- openapi_parser (0.11.2)
105
- public_suffix (4.0.5)
106
+ openapi_parser (0.12.1)
107
+ public_suffix (4.0.6)
106
108
  rack (2.2.3)
107
109
  rack-accept (0.4.5)
108
110
  rack (>= 0.4)
109
- rack-protection (2.0.8.1)
111
+ rack-protection (2.1.0)
110
112
  rack
111
- regexp_parser (1.7.1)
113
+ regexp_parser (1.8.2)
112
114
  ruby2_keywords (0.0.2)
113
115
  seg (1.2.0)
114
- sinatra (2.0.8.1)
116
+ sinatra (2.1.0)
115
117
  mustermann (~> 1.0)
116
- rack (~> 2.0)
117
- rack-protection (= 2.0.8.1)
118
+ rack (~> 2.2)
119
+ rack-protection (= 2.1.0)
118
120
  tilt (~> 2.0)
119
121
  syro (3.2.0)
120
122
  rack (>= 1.6.0)
@@ -125,7 +127,7 @@ GEM
125
127
  tzinfo (1.2.7)
126
128
  thread_safe (~> 0.1)
127
129
  uri_template (0.7.0)
128
- zeitwerk (2.3.1)
130
+ zeitwerk (2.4.0)
129
131
 
130
132
  PLATFORMS
131
133
  ruby
@@ -135,7 +137,8 @@ DEPENDENCIES
135
137
  benchmark-memory
136
138
  committee
137
139
  grape
138
- hanami-router (~> 2.0.0.alpha2)
140
+ hanami-api
141
+ hanami-router (~> 2.0.0.alpha3)
139
142
  multi_json
140
143
  openapi_first!
141
144
  sinatra
@@ -1,30 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'committee'
4
- require 'syro'
5
3
  require 'multi_json'
4
+ require 'committee'
5
+ require 'hanami/api'
6
6
 
7
- app = Syro.new do
8
- on 'hello' do
9
- on :id do
10
- get do
11
- res.json MultiJson.dump(hello: 'world', id: inbox[:id])
12
- end
13
- end
7
+ app = Class.new(Hanami::API) do
8
+ get '/hello/:id' do
9
+ json(hello: 'world', id: params.fetch(:id))
10
+ end
14
11
 
15
- get do
16
- res.json [MultiJson.dump(hello: 'world')]
17
- end
12
+ get '/hello' do
13
+ json([{ hello: 'world' }])
14
+ end
18
15
 
19
- post do
20
- res.status = 201
21
- res.json MultiJson.dump(hello: 'world')
22
- end
16
+ post '/hello' do
17
+ status 201
18
+ json(hello: 'world')
23
19
  end
24
- end
20
+ end.new
25
21
 
26
- use Committee::Middleware::RequestValidation,
27
- schema_path: './apps/openapi.yaml',
28
- coerce_date_times: false
22
+ use Committee::Middleware::RequestValidation, schema_path: './apps/openapi.yaml'
29
23
 
30
24
  run app
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'multi_json'
4
+ require 'hanami/api'
5
+
6
+ app = Class.new(Hanami::API) do
7
+ get '/hello/:id' do
8
+ json(hello: 'world', id: params.fetch(:id))
9
+ end
10
+
11
+ get '/hello' do
12
+ json([{ hello: 'world' }])
13
+ end
14
+
15
+ post '/hello' do
16
+ status 201
17
+ json(hello: 'world')
18
+ end
19
+ end.new
20
+
21
+ run app
@@ -4,7 +4,7 @@ require 'hanami/router'
4
4
  require 'multi_json'
5
5
 
6
6
  app = Hanami::Router.new do
7
- get '/hello', to: ->(_env) { [200, {}, [MultiJson.dump(hello: 'world')]] }
7
+ get '/hello', to: ->(_env) { [200, {}, [MultiJson.dump([{ hello: 'world' }])]] }
8
8
  get '/hello/:id', to: lambda { |env|
9
9
  [200, {}, [MultiJson.dump(hello: 'world', id: env['router.params'][:id])]]
10
10
  }
@@ -13,7 +13,7 @@ class SinatraExample < Sinatra::Base
13
13
 
14
14
  get '/hello' do
15
15
  content_type :json
16
- [MultiJson.dump(hello: 'world')]
16
+ MultiJson.dump([{ hello: 'world' }])
17
17
  end
18
18
 
19
19
  post '/hello' do
@@ -7,17 +7,17 @@ app = Syro.new do
7
7
  on 'hello' do
8
8
  on :id do
9
9
  get do
10
- res.json MultiJson.dump(hello: 'world', id: inbox[:id])
10
+ res.json({ hello: 'world', id: inbox[:id] })
11
11
  end
12
12
  end
13
13
 
14
14
  get do
15
- res.json [MultiJson.dump(hello: 'world')]
15
+ res.json([{ hello: 'world' }])
16
16
  end
17
17
 
18
18
  post do
19
19
  res.status = 201
20
- res.json MultiJson.dump(hello: 'world')
20
+ res.json({ hello: 'world' })
21
21
  end
22
22
  end
23
23
  end
@@ -4,15 +4,11 @@ require_relative 'operation'
4
4
 
5
5
  module OpenapiFirst
6
6
  class Definition
7
- attr_reader :filepath
7
+ attr_reader :filepath, :operations
8
8
 
9
9
  def initialize(parsed)
10
10
  @filepath = parsed.path
11
- @spec = parsed
12
- end
13
-
14
- def operations
15
- @spec.endpoints.map { |e| Operation.new(e) }
11
+ @operations = parsed.endpoints.map { |e| Operation.new(e) }
16
12
  end
17
13
  end
18
14
  end
@@ -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
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'forwardable'
4
4
  require 'json_schemer'
5
+ require_relative 'schema_validation'
5
6
  require_relative 'utils'
6
7
  require_relative 'response_object'
7
8
 
@@ -14,6 +15,9 @@ module OpenapiFirst
14
15
  :request_body,
15
16
  :operation_id
16
17
 
18
+ WRITE_METHODS = Set.new(%w[post put patch delete]).freeze
19
+ private_constant :WRITE_METHODS
20
+
17
21
  def initialize(parsed)
18
22
  @operation = parsed
19
23
  end
@@ -22,12 +26,19 @@ module OpenapiFirst
22
26
  @operation.path.path
23
27
  end
24
28
 
25
- def parameters_json_schema
26
- @parameters_json_schema ||= build_parameters_json_schema
29
+ def read?
30
+ !write?
31
+ end
32
+
33
+ def write?
34
+ WRITE_METHODS.include?(method)
27
35
  end
28
36
 
29
37
  def parameters_schema
30
- @parameters_schema ||= parameters_json_schema && JSONSchemer.schema(parameters_json_schema)
38
+ @parameters_schema ||= begin
39
+ parameters_json_schema = build_parameters_json_schema
40
+ parameters_json_schema && SchemaValidation.new(parameters_json_schema)
41
+ end
31
42
  end
32
43
 
33
44
  def content_type_for(status)
@@ -41,12 +52,22 @@ module OpenapiFirst
41
52
 
42
53
  raise ResponseInvalid, "Response has no content-type for '#{name}'" unless content_type
43
54
 
44
- media_type = content[content_type]
55
+ media_type = find_content_for_content_type(content, content_type)
45
56
  unless media_type
46
57
  message = "Response content type not found '#{content_type}' for '#{name}'"
47
58
  raise ResponseContentTypeNotFoundError, message
48
59
  end
49
- media_type['schema']
60
+ schema = media_type['schema']
61
+ SchemaValidation.new(schema, write: false) if schema
62
+ end
63
+
64
+ def request_body_schema(request_content_type)
65
+ content = @operation.request_body.content
66
+ media_type = find_content_for_content_type(content, request_content_type)
67
+ schema = media_type&.fetch('schema', nil)
68
+ return unless schema
69
+
70
+ SchemaValidation.new(schema, write: write?)
50
71
  end
51
72
 
52
73
  def response_for(status)
@@ -62,6 +83,13 @@ module OpenapiFirst
62
83
 
63
84
  private
64
85
 
86
+ def find_content_for_content_type(content, request_content_type)
87
+ content.fetch(request_content_type) do |_|
88
+ type = request_content_type.split(';')[0]
89
+ content[type] || content["#{type.split('/')[0]}/*"] || content['*/*']
90
+ end
91
+ end
92
+
65
93
  def build_parameters_json_schema
66
94
  return unless @operation.parameters&.any?
67
95
 
@@ -71,7 +99,7 @@ module OpenapiFirst
71
99
  end
72
100
  end
73
101
 
74
- def generate_schema(schema, params, parameter) # rubocop:disable Metrics/MethodLength
102
+ def generate_schema(schema, params, parameter)
75
103
  required = Set.new(schema['required'])
76
104
  params.each do |key, value|
77
105
  required << key if parameter.required
@@ -16,7 +16,7 @@ module OpenapiFirst
16
16
  @raise = raise_error
17
17
  end
18
18
 
19
- def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
19
+ def call(env) # rubocop:disable Metrics/AbcSize
20
20
  operation = env[OpenapiFirst::OPERATION]
21
21
  return @app.call(env) unless operation
22
22
 
@@ -45,17 +45,17 @@ module OpenapiFirst
45
45
  validate_request_body_presence!(body, operation)
46
46
  return if body.empty?
47
47
 
48
- schema = request_body_schema(content_type, operation)
48
+ schema = operation&.request_body_schema(content_type)
49
49
  return unless schema
50
50
 
51
51
  parsed_request_body = parse_request_body!(body)
52
- errors = validate_json_schema(schema, parsed_request_body)
52
+ errors = schema.validate(parsed_request_body)
53
53
  halt_with_error(400, serialize_request_body_errors(errors)) if errors.any?
54
- env[INBOX].merge! env[REQUEST_BODY] = parsed_request_body
54
+ env[INBOX].merge! env[REQUEST_BODY] = Utils.deep_symbolize(parsed_request_body)
55
55
  end
56
56
 
57
57
  def parse_request_body!(body)
58
- MultiJson.load(body, symbolize_keys: true)
58
+ MultiJson.load(body)
59
59
  rescue MultiJson::ParseError => e
60
60
  err = { title: 'Failed to parse body as JSON' }
61
61
  err[:detail] = e.cause unless ENV['RACK_ENV'] == 'production'
@@ -74,10 +74,6 @@ module OpenapiFirst
74
74
  halt_with_error(415, 'Request body is required')
75
75
  end
76
76
 
77
- def validate_json_schema(schema, object)
78
- schema.validate(Utils.deep_stringify(object))
79
- end
80
-
81
77
  def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
82
78
  {
83
79
  status: status.to_s,
@@ -95,13 +91,6 @@ module OpenapiFirst
95
91
  ).finish
96
92
  end
97
93
 
98
- def request_body_schema(content_type, operation)
99
- return unless operation
100
-
101
- schema = operation.request_body.content[content_type]&.fetch('schema')
102
- JSONSchemer.schema(schema) if schema
103
- end
104
-
105
94
  def serialize_request_body_errors(validation_errors)
106
95
  validation_errors.map do |error|
107
96
  {
@@ -113,14 +102,11 @@ module OpenapiFirst
113
102
  end
114
103
 
115
104
  def validate_query_parameters!(env, operation, params)
116
- json_schema = operation.parameters_json_schema
117
- return unless json_schema
118
-
119
- params = filtered_params(json_schema, params)
120
- errors = validate_json_schema(
121
- operation.parameters_schema,
122
- params
123
- )
105
+ schema = operation.parameters_schema
106
+ return unless schema
107
+
108
+ params = filtered_params(schema.raw_schema, params)
109
+ errors = schema.validate(Utils.deep_stringify(params))
124
110
  halt_with_error(400, serialize_query_parameter_errors(errors)) if errors.any?
125
111
  env[PARAMETERS] = params
126
112
  env[INBOX].merge! params
@@ -140,8 +126,9 @@ module OpenapiFirst
140
126
 
141
127
  def serialize_query_parameter_errors(validation_errors)
142
128
  validation_errors.map do |error|
129
+ pointer = error['data_pointer'][1..].to_s
143
130
  {
144
- source: { parameter: File.basename(error['data_pointer']) }
131
+ source: { parameter: pointer }
145
132
  }.update(ValidationFormat.error_details(error))
146
133
  end
147
134
  end
@@ -149,14 +136,27 @@ module OpenapiFirst
149
136
  def parse_parameter(value, schema)
150
137
  return filtered_params(schema, value) if schema['properties']
151
138
 
139
+ return parse_array_parameter(value, schema) if schema['type'] == 'array'
140
+
141
+ parse_simple_value(value, schema)
142
+ end
143
+
144
+ def parse_array_parameter(value, schema)
145
+ array = value.is_a?(Array) ? value : value.split(',')
146
+ return array unless schema['items']
147
+
148
+ array.map! { |e| parse_simple_value(e, schema['items']) }
149
+ end
150
+
151
+ def parse_simple_value(value, schema)
152
+ return to_boolean(value) if schema['type'] == 'boolean'
153
+
152
154
  begin
153
155
  return Integer(value, 10) if schema['type'] == 'integer'
154
156
  return Float(value) if schema['type'] == 'number'
155
157
  rescue ArgumentError
156
158
  value
157
159
  end
158
- return to_boolean(value) if schema['type'] == 'boolean'
159
-
160
160
  value
161
161
  end
162
162
 
@@ -41,7 +41,8 @@ module OpenapiFirst
41
41
  full_body = +''
42
42
  response.each { |chunk| full_body << chunk }
43
43
  data = full_body.empty? ? {} : load_json(full_body)
44
- errors = JSONSchemer.schema(schema).validate(data).to_a.map do |error|
44
+ errors = schema.validate(data)
45
+ errors = errors.to_a.map! do |error|
45
46
  error_message_for(error)
46
47
  end
47
48
  raise ResponseBodyInvalidError, errors.join(', ') if errors.any?
@@ -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
@@ -49,13 +53,12 @@ module OpenapiFirst
49
53
  env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
50
54
  end
51
55
 
52
- def build_router(operations) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
56
+ def build_router(operations) # rubocop:disable Metrics/AbcSize
53
57
  router = Hanami::Router.new {}
54
58
  operations.each do |operation|
55
59
  normalized_path = operation.path.gsub('{', ':').gsub('}', '')
56
60
  if operation.operation_id.nil?
57
61
  warn "operationId is missing in '#{operation.method} #{operation.path}'. I am ignoring this operation."
58
- next
59
62
  end
60
63
  router.public_send(
61
64
  operation.method,
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json_schemer'
4
+
5
+ module OpenapiFirst
6
+ class SchemaValidation
7
+ attr_reader :raw_schema
8
+
9
+ def initialize(schema, write: true)
10
+ @raw_schema = schema
11
+ custom_keywords = {}
12
+ custom_keywords['writeOnly'] = proc { |data| !data } unless write
13
+ custom_keywords['readOnly'] = proc { |data| !data } if write
14
+ @schemer = JSONSchemer.schema(
15
+ schema,
16
+ keywords: custom_keywords,
17
+ before_property_validation: proc do |data, property, property_schema, parent|
18
+ convert_nullable(data, property, property_schema, parent)
19
+ end
20
+ )
21
+ end
22
+
23
+ def validate(input)
24
+ @schemer.validate(input)
25
+ end
26
+
27
+ private
28
+
29
+ def convert_nullable(_data, _property, property_schema, _parent)
30
+ return unless property_schema.is_a?(Hash) && property_schema['nullable'] && property_schema['type']
31
+
32
+ property_schema['type'] = [*property_schema['type'], 'null']
33
+ property_schema.delete('nullable')
34
+ end
35
+ end
36
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'hanami/utils/string'
4
+ require 'hanami/utils/hash'
4
5
  require 'deep_merge/core'
5
6
 
6
7
  module OpenapiFirst
@@ -17,20 +18,12 @@ module OpenapiFirst
17
18
  Hanami::Utils::String.classify(string)
18
19
  end
19
20
 
20
- def self.deep_stringify(params) # rubocop:disable Metrics/MethodLength
21
- params.each_with_object({}) do |(key, value), output|
22
- output[key.to_s] =
23
- case value
24
- when ::Hash
25
- deep_stringify(value)
26
- when Array
27
- value.map do |item|
28
- item.is_a?(::Hash) ? deep_stringify(item) : item
29
- end
30
- else
31
- value
32
- end
33
- end
21
+ def self.deep_symbolize(hash)
22
+ Hanami::Utils::Hash.deep_symbolize(hash)
23
+ end
24
+
25
+ def self.deep_stringify(hash)
26
+ Hanami::Utils::Hash.deep_stringify(hash)
34
27
  end
35
28
  end
36
29
  end
@@ -6,17 +6,32 @@ module OpenapiFirst
6
6
 
7
7
  # rubocop:disable Metrics/MethodLength
8
8
  # rubocop:disable Metrics/AbcSize
9
+ # rubocop:disable Metrics/CyclomaticComplexity
10
+ # rubocop:disable Metrics/PerceivedComplexity
9
11
  def self.error_details(error)
10
12
  if error['type'] == 'pattern'
11
13
  {
12
14
  title: 'is not valid',
13
15
  detail: "does not match pattern '#{error['schema']['pattern']}'"
14
16
  }
17
+ elsif error['type'] == 'format'
18
+ {
19
+ title: "has not a valid #{error.dig('schema', 'format')} format",
20
+ detail: "#{error['data'].inspect} is not a valid #{error.dig('schema', 'format')} format"
21
+ }
15
22
  elsif error['type'] == 'required'
16
23
  missing_keys = error['details']['missing_keys']
17
24
  {
18
25
  title: "is missing required properties: #{missing_keys.join(', ')}"
19
26
  }
27
+ elsif error['type'] == 'readOnly'
28
+ {
29
+ title: 'appears in request, but is read-only'
30
+ }
31
+ elsif error['type'] == 'writeOnly'
32
+ {
33
+ title: 'write-only field appears in response:'
34
+ }
20
35
  elsif SIMPLE_TYPES.include?(error['type'])
21
36
  {
22
37
  title: "should be a #{error['type']}"
@@ -29,5 +44,7 @@ module OpenapiFirst
29
44
  end
30
45
  # rubocop:enable Metrics/MethodLength
31
46
  # rubocop:enable Metrics/AbcSize
47
+ # rubocop:enable Metrics/CyclomaticComplexity
48
+ # rubocop:enable Metrics/PerceivedComplexity
32
49
  end
33
50
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.12.3'
4
+ VERSION = '0.13.3'
5
5
  end
@@ -32,10 +32,12 @@ 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'
36
+
35
37
  spec.add_runtime_dependency 'deep_merge', '>= 1.2.1'
36
38
  spec.add_runtime_dependency 'hanami-router', '~> 2.0.alpha3'
37
39
  spec.add_runtime_dependency 'hanami-utils', '~> 2.0.alpha1'
38
- spec.add_runtime_dependency 'json_schemer', '~> 0.2'
40
+ spec.add_runtime_dependency 'json_schemer', '~> 0.2.16'
39
41
  spec.add_runtime_dependency 'multi_json', '~> 1.14'
40
42
  spec.add_runtime_dependency 'oas_parser', '~> 0.25.1'
41
43
  spec.add_runtime_dependency 'rack', '~> 2.2'
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.3
4
+ version: 0.13.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-07-10 00:00:00.000000000 Z
11
+ date: 2020-11-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deep_merge
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.2'
61
+ version: 0.2.16
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0.2'
68
+ version: 0.2.16
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: multi_json
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -164,7 +164,7 @@ dependencies:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
166
  version: '3'
167
- description:
167
+ description:
168
168
  email:
169
169
  - andreas.haller@posteo.de
170
170
  executables: []
@@ -187,6 +187,7 @@ files:
187
187
  - benchmarks/Gemfile.lock
188
188
  - benchmarks/apps/committee.ru
189
189
  - benchmarks/apps/grape.ru
190
+ - benchmarks/apps/hanami_api.ru
190
191
  - benchmarks/apps/hanami_router.ru
191
192
  - benchmarks/apps/openapi.yaml
192
193
  - benchmarks/apps/openapi_first.ru
@@ -213,6 +214,7 @@ files:
213
214
  - lib/openapi_first/response_validator.rb
214
215
  - lib/openapi_first/router.rb
215
216
  - lib/openapi_first/router_required.rb
217
+ - lib/openapi_first/schema_validation.rb
216
218
  - lib/openapi_first/utils.rb
217
219
  - lib/openapi_first/validation.rb
218
220
  - lib/openapi_first/validation_format.rb
@@ -225,7 +227,7 @@ metadata:
225
227
  https://github.com/ahx/openapi_first: https://github.com/ahx/openapi_first
226
228
  source_code_uri: https://github.com/ahx/openapi_first
227
229
  changelog_uri: https://github.com/ahx/openapi_first/blob/master/CHANGELOG.md
228
- post_install_message:
230
+ post_install_message:
229
231
  rdoc_options: []
230
232
  require_paths:
231
233
  - lib
@@ -233,7 +235,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
233
235
  requirements:
234
236
  - - ">="
235
237
  - !ruby/object:Gem::Version
236
- version: '0'
238
+ version: 2.6.0
237
239
  required_rubygems_version: !ruby/object:Gem::Requirement
238
240
  requirements:
239
241
  - - ">="
@@ -241,7 +243,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
241
243
  version: '0'
242
244
  requirements: []
243
245
  rubygems_version: 3.1.2
244
- signing_key:
246
+ signing_key:
245
247
  specification_version: 4
246
248
  summary: Implement REST APIs based on OpenApi.
247
249
  test_files: []