openapi_first 1.0.0.beta4 → 1.0.0.beta6
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/.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
|