openapi_first 0.12.3 → 0.13.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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: []