openapi_first 1.0.0.beta4 → 1.0.0.beta6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +2 -1
- data/CHANGELOG.md +13 -0
- data/Gemfile +2 -1
- data/Gemfile.lock +17 -22
- data/Gemfile.rack2 +15 -0
- data/README.md +17 -7
- data/lib/openapi_first/body_parser.rb +28 -0
- data/lib/openapi_first/config.rb +4 -3
- data/lib/openapi_first/definition/cookie_parameters.rb +12 -0
- data/lib/openapi_first/definition/has_content.rb +37 -0
- data/lib/openapi_first/definition/header_parameters.rb +12 -0
- data/lib/openapi_first/definition/operation.rb +103 -0
- data/lib/openapi_first/definition/parameters.rb +47 -0
- data/lib/openapi_first/definition/path_item.rb +23 -0
- data/lib/openapi_first/definition/path_parameters.rb +13 -0
- data/lib/openapi_first/definition/query_parameters.rb +12 -0
- data/lib/openapi_first/definition/request_body.rb +32 -0
- data/lib/openapi_first/definition/response.rb +37 -0
- data/lib/openapi_first/definition/schema/result.rb +17 -0
- data/lib/openapi_first/{schema_validation.rb → definition/schema.rb} +6 -6
- data/lib/openapi_first/definition.rb +26 -6
- data/lib/openapi_first/error_response.rb +28 -12
- data/lib/openapi_first/error_responses/default.rb +58 -0
- data/lib/openapi_first/error_responses/json_api.rb +58 -0
- data/lib/openapi_first/request_body_validator.rb +18 -22
- data/lib/openapi_first/request_validation.rb +68 -58
- data/lib/openapi_first/request_validation_error.rb +31 -0
- data/lib/openapi_first/response_validation.rb +33 -13
- data/lib/openapi_first/response_validator.rb +1 -0
- data/lib/openapi_first/router.rb +20 -62
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +2 -13
- data/openapi_first.gemspec +8 -5
- metadata +44 -57
- data/.rspec +0 -3
- data/.rubocop.yml +0 -14
- data/Rakefile +0 -15
- data/benchmarks/Gemfile +0 -16
- data/benchmarks/Gemfile.lock +0 -131
- data/benchmarks/README.md +0 -29
- data/benchmarks/apps/committee_with_hanami_api.ru +0 -26
- data/benchmarks/apps/committee_with_response_validation.ru +0 -29
- data/benchmarks/apps/committee_with_sinatra.ru +0 -31
- data/benchmarks/apps/grape.ru +0 -21
- data/benchmarks/apps/hanami_api.ru +0 -21
- data/benchmarks/apps/hanami_router.ru +0 -14
- data/benchmarks/apps/openapi.yaml +0 -268
- data/benchmarks/apps/openapi_first_with_hanami_api.ru +0 -24
- data/benchmarks/apps/openapi_first_with_plain_rack.ru +0 -32
- data/benchmarks/apps/openapi_first_with_response_validation.ru +0 -25
- data/benchmarks/apps/openapi_first_with_sinatra.ru +0 -29
- data/benchmarks/apps/roda.ru +0 -27
- data/benchmarks/apps/sinatra.ru +0 -26
- data/benchmarks/apps/syro.ru +0 -25
- data/benchmarks/benchmark-wrk.sh +0 -3
- data/benchmarks/benchmarks.rb +0 -48
- data/benchmarks/post.lua +0 -3
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/examples/README.md +0 -13
- data/examples/app.rb +0 -18
- data/examples/config.ru +0 -7
- data/examples/openapi.yaml +0 -29
- data/lib/openapi_first/body_parser_middleware.rb +0 -53
- data/lib/openapi_first/default_error_response.rb +0 -47
- data/lib/openapi_first/operation.rb +0 -142
- data/lib/openapi_first/operation_schemas.rb +0 -52
- data/lib/openapi_first/string_keyed_hash.rb +0 -20
- data/lib/openapi_first/validation_result.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8173c11075add6c2c7a1a6cf024ea86ba448b185574e346fe288afa3aafcf677
|
4
|
+
data.tar.gz: f3dcd95b9c06adcfcada5430a8412a3ee6ea37c1c117e79546436ade496c5778
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d29578cdcc1573ff8903a7060a85e2b334b1ff908d4b3b7973e64b336ba414bfdf7b6fb762aff48cf0feaf78f5a29403d2d063b117fb1278bed628195363dd74
|
7
|
+
data.tar.gz: 0a6409930941ea9a1f4b734f49752ec18f8e9779ce6f5279e2db65022066500d8b7f9067af23625e780e20cf0d62f542c0a4b8388fea6f7cc37f63cd372baaf5
|
data/.github/workflows/ruby.yml
CHANGED
@@ -9,4 +9,5 @@ jobs:
|
|
9
9
|
with:
|
10
10
|
ruby-version: '3.1'
|
11
11
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
12
|
-
- run: bundle exec rake
|
12
|
+
- run: BUNDLE_GEMFILE=Gemfile bundle exec rake
|
13
|
+
- run: BUNDLE_GEMFILE=Gemfile.rack2 bundle lock --add-platform x86_64-linux && bundle exec rake
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,19 @@
|
|
2
2
|
|
3
3
|
## Unreleased
|
4
4
|
|
5
|
+
## 1.0.0.beta6
|
6
|
+
- Fix: Make response header validation work with rack 3
|
7
|
+
- Refactor router
|
8
|
+
- Remove dependency hanami-router
|
9
|
+
- PathItem and Operation for a request can be found by calling methods on the Definitnion
|
10
|
+
- Fixed https://github.com/ahx/openapi_first/issues/155
|
11
|
+
- Breaking / Regression: A paths like /pets/{from}-{to} if there is a path "/pets/{id}"
|
12
|
+
|
13
|
+
## 1.0.0.beta5
|
14
|
+
|
15
|
+
- Added: `OpenapiFirst::Config.default_options=` to set default options globally
|
16
|
+
- Added: You can define custom error responses by subclassing `OpenapiFirst::ErrorResponse` and register it via `OpenapiFirst::Plugins.register_error_response(name, MyCustomErrorResponse)`
|
17
|
+
|
5
18
|
## 1.0.0.beta4
|
6
19
|
|
7
20
|
- Update json_schemer to version 2.0
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,25 +1,20 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
openapi_first (1.0.0.
|
5
|
-
hanami-router (~> 2.0.0)
|
4
|
+
openapi_first (1.0.0.beta6)
|
6
5
|
json_refs (~> 0.1, >= 0.1.7)
|
7
6
|
json_schemer (~> 2.0.0)
|
8
|
-
multi_json (~> 1.
|
9
|
-
|
7
|
+
multi_json (~> 1.15)
|
8
|
+
mustermann-contrib (~> 3.0.0)
|
9
|
+
openapi_parameters (>= 0.3.2, < 2.0)
|
10
10
|
rack (>= 2.2, < 4.0)
|
11
11
|
|
12
12
|
GEM
|
13
13
|
remote: https://rubygems.org/
|
14
14
|
specs:
|
15
15
|
ast (2.4.2)
|
16
|
-
base64 (0.1.1)
|
17
16
|
diff-lcs (1.5.0)
|
18
17
|
hana (1.3.7)
|
19
|
-
hanami-router (2.0.2)
|
20
|
-
mustermann (~> 3.0)
|
21
|
-
mustermann-contrib (~> 3.0)
|
22
|
-
rack (~> 2.0)
|
23
18
|
hansi (0.2.1)
|
24
19
|
json (2.6.3)
|
25
20
|
json_refs (0.1.8)
|
@@ -35,20 +30,20 @@ GEM
|
|
35
30
|
mustermann-contrib (3.0.0)
|
36
31
|
hansi (~> 0.2.0)
|
37
32
|
mustermann (= 3.0.0)
|
38
|
-
openapi_parameters (0.
|
33
|
+
openapi_parameters (0.3.2)
|
39
34
|
rack (>= 2.2)
|
40
35
|
zeitwerk (~> 2.6)
|
41
36
|
parallel (1.23.0)
|
42
|
-
parser (3.2.2.
|
37
|
+
parser (3.2.2.4)
|
43
38
|
ast (~> 2.4.1)
|
44
39
|
racc
|
45
|
-
racc (1.7.
|
46
|
-
rack (
|
40
|
+
racc (1.7.3)
|
41
|
+
rack (3.0.8)
|
47
42
|
rack-test (2.1.0)
|
48
43
|
rack (>= 1.3)
|
49
44
|
rainbow (3.1.1)
|
50
|
-
rake (13.0
|
51
|
-
regexp_parser (2.8.
|
45
|
+
rake (13.1.0)
|
46
|
+
regexp_parser (2.8.2)
|
52
47
|
rexml (3.2.6)
|
53
48
|
rspec (3.12.0)
|
54
49
|
rspec-core (~> 3.12.0)
|
@@ -63,19 +58,18 @@ GEM
|
|
63
58
|
diff-lcs (>= 1.2.0, < 2.0)
|
64
59
|
rspec-support (~> 3.12.0)
|
65
60
|
rspec-support (3.12.1)
|
66
|
-
rubocop (1.
|
67
|
-
base64 (~> 0.1.1)
|
61
|
+
rubocop (1.57.2)
|
68
62
|
json (~> 2.3)
|
69
63
|
language_server-protocol (>= 3.17.0)
|
70
64
|
parallel (~> 1.10)
|
71
|
-
parser (>= 3.2.2.
|
65
|
+
parser (>= 3.2.2.4)
|
72
66
|
rainbow (>= 2.2.2, < 4.0)
|
73
67
|
regexp_parser (>= 1.8, < 3.0)
|
74
68
|
rexml (>= 3.2.5, < 4.0)
|
75
69
|
rubocop-ast (>= 1.28.1, < 2.0)
|
76
70
|
ruby-progressbar (~> 1.7)
|
77
71
|
unicode-display_width (>= 2.4.0, < 3.0)
|
78
|
-
rubocop-ast (1.
|
72
|
+
rubocop-ast (1.30.0)
|
79
73
|
parser (>= 3.2.1.0)
|
80
74
|
ruby-progressbar (1.13.0)
|
81
75
|
ruby2_keywords (0.0.5)
|
@@ -83,9 +77,9 @@ GEM
|
|
83
77
|
unf (~> 0.1.4)
|
84
78
|
unf (0.1.4)
|
85
79
|
unf_ext
|
86
|
-
unf_ext (0.0.
|
87
|
-
unicode-display_width (2.
|
88
|
-
zeitwerk (2.6.
|
80
|
+
unf_ext (0.0.9)
|
81
|
+
unicode-display_width (2.5.0)
|
82
|
+
zeitwerk (2.6.12)
|
89
83
|
|
90
84
|
PLATFORMS
|
91
85
|
arm64-darwin-21
|
@@ -94,6 +88,7 @@ PLATFORMS
|
|
94
88
|
DEPENDENCIES
|
95
89
|
bundler
|
96
90
|
openapi_first!
|
91
|
+
rack (>= 3.0.0)
|
97
92
|
rack-test
|
98
93
|
rake
|
99
94
|
rspec
|
data/Gemfile.rack2
ADDED
data/README.md
CHANGED
@@ -28,11 +28,11 @@ It adds these fields to the Rack env:
|
|
28
28
|
|
29
29
|
### Options and defaults
|
30
30
|
|
31
|
-
| Name
|
32
|
-
|
|
33
|
-
| `spec:`
|
34
|
-
| `raise_error:`
|
35
|
-
| `error_response
|
31
|
+
| Name | Possible values | Description | Default |
|
32
|
+
| :---------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------- |
|
33
|
+
| `spec:` | | The path to the spec file or spec loaded via `OpenapiFirst.load` |
|
34
|
+
| `raise_error:` | `false`, `true` | If set to true the middleware raises `OpenapiFirst::RequestInvalidError` instead of returning 4xx. | `false` (don't raise an exception) |
|
35
|
+
| `error_response:` | `:default`, `:json_api`, Your implementation of `ErrorResponse` | :default |
|
36
36
|
|
37
37
|
The error responses conform with [JSON:API](https://jsonapi.org).
|
38
38
|
|
@@ -40,7 +40,7 @@ Here's an example response body for a missing query parameter "search":
|
|
40
40
|
|
41
41
|
```json
|
42
42
|
http-status: 400
|
43
|
-
content-type: "application/
|
43
|
+
content-type: "application/json"
|
44
44
|
|
45
45
|
{
|
46
46
|
"errors": [
|
@@ -54,7 +54,6 @@ content-type: "application/vnd.api+json"
|
|
54
54
|
}
|
55
55
|
```
|
56
56
|
|
57
|
-
|
58
57
|
### Parameters
|
59
58
|
|
60
59
|
The `RequestValidation` middleware adds `env[OpenapiFirst::PARAMS]` (or `env['openapi.params']` ) with the converted query and path parameters. This only includes the parameters that are defined in the API description. It supports every [`style` and `explode` value as described](https://spec.openapis.org/oas/latest.html#style-examples) in the OpenAPI 3.0 and 3.1 specs. So you can do things these:
|
@@ -120,6 +119,17 @@ This middleware adds `env['openapi.operation']` which holds an instance of `Open
|
|
120
119
|
| `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) |
|
121
120
|
| `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) |
|
122
121
|
|
122
|
+
## Global configuration
|
123
|
+
|
124
|
+
You can configure default options gobally via `OpenapiFirst::Config`:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
OpenapiFirst::Config.default_options = {
|
128
|
+
error_response: :json_api,
|
129
|
+
request_validation_raise_error: true
|
130
|
+
}
|
131
|
+
```
|
132
|
+
|
123
133
|
## Alternatives
|
124
134
|
|
125
135
|
This gem is inspired by [committee](https://github.com/interagent/committee) (Ruby) and [connexion](https://github.com/zalando/connexion) (Python).
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'multi_json'
|
4
|
+
|
5
|
+
module OpenapiFirst
|
6
|
+
class BodyParser
|
7
|
+
def parse_body(env)
|
8
|
+
request = Rack::Request.new(env)
|
9
|
+
body = read_body(request)
|
10
|
+
return if body.empty?
|
11
|
+
|
12
|
+
return MultiJson.load(body) if request.media_type =~ (/json/i) && (request.media_type =~ /json/i)
|
13
|
+
return request.POST if request.form_data?
|
14
|
+
|
15
|
+
body
|
16
|
+
rescue MultiJson::ParseError
|
17
|
+
raise BodyParsingError, 'Failed to parse body as application/json'
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def read_body(request)
|
23
|
+
body = request.body.read
|
24
|
+
request.body.rewind
|
25
|
+
body
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/openapi_first/config.rb
CHANGED
@@ -2,11 +2,12 @@
|
|
2
2
|
|
3
3
|
module OpenapiFirst
|
4
4
|
class Config
|
5
|
-
def initialize(error_response: :default)
|
6
|
-
@error_response = error_response
|
5
|
+
def initialize(error_response: :default, request_validation_raise_error: false)
|
6
|
+
@error_response = Plugins.find_error_response(error_response)
|
7
|
+
@request_validation_raise_error = request_validation_raise_error
|
7
8
|
end
|
8
9
|
|
9
|
-
attr_reader :error_response
|
10
|
+
attr_reader :error_response, :request_validation_raise_error
|
10
11
|
|
11
12
|
def self.default_options
|
12
13
|
@default_options ||= new
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openapi_parameters'
|
4
|
+
require_relative 'parameters'
|
5
|
+
|
6
|
+
module OpenapiFirst
|
7
|
+
class CookieParameters < Parameters
|
8
|
+
def unpack(env)
|
9
|
+
OpenapiParameters::Cookie.new(@parameter_definitions).unpack(env['HTTP_COOKIE'])
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'schema'
|
4
|
+
|
5
|
+
module OpenapiFirst
|
6
|
+
module HasContent
|
7
|
+
def schema_for(content_type)
|
8
|
+
return unless content&.any?
|
9
|
+
|
10
|
+
content_schemas&.fetch(content_type) do
|
11
|
+
type = content_type.split(';')[0]
|
12
|
+
content_schemas[type] || content_schemas["#{type.split('/')[0]}/*"] || content_schemas['*/*']
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def content
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
def schema_write?
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
def content_schemas
|
27
|
+
@content_schemas ||= content&.each_with_object({}) do |kv, result|
|
28
|
+
type, media_type = kv
|
29
|
+
schema_object = media_type['schema']
|
30
|
+
next unless schema_object
|
31
|
+
|
32
|
+
result[type] = Schema.new(schema_object, write: schema_write?,
|
33
|
+
openapi_version: @operation.openapi_version)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openapi_parameters'
|
4
|
+
require_relative 'parameters'
|
5
|
+
|
6
|
+
module OpenapiFirst
|
7
|
+
class HeaderParameters < Parameters
|
8
|
+
def unpack(env)
|
9
|
+
OpenapiParameters::Header.new(@parameter_definitions).unpack_env(env)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'set'
|
5
|
+
require_relative 'request_body'
|
6
|
+
require_relative 'response'
|
7
|
+
require_relative 'query_parameters'
|
8
|
+
require_relative 'header_parameters'
|
9
|
+
require_relative 'path_parameters'
|
10
|
+
require_relative 'cookie_parameters'
|
11
|
+
require_relative 'schema'
|
12
|
+
|
13
|
+
module OpenapiFirst
|
14
|
+
class Operation
|
15
|
+
extend Forwardable
|
16
|
+
def_delegators :operation_object,
|
17
|
+
:[],
|
18
|
+
:dig
|
19
|
+
|
20
|
+
WRITE_METHODS = Set.new(%w[post put patch delete]).freeze
|
21
|
+
private_constant :WRITE_METHODS
|
22
|
+
|
23
|
+
attr_reader :path, :method, :openapi_version
|
24
|
+
|
25
|
+
def initialize(path, request_method, path_item_object, openapi_version:)
|
26
|
+
@path = path
|
27
|
+
@method = request_method
|
28
|
+
@path_item_object = path_item_object
|
29
|
+
@openapi_version = openapi_version
|
30
|
+
@operation_object = @path_item_object[request_method]
|
31
|
+
end
|
32
|
+
|
33
|
+
def operation_id
|
34
|
+
operation_object['operationId']
|
35
|
+
end
|
36
|
+
|
37
|
+
def read?
|
38
|
+
!write?
|
39
|
+
end
|
40
|
+
|
41
|
+
def write?
|
42
|
+
WRITE_METHODS.include?(method)
|
43
|
+
end
|
44
|
+
|
45
|
+
def request_body
|
46
|
+
@request_body ||= RequestBody.new(operation_object['requestBody'], self) if operation_object['requestBody']
|
47
|
+
end
|
48
|
+
|
49
|
+
def response_for(status)
|
50
|
+
response_object = operation_object.dig('responses', status.to_s) ||
|
51
|
+
operation_object.dig('responses', "#{status / 100}XX") ||
|
52
|
+
operation_object.dig('responses', "#{status / 100}xx") ||
|
53
|
+
operation_object.dig('responses', 'default')
|
54
|
+
Response.new(status, response_object, self) if response_object
|
55
|
+
end
|
56
|
+
|
57
|
+
def name
|
58
|
+
@name ||= "#{method.upcase} #{path} (#{operation_id})"
|
59
|
+
end
|
60
|
+
|
61
|
+
def query_parameters
|
62
|
+
@query_parameters ||= build_parameters(all_parameters.filter { |p| p['in'] == 'query' }, QueryParameters)
|
63
|
+
end
|
64
|
+
|
65
|
+
def path_parameters
|
66
|
+
@path_parameters ||= build_parameters(all_parameters.filter { |p| p['in'] == 'path' }, PathParameters)
|
67
|
+
end
|
68
|
+
|
69
|
+
IGNORED_HEADERS = Set['Content-Type', 'Accept', 'Authorization'].freeze
|
70
|
+
private_constant :IGNORED_HEADERS
|
71
|
+
|
72
|
+
def header_parameters
|
73
|
+
@header_parameters ||= build_parameters(find_header_parameters, HeaderParameters)
|
74
|
+
end
|
75
|
+
|
76
|
+
def cookie_parameters
|
77
|
+
@cookie_parameters ||= build_parameters(all_parameters.filter { |p| p['in'] == 'cookie' }, CookieParameters)
|
78
|
+
end
|
79
|
+
|
80
|
+
def all_parameters
|
81
|
+
@all_parameters ||= begin
|
82
|
+
parameters = @path_item_object['parameters']&.dup || []
|
83
|
+
parameters_on_operation = operation_object['parameters']
|
84
|
+
parameters.concat(parameters_on_operation) if parameters_on_operation
|
85
|
+
parameters
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
attr_reader :operation_object
|
92
|
+
|
93
|
+
def build_parameters(parameters, klass)
|
94
|
+
klass.new(parameters, openapi_version:) if parameters.any?
|
95
|
+
end
|
96
|
+
|
97
|
+
def find_header_parameters
|
98
|
+
all_parameters.filter do |p|
|
99
|
+
p['in'] == 'header' && !IGNORED_HEADERS.include?(p['name'])
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require_relative 'schema'
|
5
|
+
|
6
|
+
module OpenapiFirst
|
7
|
+
class Parameters
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def initialize(parameter_definitions, openapi_version:)
|
11
|
+
@parameter_definitions = parameter_definitions
|
12
|
+
@openapi_version = openapi_version
|
13
|
+
end
|
14
|
+
|
15
|
+
def_delegators :parameters, :map
|
16
|
+
|
17
|
+
def empty?
|
18
|
+
@parameter_definitions.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def schema
|
22
|
+
@schema ||= build_schema
|
23
|
+
end
|
24
|
+
|
25
|
+
def parameters
|
26
|
+
@parameter_definitions.map do |parameter_object|
|
27
|
+
OpenapiParameters::Parameter.new(parameter_object)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def build_schema
|
34
|
+
init_schema = {
|
35
|
+
'type' => 'object',
|
36
|
+
'properties' => {},
|
37
|
+
'required' => []
|
38
|
+
}
|
39
|
+
schema = @parameter_definitions.each_with_object(init_schema) do |parameter_def, result|
|
40
|
+
parameter = OpenapiParameters::Parameter.new(parameter_def)
|
41
|
+
result['properties'][parameter.name] = parameter.schema if parameter.schema
|
42
|
+
result['required'] << parameter.name if parameter.required?
|
43
|
+
end
|
44
|
+
Schema.new(schema, openapi_version: @openapi_version)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'operation'
|
4
|
+
|
5
|
+
module OpenapiFirst
|
6
|
+
class PathItem
|
7
|
+
def initialize(path, path_item_object, openapi_version:)
|
8
|
+
@path = path
|
9
|
+
@path_item_object = path_item_object
|
10
|
+
@openapi_version = openapi_version
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :path
|
14
|
+
|
15
|
+
def find_operation(request_method)
|
16
|
+
return unless @path_item_object[request_method]
|
17
|
+
|
18
|
+
Operation.new(
|
19
|
+
@path, request_method, @path_item_object, openapi_version: @openapi_version
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openapi_parameters'
|
4
|
+
require_relative 'parameters'
|
5
|
+
require_relative '../router'
|
6
|
+
|
7
|
+
module OpenapiFirst
|
8
|
+
class PathParameters < Parameters
|
9
|
+
def unpack(env)
|
10
|
+
OpenapiParameters::Path.new(@parameter_definitions).unpack(env[Router::RAW_PATH_PARAMS])
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openapi_parameters'
|
4
|
+
require_relative 'parameters'
|
5
|
+
|
6
|
+
module OpenapiFirst
|
7
|
+
class QueryParameters < Parameters
|
8
|
+
def unpack(env)
|
9
|
+
OpenapiParameters::Query.new(@parameter_definitions).unpack(env['QUERY_STRING'])
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'has_content'
|
4
|
+
|
5
|
+
module OpenapiFirst
|
6
|
+
class RequestBody
|
7
|
+
include HasContent
|
8
|
+
|
9
|
+
def initialize(request_body_object, operation)
|
10
|
+
@object = request_body_object
|
11
|
+
@operation = operation
|
12
|
+
end
|
13
|
+
|
14
|
+
def description
|
15
|
+
@object['description']
|
16
|
+
end
|
17
|
+
|
18
|
+
def required?
|
19
|
+
!!@object['required']
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def schema_write?
|
25
|
+
@operation.write?
|
26
|
+
end
|
27
|
+
|
28
|
+
def content
|
29
|
+
@object['content']
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'has_content'
|
4
|
+
|
5
|
+
module OpenapiFirst
|
6
|
+
class Response
|
7
|
+
include HasContent
|
8
|
+
|
9
|
+
def initialize(status, response_object, operation)
|
10
|
+
@status = status&.to_i
|
11
|
+
@object = response_object
|
12
|
+
@operation = operation
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :status
|
16
|
+
|
17
|
+
def description
|
18
|
+
@object['description']
|
19
|
+
end
|
20
|
+
|
21
|
+
def headers
|
22
|
+
@object['headers']
|
23
|
+
end
|
24
|
+
|
25
|
+
def content?
|
26
|
+
!!content&.any?
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def schema_write? = false
|
32
|
+
|
33
|
+
def content
|
34
|
+
@object['content']
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
class Schema
|
5
|
+
Result = Struct.new(:output, :schema, :data, keyword_init: true) do
|
6
|
+
def valid? = output['valid']
|
7
|
+
def error? = !output['valid']
|
8
|
+
|
9
|
+
# Returns a message that is used in exception messages.
|
10
|
+
def message
|
11
|
+
return if valid?
|
12
|
+
|
13
|
+
(output['errors']&.map { |e| e['error'] }&.join('. ') || output['error'])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'json_schemer'
|
4
|
-
require_relative '
|
4
|
+
require_relative 'schema/result'
|
5
5
|
|
6
6
|
module OpenapiFirst
|
7
|
-
class
|
8
|
-
attr_reader :
|
7
|
+
class Schema
|
8
|
+
attr_reader :schema
|
9
9
|
|
10
10
|
SCHEMAS = {
|
11
11
|
'3.1' => 'https://spec.openapis.org/oas/3.1/dialect/base',
|
@@ -13,7 +13,7 @@ module OpenapiFirst
|
|
13
13
|
}.freeze
|
14
14
|
|
15
15
|
def initialize(schema, openapi_version:, write: true)
|
16
|
-
@
|
16
|
+
@schema = schema
|
17
17
|
@schemer = JSONSchemer.schema(
|
18
18
|
schema,
|
19
19
|
access_mode: write ? 'write' : 'read',
|
@@ -25,9 +25,9 @@ module OpenapiFirst
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def validate(data)
|
28
|
-
|
28
|
+
Result.new(
|
29
29
|
output: @schemer.validate(data),
|
30
|
-
schema
|
30
|
+
schema:,
|
31
31
|
data:
|
32
32
|
)
|
33
33
|
end
|