openapi_first 0.13.2 → 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 +4 -4
- data/.rubocop.yml +3 -44
- data/CHANGELOG.md +4 -1
- data/Gemfile.lock +17 -17
- data/README.md +7 -1
- data/benchmarks/Gemfile +2 -1
- data/benchmarks/Gemfile.lock +19 -16
- data/benchmarks/apps/committee.ru +14 -20
- data/benchmarks/apps/hanami_api.ru +21 -0
- data/benchmarks/apps/hanami_router.ru +1 -1
- data/benchmarks/apps/sinatra.ru +1 -1
- data/benchmarks/apps/syro.ru +3 -3
- data/lib/openapi_first/definition.rb +2 -6
- data/lib/openapi_first/operation.rb +22 -7
- data/lib/openapi_first/request_validation.rb +10 -25
- data/lib/openapi_first/response_validation.rb +2 -1
- data/lib/openapi_first/router.rb +1 -2
- data/lib/openapi_first/schema_validation.rb +36 -0
- data/lib/openapi_first/utils.rb +7 -14
- data/lib/openapi_first/validation_format.rb +17 -0
- data/lib/openapi_first/version.rb +1 -1
- data/openapi_first.gemspec +3 -1
- metadata +11 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 94cbe97158c8482c40dd1521e1f83cef287ad2e7e32f82746f24adcf45f60c4e
|
4
|
+
data.tar.gz: 1aff02e97e51b67334c34bc6ee967559def9218c4e81dc6295204bb5447f6715
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4ca9c77f8b70024083b831a02d824c2fd0321ff3598234f085133b161ba46a2177e9279e80e22f2ca56da466b087c0c7ac3a62c6dec9a7f906bf03fd44f2eb47
|
7
|
+
data.tar.gz: bcffa9f70194081780a0348f17e695b05f879eb029e4932baa224c3ca1d03e7fb531931f111bfdbcf452f772c0729e725cd3101779aa363408f859e63fd989f8
|
data/.rubocop.yml
CHANGED
@@ -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,47 +9,5 @@ Metrics/BlockLength:
|
|
8
9
|
Exclude:
|
9
10
|
- 'spec/**/*.rb'
|
10
11
|
- '*.gemspec'
|
11
|
-
|
12
|
-
|
13
|
-
Layout/SpaceAroundMethodCallOperator:
|
14
|
-
Enabled: true
|
15
|
-
Lint/DeprecatedOpenSSLConstant:
|
16
|
-
Enabled: true
|
17
|
-
Lint/DuplicateElsifCondition:
|
18
|
-
Enabled: true
|
19
|
-
Lint/RaiseException:
|
20
|
-
Enabled: true
|
21
|
-
Lint/MixedRegexpCaptureTypes:
|
22
|
-
Enabled: true
|
23
|
-
Style/RedundantRegexpCharacterClass:
|
24
|
-
Enabled: true
|
25
|
-
Style/RedundantRegexpEscape:
|
26
|
-
Enabled: true
|
27
|
-
Style/SlicingWithRange:
|
28
|
-
Enabled: true
|
29
|
-
Lint/StructNewOverride:
|
30
|
-
Enabled: true
|
31
|
-
Style/HashEachMethods:
|
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
|
45
|
-
Style/HashTransformKeys:
|
46
|
-
Enabled: true
|
47
|
-
Style/HashTransformValues:
|
48
|
-
Enabled: true
|
49
|
-
Style/RedundantAssignment:
|
50
|
-
Enabled: true
|
51
|
-
Style/RedundantFetchBlock:
|
52
|
-
Enabled: true
|
53
|
-
Style/RedundantFileExtensionInRequire:
|
54
|
-
Enabled: true
|
12
|
+
Metrics/MethodLength:
|
13
|
+
Max: 20
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 0.13.3
|
4
|
+
- Better error message if string does not match format
|
5
|
+
|
3
6
|
## 0.13.2
|
4
7
|
- Return indicator (`source: { parameter: 'list/1' }`) in error response body when array item in query parameter is invalid
|
5
8
|
|
@@ -22,7 +25,7 @@
|
|
22
25
|
|
23
26
|
## 0.12.1
|
24
27
|
- Fix response when handler returns 404 or 405
|
25
|
-
- Don't validate the response content if status is
|
28
|
+
- Don't validate the response content if status is 204 (no content)
|
26
29
|
|
27
30
|
## 0.12.0
|
28
31
|
- Change `ResponseValidator` to raise an exception if it found a problem
|
data/Gemfile.lock
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
openapi_first (0.13.
|
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.
|
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)
|
@@ -41,14 +41,14 @@ GEM
|
|
41
41
|
hash-deep-merge (0.1.1)
|
42
42
|
i18n (1.8.5)
|
43
43
|
concurrent-ruby (~> 1.0)
|
44
|
-
json_schemer (0.2.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
95
|
+
rubocop (0.93.1)
|
96
96
|
parallel (~> 1.10)
|
97
|
-
parser (>= 2.7.1.
|
97
|
+
parser (>= 2.7.1.5)
|
98
98
|
rainbow (>= 2.2.2, < 4.0)
|
99
|
-
regexp_parser (>= 1.
|
99
|
+
regexp_parser (>= 1.8)
|
100
100
|
rexml
|
101
|
-
rubocop-ast (>= 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.
|
105
|
-
parser (>= 2.7.
|
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.
|
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.4.
|
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
|
@@ -99,6 +99,12 @@ This will also add the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
|
|
99
99
|
|
100
100
|
tbd.
|
101
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
|
+
|
102
108
|
## OpenapiFirst::Responder
|
103
109
|
|
104
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.
|
data/benchmarks/Gemfile
CHANGED
data/benchmarks/Gemfile.lock
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ..
|
3
3
|
specs:
|
4
|
-
openapi_first (0.13.
|
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.
|
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,7 +21,7 @@ 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.
|
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)
|
@@ -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.
|
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.
|
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)
|
@@ -75,14 +77,14 @@ GEM
|
|
75
77
|
i18n (1.8.5)
|
76
78
|
concurrent-ruby (~> 1.0)
|
77
79
|
json_schema (0.20.9)
|
78
|
-
json_schemer (0.2.
|
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.
|
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.
|
98
|
+
oas_parser (0.25.2)
|
97
99
|
activesupport (>= 4.0.0)
|
98
100
|
addressable (~> 2.3)
|
99
101
|
builder (~> 3.2.3)
|
@@ -102,19 +104,19 @@ GEM
|
|
102
104
|
mustermann-contrib (~> 1.1.1)
|
103
105
|
nokogiri
|
104
106
|
openapi_parser (0.12.1)
|
105
|
-
public_suffix (4.0.
|
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
|
111
|
+
rack-protection (2.1.0)
|
110
112
|
rack
|
111
|
-
regexp_parser (1.
|
113
|
+
regexp_parser (1.8.2)
|
112
114
|
ruby2_keywords (0.0.2)
|
113
115
|
seg (1.2.0)
|
114
|
-
sinatra (2.0
|
116
|
+
sinatra (2.1.0)
|
115
117
|
mustermann (~> 1.0)
|
116
|
-
rack (~> 2.
|
117
|
-
rack-protection (= 2.0
|
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)
|
@@ -135,7 +137,8 @@ DEPENDENCIES
|
|
135
137
|
benchmark-memory
|
136
138
|
committee
|
137
139
|
grape
|
138
|
-
hanami-
|
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 =
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
12
|
+
get '/hello' do
|
13
|
+
json([{ hello: 'world' }])
|
14
|
+
end
|
18
15
|
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
}
|
data/benchmarks/apps/sinatra.ru
CHANGED
data/benchmarks/apps/syro.ru
CHANGED
@@ -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
|
10
|
+
res.json({ hello: 'world', id: inbox[:id] })
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
14
|
get do
|
15
|
-
res.json
|
15
|
+
res.json([{ hello: 'world' }])
|
16
16
|
end
|
17
17
|
|
18
18
|
post do
|
19
19
|
res.status = 201
|
20
|
-
res.json
|
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
|
-
@
|
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
|
@@ -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
|
26
|
-
|
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 ||=
|
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)
|
@@ -46,13 +57,17 @@ module OpenapiFirst
|
|
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
|
50
62
|
end
|
51
63
|
|
52
|
-
def
|
64
|
+
def request_body_schema(request_content_type)
|
53
65
|
content = @operation.request_body.content
|
54
66
|
media_type = find_content_for_content_type(content, request_content_type)
|
55
|
-
media_type&.fetch('schema', nil)
|
67
|
+
schema = media_type&.fetch('schema', nil)
|
68
|
+
return unless schema
|
69
|
+
|
70
|
+
SchemaValidation.new(schema, write: write?)
|
56
71
|
end
|
57
72
|
|
58
73
|
def response_for(status)
|
@@ -84,7 +99,7 @@ module OpenapiFirst
|
|
84
99
|
end
|
85
100
|
end
|
86
101
|
|
87
|
-
def generate_schema(schema, params, parameter)
|
102
|
+
def generate_schema(schema, params, parameter)
|
88
103
|
required = Set.new(schema['required'])
|
89
104
|
params.each do |key, value|
|
90
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
|
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
|
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 =
|
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
|
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,14 +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_schema_for(content_type)
|
102
|
-
|
103
|
-
JSONSchemer.schema(schema) if schema
|
104
|
-
end
|
105
|
-
|
106
94
|
def serialize_request_body_errors(validation_errors)
|
107
95
|
validation_errors.map do |error|
|
108
96
|
{
|
@@ -114,14 +102,11 @@ module OpenapiFirst
|
|
114
102
|
end
|
115
103
|
|
116
104
|
def validate_query_parameters!(env, operation, params)
|
117
|
-
|
118
|
-
return unless
|
119
|
-
|
120
|
-
params = filtered_params(
|
121
|
-
errors =
|
122
|
-
operation.parameters_schema,
|
123
|
-
params
|
124
|
-
)
|
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))
|
125
110
|
halt_with_error(400, serialize_query_parameter_errors(errors)) if errors.any?
|
126
111
|
env[PARAMETERS] = params
|
127
112
|
env[INBOX].merge! params
|
@@ -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 =
|
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?
|
data/lib/openapi_first/router.rb
CHANGED
@@ -53,13 +53,12 @@ module OpenapiFirst
|
|
53
53
|
env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
|
54
54
|
end
|
55
55
|
|
56
|
-
def build_router(operations) # rubocop:disable Metrics/AbcSize
|
56
|
+
def build_router(operations) # rubocop:disable Metrics/AbcSize
|
57
57
|
router = Hanami::Router.new {}
|
58
58
|
operations.each do |operation|
|
59
59
|
normalized_path = operation.path.gsub('{', ':').gsub('}', '')
|
60
60
|
if operation.operation_id.nil?
|
61
61
|
warn "operationId is missing in '#{operation.method} #{operation.path}'. I am ignoring this operation."
|
62
|
-
next
|
63
62
|
end
|
64
63
|
router.public_send(
|
65
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
|
data/lib/openapi_first/utils.rb
CHANGED
@@ -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.
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
data/openapi_first.gemspec
CHANGED
@@ -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.13.
|
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-
|
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:
|
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:
|
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:
|
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: []
|