openapi_first 0.13.3 → 0.14.0

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: 94cbe97158c8482c40dd1521e1f83cef287ad2e7e32f82746f24adcf45f60c4e
4
- data.tar.gz: 1aff02e97e51b67334c34bc6ee967559def9218c4e81dc6295204bb5447f6715
3
+ metadata.gz: 4f1e3ae677c078e5e4b64641c76c40a66994edb8599d9001e3673458eb83338d
4
+ data.tar.gz: 6e394c9017964515be4e8970899fe697480600753ca6e81db78d34faed7c982b
5
5
  SHA512:
6
- metadata.gz: 4ca9c77f8b70024083b831a02d824c2fd0321ff3598234f085133b161ba46a2177e9279e80e22f2ca56da466b087c0c7ac3a62c6dec9a7f906bf03fd44f2eb47
7
- data.tar.gz: bcffa9f70194081780a0348f17e695b05f879eb029e4932baa224c3ca1d03e7fb531931f111bfdbcf452f772c0729e725cd3101779aa363408f859e63fd989f8
6
+ metadata.gz: e41290229c5e5bc7d65179783b6b1ec0403ab987433c5b65535c10d3fee00a42440626f26f2a8ffd02d69a63b64684780165cc4398baf873842e20731570a076
7
+ data.tar.gz: b93594502a2dff93be5b0bff277f49431c40cae4a77a263acfac14370c855dbd424ac36156b832ec2f2ef1f892e44c0c7aaf84d6b567d772220da0ec991357a5
data/.rubocop.yml CHANGED
@@ -1,13 +1,14 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.6
3
3
  NewCops: enable
4
+ SuggestExtensions: false
4
5
  Style/Documentation:
5
6
  Enabled: false
6
7
  Style/ExponentialNotation:
7
8
  Enabled: true
8
9
  Metrics/BlockLength:
9
10
  Exclude:
10
- - 'spec/**/*.rb'
11
- - '*.gemspec'
11
+ - "spec/**/*.rb"
12
+ - "*.gemspec"
12
13
  Metrics/MethodLength:
13
14
  Max: 20
data/CHANGELOG.md CHANGED
@@ -1,7 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.0
4
+ - Handle custom x-handler field in the API description to find a handler method not based on operationId
5
+ - Add `resolver` option to provide a custom resolver to find a handler method
6
+
3
7
  ## 0.13.3
4
8
  - Better error message if string does not match format
9
+ - readOnly and writeOnly just works when used inside allOf
5
10
 
6
11
  ## 0.13.2
7
12
  - Return indicator (`source: { parameter: 'list/1' }`) in error response body when array item in query parameter is invalid
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (0.13.3)
4
+ openapi_first (0.14.0)
5
5
  deep_merge (>= 1.2.1)
6
6
  hanami-router (~> 2.0.alpha3)
7
7
  hanami-utils (~> 2.0.alpha1)
@@ -13,51 +13,53 @@ PATH
13
13
  GEM
14
14
  remote: https://rubygems.org/
15
15
  specs:
16
- activesupport (6.0.3.4)
16
+ activesupport (6.1.4)
17
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
- i18n (>= 0.7, < 2)
19
- minitest (~> 5.1)
20
- tzinfo (~> 1.1)
21
- zeitwerk (~> 2.2, >= 2.2.2)
22
- addressable (2.7.0)
18
+ i18n (>= 1.6, < 2)
19
+ minitest (>= 5.1)
20
+ tzinfo (~> 2.0)
21
+ zeitwerk (~> 2.3)
22
+ addressable (2.8.0)
23
23
  public_suffix (>= 2.0.2, < 5.0)
24
- ast (2.4.1)
24
+ ast (2.4.2)
25
25
  builder (3.2.4)
26
26
  coderay (1.1.3)
27
- concurrent-ruby (1.1.7)
27
+ concurrent-ruby (1.1.9)
28
28
  deep_merge (1.2.1)
29
29
  diff-lcs (1.4.4)
30
- ecma-re-validator (0.2.1)
31
- regexp_parser (~> 1.2)
32
- hana (1.3.6)
33
- hanami-router (2.0.0.alpha3)
30
+ dry-transformer (0.1.1)
31
+ ecma-re-validator (0.3.0)
32
+ regexp_parser (~> 2.0)
33
+ hana (1.3.7)
34
+ hanami-router (2.0.0.alpha5)
34
35
  mustermann (~> 1.0)
35
36
  mustermann-contrib (~> 1.0)
36
37
  rack (~> 2.0)
37
- hanami-utils (2.0.0.alpha1)
38
+ hanami-utils (2.0.0.alpha2)
38
39
  concurrent-ruby (~> 1.0)
39
- transproc (~> 1.0)
40
+ dry-transformer (~> 0.1)
40
41
  hansi (0.2.0)
41
42
  hash-deep-merge (0.1.1)
42
- i18n (1.8.5)
43
+ i18n (1.8.10)
43
44
  concurrent-ruby (~> 1.0)
44
- json_schemer (0.2.16)
45
- ecma-re-validator (~> 0.2)
45
+ json_schemer (0.2.18)
46
+ ecma-re-validator (~> 0.3)
46
47
  hana (~> 1.3)
47
- regexp_parser (~> 1.5)
48
+ regexp_parser (~> 2.0)
48
49
  uri_template (~> 0.7)
49
50
  method_source (1.0.0)
50
- mini_portile2 (2.4.0)
51
- minitest (5.14.2)
51
+ mini_portile2 (2.6.1)
52
+ minitest (5.14.4)
52
53
  multi_json (1.15.0)
53
54
  mustermann (1.1.1)
54
55
  ruby2_keywords (~> 0.0.1)
55
56
  mustermann-contrib (1.1.1)
56
57
  hansi (~> 0.2.0)
57
58
  mustermann (= 1.1.1)
58
- nokogiri (1.10.10)
59
- mini_portile2 (~> 2.4.0)
60
- oas_parser (0.25.1)
59
+ nokogiri (1.12.2)
60
+ mini_portile2 (~> 2.6.1)
61
+ racc (~> 1.4)
62
+ oas_parser (0.25.4)
61
63
  activesupport (>= 4.0.0)
62
64
  addressable (~> 2.3)
63
65
  builder (~> 3.2.3)
@@ -65,53 +67,52 @@ GEM
65
67
  hash-deep-merge
66
68
  mustermann-contrib (~> 1.1.1)
67
69
  nokogiri
68
- parallel (1.19.2)
69
- parser (2.7.2.0)
70
+ parallel (1.20.1)
71
+ parser (3.0.2.0)
70
72
  ast (~> 2.4.1)
71
- pry (0.13.1)
73
+ pry (0.14.1)
72
74
  coderay (~> 1.1)
73
75
  method_source (~> 1.0)
74
76
  public_suffix (4.0.6)
77
+ racc (1.5.2)
75
78
  rack (2.2.3)
76
79
  rack-test (1.1.0)
77
80
  rack (>= 1.0, < 3)
78
81
  rainbow (3.0.0)
79
- rake (13.0.1)
80
- regexp_parser (1.8.2)
81
- rexml (3.2.4)
82
- rspec (3.9.0)
83
- rspec-core (~> 3.9.0)
84
- rspec-expectations (~> 3.9.0)
85
- rspec-mocks (~> 3.9.0)
86
- rspec-core (3.9.3)
87
- rspec-support (~> 3.9.3)
88
- rspec-expectations (3.9.2)
82
+ rake (13.0.6)
83
+ regexp_parser (2.1.1)
84
+ rexml (3.2.5)
85
+ rspec (3.10.0)
86
+ rspec-core (~> 3.10.0)
87
+ rspec-expectations (~> 3.10.0)
88
+ rspec-mocks (~> 3.10.0)
89
+ rspec-core (3.10.1)
90
+ rspec-support (~> 3.10.0)
91
+ rspec-expectations (3.10.1)
89
92
  diff-lcs (>= 1.2.0, < 2.0)
90
- rspec-support (~> 3.9.0)
91
- rspec-mocks (3.9.1)
93
+ rspec-support (~> 3.10.0)
94
+ rspec-mocks (3.10.2)
92
95
  diff-lcs (>= 1.2.0, < 2.0)
93
- rspec-support (~> 3.9.0)
94
- rspec-support (3.9.3)
95
- rubocop (0.93.1)
96
+ rspec-support (~> 3.10.0)
97
+ rspec-support (3.10.2)
98
+ rubocop (1.18.4)
96
99
  parallel (~> 1.10)
97
- parser (>= 2.7.1.5)
100
+ parser (>= 3.0.0.0)
98
101
  rainbow (>= 2.2.2, < 4.0)
99
- regexp_parser (>= 1.8)
102
+ regexp_parser (>= 1.8, < 3.0)
100
103
  rexml
101
- rubocop-ast (>= 0.6.0)
104
+ rubocop-ast (>= 1.8.0, < 2.0)
102
105
  ruby-progressbar (~> 1.7)
103
- unicode-display_width (>= 1.4.0, < 2.0)
104
- rubocop-ast (0.8.0)
105
- parser (>= 2.7.1.5)
106
- ruby-progressbar (1.10.1)
107
- ruby2_keywords (0.0.2)
108
- thread_safe (0.3.6)
109
- transproc (1.1.1)
110
- tzinfo (1.2.8)
111
- thread_safe (~> 0.1)
112
- unicode-display_width (1.7.0)
106
+ unicode-display_width (>= 1.4.0, < 3.0)
107
+ rubocop-ast (1.9.0)
108
+ parser (>= 3.0.1.1)
109
+ ruby-progressbar (1.11.0)
110
+ ruby2_keywords (0.0.5)
111
+ tzinfo (2.0.4)
112
+ concurrent-ruby (~> 1.0)
113
+ unicode-display_width (2.0.0)
113
114
  uri_template (0.7.0)
114
- zeitwerk (2.4.1)
115
+ zeitwerk (2.4.2)
115
116
 
116
117
  PLATFORMS
117
118
  ruby
@@ -126,4 +127,4 @@ DEPENDENCIES
126
127
  rubocop
127
128
 
128
129
  BUNDLED WITH
129
- 2.1.4
130
+ 2.2.3
data/README.md CHANGED
@@ -29,9 +29,9 @@ You always have to add this middleware first in order to make the other middlewa
29
29
  use OpenapiFirst::Router, spec: OpenapiFirst.load('./openapi/openapi.yaml')
30
30
  ```
31
31
 
32
- This middleware adds `env[OpenapiFirst::OPERATION]` which holds an Operation object that responds to `operation_id` and `path`.
32
+ This middleware adds `env[OpenapiFirst::OPERATION]` which holds an Operation object that responds to `#operation_id`, `#path` (and `#[]` to access raw fields).
33
33
 
34
- Options and their defaults:
34
+ ### Options and defaults
35
35
 
36
36
  | Name | Possible values | Description | Default
37
37
  |:---|---|---|---|
@@ -48,7 +48,7 @@ use OpenapiFirst::RequestValidation
48
48
  ```
49
49
 
50
50
 
51
- Options and their defaults:
51
+ ### Options and defaults
52
52
 
53
53
  | Name | Possible values | Description | Default
54
54
  |:---|---|---|---|
@@ -109,16 +109,19 @@ Response validation fails if response body includes a property with `writeOnly:
109
109
 
110
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.
111
111
 
112
- Currently there are no customization options for this part. Please [share your ideas](#contributing) on how to best meet your needs and preferred style.
113
-
114
112
  ```ruby
115
113
  run OpenapiFirst::Responder, spec: OpenapiFirst.load('./openapi/openapi.yaml')
116
114
  ```
117
115
 
116
+ ### Options
118
117
  | Name | Description
119
118
  |:---|---|
120
- |`spec:`| The spec loaded via `OpenapiFirst.load` |
121
- | `namespace:` | A class or module where to find the handler method. |
119
+ | `namespace:` | Optional. A class or module where to find the handler method. |
120
+ | `resolver:` | Optional. An object that responds to `#call(operation)` and returns a (handler)[#handler]. By default this is an instance of [DefaultOperationResolver](#OpenapiFirst::DefaultOperationResolver) |
121
+
122
+
123
+ ### OpenapiFirst::DefaultOperationResolver
124
+ This is the default way to look up a handler method for an operation. Handlers are always looked up in a namespace module that needs to be specified.
122
125
 
123
126
  It works like this:
124
127
 
@@ -176,7 +179,7 @@ The above will use the mentioned Rack middlewares to:
176
179
  - Map the request to a method call `Pets.find_pet` based on the `operationId` in the API description
177
180
  - Set the response content type according to your spec (here with the default status code `200`)
178
181
 
179
- ### Options and their defaults:
182
+ ### Options and defaults
180
183
 
181
184
  | Name | Possible values | Description | Default
182
185
  |:---|---|---|---|
@@ -185,6 +188,7 @@ The above will use the mentioned Rack middlewares to:
185
188
  | `response_validation:` | `true`, `false` | If set to true it raises an exception if the response is invalid. This is useful during testing. | `false`
186
189
  | `router_raise_error:` | `true`, `false` | If set to true it raises an exception (subclass of `OpenapiFirst::Error` when a request path/method is not specified. This is useful during testing. | `false`
187
190
  | `request_validation_raise_error:` | `true`, `false` | If set to true it raises an exception (subclass of `OpenapiFirst::Error` when a request is not valid. | `false`
191
+ | `resolver:` | | Option to customize finding the [handler](#handler) method for an operation. See [OpenapiFirst::Responder](#OpenapiFirst::Responder) for details.
188
192
 
189
193
 
190
194
  Handler functions (`find_pet`) are called with two arguments:
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- openapi_first (0.13.3)
4
+ openapi_first (0.14.0)
5
5
  deep_merge (>= 1.2.1)
6
6
  hanami-router (~> 2.0.alpha3)
7
7
  hanami-utils (~> 2.0.alpha1)
@@ -13,78 +13,75 @@ PATH
13
13
  GEM
14
14
  remote: https://rubygems.org/
15
15
  specs:
16
- activesupport (6.0.3.4)
16
+ activesupport (6.1.4)
17
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
- i18n (>= 0.7, < 2)
19
- minitest (~> 5.1)
20
- tzinfo (~> 1.1)
21
- zeitwerk (~> 2.2, >= 2.2.2)
22
- addressable (2.7.0)
18
+ i18n (>= 1.6, < 2)
19
+ minitest (>= 5.1)
20
+ tzinfo (~> 2.0)
21
+ zeitwerk (~> 2.3)
22
+ addressable (2.8.0)
23
23
  public_suffix (>= 2.0.2, < 5.0)
24
- benchmark-ips (2.8.3)
24
+ benchmark-ips (2.9.1)
25
25
  benchmark-memory (0.1.2)
26
26
  memory_profiler (~> 0.9)
27
27
  builder (3.2.4)
28
- committee (4.2.0)
28
+ committee (4.4.0)
29
29
  json_schema (~> 0.14, >= 0.14.3)
30
- openapi_parser (>= 0.11.1)
30
+ openapi_parser (>= 0.11.1, < 1.0)
31
31
  rack (>= 1.5)
32
- concurrent-ruby (1.1.7)
32
+ concurrent-ruby (1.1.9)
33
33
  deep_merge (1.2.1)
34
- dry-configurable (0.11.6)
34
+ dry-configurable (0.12.1)
35
35
  concurrent-ruby (~> 1.0)
36
- dry-core (~> 0.4, >= 0.4.7)
37
- dry-equalizer (~> 0.2)
38
- dry-container (0.7.2)
36
+ dry-core (~> 0.5, >= 0.5.0)
37
+ dry-container (0.8.0)
39
38
  concurrent-ruby (~> 1.0)
40
39
  dry-configurable (~> 0.1, >= 0.1.3)
41
- dry-core (0.4.9)
40
+ dry-core (0.7.1)
42
41
  concurrent-ruby (~> 1.0)
43
- dry-equalizer (0.3.0)
44
- dry-inflector (0.2.0)
45
- dry-logic (1.0.8)
42
+ dry-inflector (0.2.1)
43
+ dry-logic (1.2.0)
46
44
  concurrent-ruby (~> 1.0)
47
- dry-core (~> 0.2)
48
- dry-equalizer (~> 0.2)
49
- dry-types (1.4.0)
45
+ dry-core (~> 0.5, >= 0.5)
46
+ dry-transformer (0.1.1)
47
+ dry-types (1.5.1)
50
48
  concurrent-ruby (~> 1.0)
51
49
  dry-container (~> 0.3)
52
- dry-core (~> 0.4, >= 0.4.4)
53
- dry-equalizer (~> 0.3)
50
+ dry-core (~> 0.5, >= 0.5)
54
51
  dry-inflector (~> 0.1, >= 0.1.2)
55
52
  dry-logic (~> 1.0, >= 1.0.2)
56
- ecma-re-validator (0.2.1)
57
- regexp_parser (~> 1.2)
58
- grape (1.5.0)
53
+ ecma-re-validator (0.3.0)
54
+ regexp_parser (~> 2.0)
55
+ grape (1.5.3)
59
56
  activesupport
60
57
  builder
61
58
  dry-types (>= 1.1)
62
59
  mustermann-grape (~> 1.0.0)
63
60
  rack (>= 1.3.0)
64
61
  rack-accept
65
- hana (1.3.6)
66
- hanami-api (0.1.1)
62
+ hana (1.3.7)
63
+ hanami-api (0.2.0)
67
64
  hanami-router (~> 2.0.alpha)
68
- hanami-router (2.0.0.alpha3)
65
+ hanami-router (2.0.0.alpha5)
69
66
  mustermann (~> 1.0)
70
67
  mustermann-contrib (~> 1.0)
71
68
  rack (~> 2.0)
72
- hanami-utils (2.0.0.alpha1)
69
+ hanami-utils (2.0.0.alpha2)
73
70
  concurrent-ruby (~> 1.0)
74
- transproc (~> 1.0)
71
+ dry-transformer (~> 0.1)
75
72
  hansi (0.2.0)
76
73
  hash-deep-merge (0.1.1)
77
- i18n (1.8.5)
74
+ i18n (1.8.10)
78
75
  concurrent-ruby (~> 1.0)
79
- json_schema (0.20.9)
80
- json_schemer (0.2.16)
81
- ecma-re-validator (~> 0.2)
76
+ json_schema (0.21.0)
77
+ json_schemer (0.2.18)
78
+ ecma-re-validator (~> 0.3)
82
79
  hana (~> 1.3)
83
- regexp_parser (~> 1.5)
80
+ regexp_parser (~> 2.0)
84
81
  uri_template (~> 0.7)
85
82
  memory_profiler (0.9.14)
86
- mini_portile2 (2.4.0)
87
- minitest (5.14.2)
83
+ mini_portile2 (2.6.1)
84
+ minitest (5.14.4)
88
85
  multi_json (1.15.0)
89
86
  mustermann (1.1.1)
90
87
  ruby2_keywords (~> 0.0.1)
@@ -93,9 +90,10 @@ GEM
93
90
  mustermann (= 1.1.1)
94
91
  mustermann-grape (1.0.1)
95
92
  mustermann (>= 1.0.0)
96
- nokogiri (1.10.10)
97
- mini_portile2 (~> 2.4.0)
98
- oas_parser (0.25.2)
93
+ nokogiri (1.12.2)
94
+ mini_portile2 (~> 2.6.1)
95
+ racc (~> 1.4)
96
+ oas_parser (0.25.4)
99
97
  activesupport (>= 4.0.0)
100
98
  addressable (~> 2.3)
101
99
  builder (~> 3.2.3)
@@ -103,31 +101,30 @@ GEM
103
101
  hash-deep-merge
104
102
  mustermann-contrib (~> 1.1.1)
105
103
  nokogiri
106
- openapi_parser (0.12.1)
104
+ openapi_parser (0.14.1)
107
105
  public_suffix (4.0.6)
106
+ racc (1.5.2)
108
107
  rack (2.2.3)
109
108
  rack-accept (0.4.5)
110
109
  rack (>= 0.4)
111
110
  rack-protection (2.1.0)
112
111
  rack
113
- regexp_parser (1.8.2)
114
- ruby2_keywords (0.0.2)
112
+ regexp_parser (2.1.1)
113
+ ruby2_keywords (0.0.5)
115
114
  seg (1.2.0)
116
115
  sinatra (2.1.0)
117
116
  mustermann (~> 1.0)
118
117
  rack (~> 2.2)
119
118
  rack-protection (= 2.1.0)
120
119
  tilt (~> 2.0)
121
- syro (3.2.0)
120
+ syro (3.2.1)
122
121
  rack (>= 1.6.0)
123
122
  seg
124
- thread_safe (0.3.6)
125
123
  tilt (2.0.10)
126
- transproc (1.1.1)
127
- tzinfo (1.2.7)
128
- thread_safe (~> 0.1)
124
+ tzinfo (2.0.4)
125
+ concurrent-ruby (~> 1.0)
129
126
  uri_template (0.7.0)
130
- zeitwerk (2.4.0)
127
+ zeitwerk (2.4.2)
131
128
 
132
129
  PLATFORMS
133
130
  ruby
@@ -145,4 +142,4 @@ DEPENDENCIES
145
142
  syro
146
143
 
147
144
  BUNDLED WITH
148
- 2.1.4
145
+ 2.2.3
@@ -19,6 +19,8 @@ app = Class.new(Hanami::API) do
19
19
  end
20
20
  end.new
21
21
 
22
- use Committee::Middleware::RequestValidation, schema_path: './apps/openapi.yaml'
22
+ use Committee::Middleware::RequestValidation,
23
+ schema_path: './apps/openapi.yaml',
24
+ parse_response_by_content_type: true
23
25
 
24
26
  run app
@@ -19,28 +19,25 @@ apps = Dir['./apps/*.ru'].each_with_object({}) do |config, hash|
19
19
  end
20
20
  apps.freeze
21
21
 
22
+ bench = lambda do |app|
23
+ examples.each do |example|
24
+ env, expected_status = example
25
+ 100.times { app.call(env) }
26
+ response = app.call(env)
27
+ raise unless response[0] == expected_status
28
+ end
29
+ end
30
+
22
31
  Benchmark.ips do |x|
23
32
  apps.each do |config, app|
24
- x.report(config) do
25
- examples.each do |example|
26
- env, expected_status = example
27
- response = app.call(env)
28
- raise unless response[0] == expected_status
29
- end
30
- end
33
+ x.report(config) { bench.call(app) }
31
34
  end
32
35
  x.compare!
33
36
  end
34
37
 
35
38
  Benchmark.memory do |x|
36
39
  apps.each do |config, app|
37
- x.report(config) do
38
- examples.each do |example|
39
- env, expected_status = example
40
- response = app.call(env)
41
- raise unless response[0] == expected_status
42
- end
43
- end
40
+ x.report(config) { bench.call(app) }
44
41
  end
45
42
  x.compare!
46
43
  end
data/lib/openapi_first.rb CHANGED
@@ -25,10 +25,9 @@ module OpenapiFirst
25
25
 
26
26
  def self.load(spec_path, only: nil)
27
27
  content = YAML.load_file(spec_path)
28
- raw = OasParser::Parser.new(spec_path, content).resolve
29
- raw['paths'].filter!(&->(key, _) { only.call(key) }) if only
30
- parsed = OasParser::Definition.new(raw, spec_path)
31
- Definition.new(parsed)
28
+ resolved = OasParser::Parser.new(spec_path, content).resolve
29
+ resolved['paths'].filter!(&->(key, _) { only.call(key) }) if only
30
+ Definition.new(resolved, spec_path)
32
31
  end
33
32
 
34
33
  def self.app(
@@ -78,11 +77,17 @@ module OpenapiFirst
78
77
  end
79
78
 
80
79
  class Error < StandardError; end
80
+
81
81
  class NotFoundError < Error; end
82
+
82
83
  class NotImplementedError < RuntimeError; end
84
+
83
85
  class ResponseInvalid < Error; end
86
+
84
87
  class ResponseCodeNotFoundError < ResponseInvalid; end
88
+
85
89
  class ResponseContentTypeNotFoundError < ResponseInvalid; end
90
+
86
91
  class ResponseBodyInvalidError < ResponseInvalid; end
87
92
 
88
93
  class RequestInvalidError < Error
@@ -11,17 +11,15 @@ module OpenapiFirst
11
11
  namespace:,
12
12
  router_raise_error: false,
13
13
  request_validation_raise_error: false,
14
- response_validation: false
14
+ response_validation: false,
15
+ resolver: nil
15
16
  )
16
17
  @stack = Rack::Builder.app do
17
18
  freeze_app
18
19
  use OpenapiFirst::Router, spec: spec, raise_error: router_raise_error, parent_app: parent_app
19
20
  use OpenapiFirst::RequestValidation, raise_error: request_validation_raise_error
20
21
  use OpenapiFirst::ResponseValidation if response_validation
21
- run OpenapiFirst::Responder.new(
22
- spec: spec,
23
- namespace: namespace
24
- )
22
+ run OpenapiFirst::Responder.new(namespace: namespace, resolver: resolver)
25
23
  end
26
24
  end
27
25
 
@@ -3,20 +3,14 @@
3
3
  require_relative 'utils'
4
4
 
5
5
  module OpenapiFirst
6
- class FindHandler
7
- def initialize(spec, namespace)
6
+ class DefaultOperationResolver
7
+ def initialize(namespace)
8
8
  @namespace = namespace
9
- @handlers = spec.operations.each_with_object({}) do |operation, hash|
10
- operation_id = operation.operation_id
11
- handler = find_handler(operation_id)
12
- next if handler.nil?
13
-
14
- hash[operation_id] = handler
15
- end
9
+ @handlers = {}
16
10
  end
17
11
 
18
- def [](operation_id)
19
- @handlers[operation_id]
12
+ def call(operation)
13
+ @handlers[operation.name] ||= find_handler(operation['x-handler'] || operation['operationId'])
20
14
  end
21
15
 
22
16
  def find_handler(operation_id)
@@ -6,9 +6,14 @@ module OpenapiFirst
6
6
  class Definition
7
7
  attr_reader :filepath, :operations
8
8
 
9
- def initialize(parsed)
10
- @filepath = parsed.path
11
- @operations = parsed.endpoints.map { |e| Operation.new(e) }
9
+ def initialize(resolved, filepath)
10
+ @filepath = filepath
11
+ methods = %w[get head post put patch delete trace options]
12
+ @operations = resolved['paths'].flat_map do |path, path_item|
13
+ path_item.slice(*methods).map do |request_method, _operation_object|
14
+ Operation.new(path, request_method, path_item)
15
+ end
16
+ end
12
17
  end
13
18
  end
14
19
  end
@@ -7,23 +7,25 @@ require_relative 'utils'
7
7
  require_relative 'response_object'
8
8
 
9
9
  module OpenapiFirst
10
- class Operation
10
+ class Operation # rubocop:disable Metrics/ClassLength
11
11
  extend Forwardable
12
- def_delegators :@operation,
13
- :parameters,
14
- :method,
15
- :request_body,
16
- :operation_id
12
+ def_delegators :operation_object,
13
+ :[],
14
+ :dig
17
15
 
18
16
  WRITE_METHODS = Set.new(%w[post put patch delete]).freeze
19
17
  private_constant :WRITE_METHODS
20
18
 
21
- def initialize(parsed)
22
- @operation = parsed
19
+ attr_reader :path, :method
20
+
21
+ def initialize(path, request_method, path_item_object)
22
+ @path = path
23
+ @method = request_method
24
+ @path_item_object = path_item_object
23
25
  end
24
26
 
25
- def path
26
- @operation.path.path
27
+ def operation_id
28
+ operation_object['operationId']
27
29
  end
28
30
 
29
31
  def read?
@@ -34,6 +36,10 @@ module OpenapiFirst
34
36
  WRITE_METHODS.include?(method)
35
37
  end
36
38
 
39
+ def request_body
40
+ operation_object['requestBody']
41
+ end
42
+
37
43
  def parameters_schema
38
44
  @parameters_schema ||= begin
39
45
  parameters_json_schema = build_parameters_json_schema
@@ -53,6 +59,7 @@ module OpenapiFirst
53
59
  raise ResponseInvalid, "Response has no content-type for '#{name}'" unless content_type
54
60
 
55
61
  media_type = find_content_for_content_type(content, content_type)
62
+
56
63
  unless media_type
57
64
  message = "Response content type not found '#{content_type}' for '#{name}'"
58
65
  raise ResponseContentTypeNotFoundError, message
@@ -62,7 +69,7 @@ module OpenapiFirst
62
69
  end
63
70
 
64
71
  def request_body_schema(request_content_type)
65
- content = @operation.request_body.content
72
+ content = operation_object.dig('requestBody', 'content')
66
73
  media_type = find_content_for_content_type(content, request_content_type)
67
74
  schema = media_type&.fetch('schema', nil)
68
75
  return unless schema
@@ -71,8 +78,9 @@ module OpenapiFirst
71
78
  end
72
79
 
73
80
  def response_for(status)
74
- @operation.response_by_code(status.to_s, use_default: true).raw
75
- rescue OasParser::ResponseCodeNotFound
81
+ response_content = response_by_code(status)
82
+ return response_content if response_content
83
+
76
84
  message = "Response status code or default not found: #{status} for '#{name}'"
77
85
  raise OpenapiFirst::ResponseCodeNotFoundError, message
78
86
  end
@@ -83,6 +91,15 @@ module OpenapiFirst
83
91
 
84
92
  private
85
93
 
94
+ def response_by_code(status)
95
+ operation_object.dig('responses', status.to_s) ||
96
+ operation_object.dig('responses', 'default')
97
+ end
98
+
99
+ def operation_object
100
+ @path_item_object[method]
101
+ end
102
+
86
103
  def find_content_for_content_type(content, request_content_type)
87
104
  content.fetch(request_content_type) do |_|
88
105
  type = request_content_type.split(';')[0]
@@ -91,24 +108,32 @@ module OpenapiFirst
91
108
  end
92
109
 
93
110
  def build_parameters_json_schema
94
- return unless @operation.parameters&.any?
111
+ parameters = all_parameters
112
+ return unless parameters&.any?
95
113
 
96
- @operation.parameters.each_with_object(new_node) do |parameter, schema|
97
- params = Rack::Utils.parse_nested_query(parameter.name)
114
+ parameters.each_with_object(new_node) do |parameter, schema|
115
+ params = Rack::Utils.parse_nested_query(parameter['name'])
98
116
  generate_schema(schema, params, parameter)
99
117
  end
100
118
  end
101
119
 
120
+ def all_parameters
121
+ parameters = @path_item_object['parameters'] || []
122
+ parameters_on_operation = operation_object['parameters']
123
+ parameters.concat(parameters_on_operation) if parameters_on_operation
124
+ parameters
125
+ end
126
+
102
127
  def generate_schema(schema, params, parameter)
103
128
  required = Set.new(schema['required'])
104
129
  params.each do |key, value|
105
- required << key if parameter.required
130
+ required << key if parameter['required']
106
131
  if value.is_a? Hash
107
132
  property_schema = new_node
108
133
  generate_schema(property_schema, value, parameter)
109
134
  Utils.deep_merge!(schema['properties'], { key => property_schema })
110
135
  else
111
- schema['properties'][key] = parameter.schema
136
+ schema['properties'][key] = parameter['schema']
112
137
  end
113
138
  end
114
139
  schema['required'] = required.to_a
@@ -63,13 +63,13 @@ module OpenapiFirst
63
63
  end
64
64
 
65
65
  def validate_request_content_type!(content_type, operation)
66
- return if operation.request_body.content[content_type]
66
+ return if operation.request_body.dig('content', content_type)
67
67
 
68
68
  halt_with_error(415)
69
69
  end
70
70
 
71
71
  def validate_request_body_presence!(body, operation)
72
- return unless operation.request_body.required && body.empty?
72
+ return unless operation.request_body['required'] && body.empty?
73
73
 
74
74
  halt_with_error(415, 'Request body is required')
75
75
  end
@@ -2,12 +2,12 @@
2
2
 
3
3
  require 'rack'
4
4
  require_relative 'inbox'
5
- require_relative 'find_handler'
5
+ require_relative 'default_operation_resolver'
6
6
 
7
7
  module OpenapiFirst
8
8
  class Responder
9
- def initialize(spec:, namespace:, resolver: FindHandler.new(spec, namespace))
10
- @resolver = resolver
9
+ def initialize(namespace: nil, resolver: nil)
10
+ @resolver = resolver || DefaultOperationResolver.new(namespace)
11
11
  @namespace = namespace
12
12
  end
13
13
 
@@ -24,7 +24,7 @@ module OpenapiFirst
24
24
  private
25
25
 
26
26
  def find_handler(operation)
27
- handler = @resolver[operation.operation_id]
27
+ handler = @resolver.call(operation)
28
28
  raise NotImplementedError, "Could not find handler for #{operation.name}" unless handler
29
29
 
30
30
  handler
@@ -40,7 +40,10 @@ module OpenapiFirst
40
40
 
41
41
  def raise_error(env)
42
42
  req = Rack::Request.new(env)
43
- msg = "Could not find definition for #{req.request_method} '#{req.path}' in API description #{@filepath}"
43
+ msg =
44
+ "Could not find definition for #{req.request_method} '#{
45
+ req.path
46
+ }' in API description #{@filepath}"
44
47
  raise NotFoundError, msg
45
48
  end
46
49
 
@@ -54,7 +57,7 @@ module OpenapiFirst
54
57
  end
55
58
 
56
59
  def build_router(operations) # rubocop:disable Metrics/AbcSize
57
- router = Hanami::Router.new {}
60
+ router = Hanami::Router.new
58
61
  operations.each do |operation|
59
62
  normalized_path = operation.path.gsub('{', ':').gsub('}', '')
60
63
  if operation.operation_id.nil?
@@ -19,6 +19,11 @@ module OpenapiFirst
19
19
  title: "has not a valid #{error.dig('schema', 'format')} format",
20
20
  detail: "#{error['data'].inspect} is not a valid #{error.dig('schema', 'format')} format"
21
21
  }
22
+ elsif error['type'] == 'enum'
23
+ {
24
+ title: "value #{error['data'].inspect} is not defined in enum",
25
+ detail: "value can be one of #{error.dig('schema', 'enum')&.join(', ')}"
26
+ }
22
27
  elsif error['type'] == 'required'
23
28
  missing_keys = error['details']['missing_keys']
24
29
  {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.13.3'
4
+ VERSION = '0.14.0'
5
5
  end
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.metadata['changelog_uri'] = 'https://github.com/ahx/openapi_first/blob/master/CHANGELOG.md'
21
21
  else
22
22
  raise 'RubyGems 2.0 or newer is required to protect against ' \
23
- 'public gem pushes.'
23
+ 'public gem pushes.'
24
24
  end
25
25
 
26
26
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
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.3
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-10 00:00:00.000000000 Z
11
+ date: 2021-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deep_merge
@@ -203,8 +203,8 @@ files:
203
203
  - lib/openapi_first.rb
204
204
  - lib/openapi_first/app.rb
205
205
  - lib/openapi_first/coverage.rb
206
+ - lib/openapi_first/default_operation_resolver.rb
206
207
  - lib/openapi_first/definition.rb
207
- - lib/openapi_first/find_handler.rb
208
208
  - lib/openapi_first/inbox.rb
209
209
  - lib/openapi_first/operation.rb
210
210
  - lib/openapi_first/request_validation.rb
@@ -242,7 +242,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
242
242
  - !ruby/object:Gem::Version
243
243
  version: '0'
244
244
  requirements: []
245
- rubygems_version: 3.1.2
245
+ rubygems_version: 3.2.3
246
246
  signing_key:
247
247
  specification_version: 4
248
248
  summary: Implement REST APIs based on OpenApi.