openapi_first 1.4.2 → 2.0.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 +4 -4
- data/CHANGELOG.md +47 -1
- data/README.md +105 -28
- data/lib/openapi_first/body_parser.rb +8 -11
- data/lib/openapi_first/builder.rb +81 -0
- data/lib/openapi_first/configuration.rb +24 -3
- data/lib/openapi_first/definition.rb +44 -100
- data/lib/openapi_first/error_response.rb +2 -2
- data/lib/openapi_first/error_responses/default.rb +73 -0
- data/lib/openapi_first/error_responses/jsonapi.rb +59 -0
- data/lib/openapi_first/errors.rb +26 -4
- data/lib/openapi_first/failure.rb +29 -26
- data/lib/openapi_first/json_refs.rb +1 -3
- data/lib/openapi_first/middlewares/request_validation.rb +2 -2
- data/lib/openapi_first/middlewares/response_validation.rb +4 -3
- data/lib/openapi_first/request.rb +92 -0
- data/lib/openapi_first/request_parser.rb +35 -0
- data/lib/openapi_first/request_validator.rb +25 -0
- data/lib/openapi_first/response.rb +57 -0
- data/lib/openapi_first/response_parser.rb +49 -0
- data/lib/openapi_first/response_validator.rb +27 -0
- data/lib/openapi_first/router/find_content.rb +17 -0
- data/lib/openapi_first/router/find_response.rb +45 -0
- data/lib/openapi_first/{definition → router}/path_template.rb +9 -1
- data/lib/openapi_first/router.rb +100 -0
- data/lib/openapi_first/schema/validation_error.rb +16 -10
- data/lib/openapi_first/schema/validation_result.rb +8 -6
- data/lib/openapi_first/schema.rb +4 -8
- data/lib/openapi_first/test/methods.rb +21 -0
- data/lib/openapi_first/test.rb +19 -0
- data/lib/openapi_first/validated_request.rb +81 -0
- data/lib/openapi_first/validated_response.rb +33 -0
- data/lib/openapi_first/validators/request_body.rb +39 -0
- data/lib/openapi_first/validators/request_parameters.rb +61 -0
- data/lib/openapi_first/validators/response_body.rb +30 -0
- data/lib/openapi_first/validators/response_headers.rb +25 -0
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +40 -21
- metadata +35 -24
- data/lib/openapi_first/definition/operation.rb +0 -197
- data/lib/openapi_first/definition/path_item.rb +0 -40
- data/lib/openapi_first/definition/request_body.rb +0 -46
- data/lib/openapi_first/definition/response.rb +0 -32
- data/lib/openapi_first/definition/responses.rb +0 -87
- data/lib/openapi_first/plugins/default/error_response.rb +0 -74
- data/lib/openapi_first/plugins/default.rb +0 -11
- data/lib/openapi_first/plugins/jsonapi/error_response.rb +0 -60
- data/lib/openapi_first/plugins/jsonapi.rb +0 -11
- data/lib/openapi_first/plugins.rb +0 -25
- data/lib/openapi_first/request_validation/request_body_validator.rb +0 -41
- data/lib/openapi_first/request_validation/validator.rb +0 -82
- data/lib/openapi_first/response_validation/validator.rb +0 -98
- data/lib/openapi_first/runtime_request.rb +0 -166
- data/lib/openapi_first/runtime_response.rb +0 -124
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module OpenapiFirst
|
4
|
-
class
|
4
|
+
class Router
|
5
5
|
# @visibility private
|
6
6
|
class PathTemplate
|
7
7
|
# See also https://spec.openapis.org/oas/v3.1.0#path-templating
|
@@ -9,12 +9,20 @@ module OpenapiFirst
|
|
9
9
|
TEMPLATE_EXPRESSION_NAME = /\{([^}]+)\}/
|
10
10
|
ALLOWED_PARAMETER_CHARACTERS = %r{([^/?#]+)}
|
11
11
|
|
12
|
+
def self.template?(string)
|
13
|
+
string.include?('{')
|
14
|
+
end
|
15
|
+
|
12
16
|
def initialize(template)
|
13
17
|
@template = template
|
14
18
|
@names = template.scan(TEMPLATE_EXPRESSION_NAME).flatten
|
15
19
|
@pattern = build_pattern(template)
|
16
20
|
end
|
17
21
|
|
22
|
+
def to_s
|
23
|
+
@template
|
24
|
+
end
|
25
|
+
|
18
26
|
def match(path)
|
19
27
|
return {} if path == @template
|
20
28
|
return if @names.empty?
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'router/path_template'
|
4
|
+
require_relative 'router/find_content'
|
5
|
+
require_relative 'router/find_response'
|
6
|
+
|
7
|
+
module OpenapiFirst
|
8
|
+
# Router can map requests / responses to their API definition
|
9
|
+
class Router
|
10
|
+
# Returned by {#match}
|
11
|
+
RequestMatch = Data.define(:request_definition, :params, :error, :responses) do
|
12
|
+
def match_response(status:, content_type:)
|
13
|
+
FindResponse.call(responses, status, content_type, request_method: request_definition.request_method,
|
14
|
+
path: request_definition.path)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returned by {#routes} to introspect all routes
|
19
|
+
Route = Data.define(:path, :request_method, :requests, :responses)
|
20
|
+
|
21
|
+
NOT_FOUND = RequestMatch.new(request_definition: nil, params: nil, responses: nil, error: Failure.new(:not_found))
|
22
|
+
private_constant :NOT_FOUND
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@static = {}
|
26
|
+
@dynamic = {} # TODO: use a trie or similar
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns an enumerator of all routes
|
30
|
+
def routes
|
31
|
+
@static.chain(@dynamic).lazy.flat_map do |path, request_methods|
|
32
|
+
request_methods.filter_map do |request_method, content|
|
33
|
+
next if request_method == :template
|
34
|
+
|
35
|
+
Route.new(path:, request_method:, requests: content[:requests].each_value,
|
36
|
+
responses: content[:responses].each_value.lazy.flat_map(&:values))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Add a request definition
|
42
|
+
def add_request(request, request_method:, path:, content_type: nil)
|
43
|
+
route_at(path, request_method)[:requests][content_type] = request
|
44
|
+
end
|
45
|
+
|
46
|
+
# Add a response definition
|
47
|
+
def add_response(response, request_method:, path:, status:, response_content_type: nil)
|
48
|
+
(route_at(path, request_method)[:responses][status] ||= {})[response_content_type] = response
|
49
|
+
end
|
50
|
+
|
51
|
+
# Return all request objects that match the given path and request method
|
52
|
+
def match(request_method, path, content_type: nil)
|
53
|
+
path_item, params = find_path_item(path)
|
54
|
+
return NOT_FOUND unless path_item
|
55
|
+
|
56
|
+
contents = path_item.dig(request_method, :requests)
|
57
|
+
return NOT_FOUND.with(error: Failure.new(:method_not_allowed)) unless contents
|
58
|
+
|
59
|
+
request_definition = FindContent.call(contents, content_type)
|
60
|
+
unless request_definition
|
61
|
+
message = "#{content_type_err(content_type)} Content-Type should be #{contents.keys.join(' or ')}."
|
62
|
+
return NOT_FOUND.with(error: Failure.new(:unsupported_media_type, message:))
|
63
|
+
end
|
64
|
+
|
65
|
+
responses = path_item.dig(request_method, :responses)
|
66
|
+
RequestMatch.new(request_definition:, params:, error: nil, responses:)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def route_at(path, request_method)
|
72
|
+
request_method = request_method.upcase
|
73
|
+
path_item = if PathTemplate.template?(path)
|
74
|
+
@dynamic[path] ||= { template: PathTemplate.new(path) }
|
75
|
+
else
|
76
|
+
@static[path] ||= {}
|
77
|
+
end
|
78
|
+
path_item[request_method] ||= {
|
79
|
+
requests: {},
|
80
|
+
responses: {}
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def content_type_err(content_type)
|
85
|
+
return 'Content-Type must not be empty.' if content_type.nil? || content_type.empty?
|
86
|
+
|
87
|
+
"Content-Type #{content_type} is not defined."
|
88
|
+
end
|
89
|
+
|
90
|
+
def find_path_item(request_path)
|
91
|
+
found = @static[request_path]
|
92
|
+
return [found, {}] if found
|
93
|
+
|
94
|
+
@dynamic.find do |_path, path_item|
|
95
|
+
params = path_item[:template].match(request_path)
|
96
|
+
return [path_item, params] if params
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -3,18 +3,24 @@
|
|
3
3
|
module OpenapiFirst
|
4
4
|
class Schema
|
5
5
|
# One of multiple validation errors. Returned by Schema::ValidationResult#errors.
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
ValidationError = Data.define(:message, :data_pointer, :schema_pointer, :type, :details) do
|
7
|
+
# @deprecated Please use {#message} instead
|
8
|
+
def error
|
9
|
+
warn 'OpenapiFirst::Schema::ValidationError#error is deprecated. Use #message instead.'
|
10
|
+
message
|
9
11
|
end
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
# @deprecated Please use {#data_pointer} instead
|
14
|
+
def instance_location
|
15
|
+
warn 'OpenapiFirst::Schema::ValidationError#instance_location is deprecated. Use #data_pointer instead.'
|
16
|
+
data_pointer
|
17
|
+
end
|
18
|
+
|
19
|
+
# @deprecated Please use {#schema_pointer} instead
|
20
|
+
def schema_location
|
21
|
+
warn 'OpenapiFirst::Schema::ValidationError#schema_location is deprecated. Use #schema_pointer instead.'
|
22
|
+
schema_pointer
|
23
|
+
end
|
18
24
|
end
|
19
25
|
end
|
20
26
|
end
|
@@ -6,20 +6,22 @@ module OpenapiFirst
|
|
6
6
|
class Schema
|
7
7
|
# Result of validating data against a schema. Return value of Schema#validate.
|
8
8
|
class ValidationResult
|
9
|
-
def initialize(validation
|
9
|
+
def initialize(validation)
|
10
10
|
@validation = validation
|
11
|
-
@schema = schema
|
12
|
-
@data = data
|
13
11
|
end
|
14
12
|
|
15
|
-
attr_reader :schema, :data
|
16
|
-
|
17
13
|
def error? = @validation.any?
|
18
14
|
|
19
15
|
# Returns an array of ValidationError objects.
|
20
16
|
def errors
|
21
17
|
@errors ||= @validation.map do |err|
|
22
|
-
ValidationError.new(
|
18
|
+
ValidationError.new(
|
19
|
+
message: err['error'],
|
20
|
+
data_pointer: err['data_pointer'],
|
21
|
+
schema_pointer: err['schema_pointer'],
|
22
|
+
type: err['type'],
|
23
|
+
details: err['details']
|
24
|
+
)
|
23
25
|
end
|
24
26
|
end
|
25
27
|
end
|
data/lib/openapi_first/schema.rb
CHANGED
@@ -13,24 +13,20 @@ module OpenapiFirst
|
|
13
13
|
'3.0' => 'json-schemer://openapi30/schema'
|
14
14
|
}.freeze
|
15
15
|
|
16
|
-
def initialize(schema, openapi_version
|
17
|
-
@schema = schema
|
16
|
+
def initialize(schema, openapi_version: '3.1', write: true, after_property_validation: nil)
|
18
17
|
@schemer = JSONSchemer.schema(
|
19
18
|
schema,
|
20
19
|
access_mode: write ? 'write' : 'read',
|
21
20
|
meta_schema: SCHEMAS.fetch(openapi_version),
|
22
21
|
insert_property_defaults: true,
|
23
22
|
output_format: 'classic',
|
24
|
-
before_property_validation: method(:before_property_validation)
|
23
|
+
before_property_validation: method(:before_property_validation),
|
24
|
+
after_property_validation:
|
25
25
|
)
|
26
26
|
end
|
27
27
|
|
28
28
|
def validate(data)
|
29
|
-
ValidationResult.new(
|
30
|
-
@schemer.validate(data),
|
31
|
-
schema:,
|
32
|
-
data:
|
33
|
-
)
|
29
|
+
ValidationResult.new(@schemer.validate(data))
|
34
30
|
end
|
35
31
|
|
36
32
|
private
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
module Test
|
5
|
+
# Methods to use in integration tests
|
6
|
+
module Methods
|
7
|
+
def assert_api_conform(status: nil, api: :default)
|
8
|
+
api = OpenapiFirst::Test[api]
|
9
|
+
request = respond_to?(:last_request) ? last_request : @request
|
10
|
+
response = respond_to?(:last_response) ? last_response : @response
|
11
|
+
if status && status != response.status
|
12
|
+
raise OpenapiFirst::Error,
|
13
|
+
"Expected status #{status}, but got #{response.status} " \
|
14
|
+
"from #{request.request_method.upcase} #{request.path}."
|
15
|
+
end
|
16
|
+
api.validate_request(request, raise_error: true)
|
17
|
+
api.validate_response(request, response, raise_error: true)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'test/methods'
|
4
|
+
|
5
|
+
module OpenapiFirst
|
6
|
+
# Test integration
|
7
|
+
module Test
|
8
|
+
def self.register(path, as: :default)
|
9
|
+
@registry ||= {}
|
10
|
+
@registry[as] = OpenapiFirst.load(path)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.[](api)
|
14
|
+
@registry[api] || raise(ArgumentError,
|
15
|
+
"API description #{api} not found to be used via assert_api_conform. " \
|
16
|
+
'Use OpenapiFirst::Test.register to load an API description first.')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'delegate'
|
5
|
+
|
6
|
+
module OpenapiFirst
|
7
|
+
# A validated request. It can be valid or not.
|
8
|
+
class ValidatedRequest < SimpleDelegator
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
def initialize(original_request, error:, parsed_values: {}, request_definition: nil)
|
12
|
+
super(original_request)
|
13
|
+
@parsed_values = Hash.new({}).merge(parsed_values)
|
14
|
+
@error = error
|
15
|
+
@request_definition = request_definition
|
16
|
+
end
|
17
|
+
|
18
|
+
# @!method error
|
19
|
+
# @return [Failure, nil] The error that occurred during validation.
|
20
|
+
# @!method request_definition
|
21
|
+
# @return [Request, nil]
|
22
|
+
attr_reader :parsed_values, :error, :request_definition
|
23
|
+
|
24
|
+
# Openapi 3 specific
|
25
|
+
# @!method operation
|
26
|
+
# @return [Hash] The OpenAPI 3 operation object
|
27
|
+
# @!method operation_id
|
28
|
+
# @return [String, nil] The OpenAPI 3 operationId
|
29
|
+
def_delegators :request_definition, :operation_id, :operation
|
30
|
+
|
31
|
+
# Parsed path parameters
|
32
|
+
# @return [Hash] A string keyed hash of path parameters
|
33
|
+
def parsed_path_parameters
|
34
|
+
parsed_values[:path]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Parsed query parameters. This only returns the query parameters that are defined in the OpenAPI spec.
|
38
|
+
def parsed_query
|
39
|
+
parsed_values[:query]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Parsed headers. This only returns the query parameters that are defined in the OpenAPI spec.
|
43
|
+
def parsed_headers
|
44
|
+
parsed_values[:headers]
|
45
|
+
end
|
46
|
+
|
47
|
+
# Parsed cookies. This only returns the query parameters that are defined in the OpenAPI spec.
|
48
|
+
def parsed_cookies
|
49
|
+
parsed_values[:cookies]
|
50
|
+
end
|
51
|
+
|
52
|
+
# Parsed body. This parses the body according to the content type.
|
53
|
+
# Note that this returns the hole body, not only the fields that are defined in the OpenAPI spec.
|
54
|
+
# You can use JSON Schemas `additionalProperties` or `unevaluatedProperties` to
|
55
|
+
# returns a validation error if the body contains unknown fields.
|
56
|
+
def parsed_body
|
57
|
+
parsed_values[:body]
|
58
|
+
end
|
59
|
+
|
60
|
+
# Checks if the request is valid.
|
61
|
+
def valid?
|
62
|
+
error.nil?
|
63
|
+
end
|
64
|
+
|
65
|
+
# Checks if the request is invalid.
|
66
|
+
def invalid?
|
67
|
+
!valid?
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns true if the request is defined.
|
71
|
+
def known?
|
72
|
+
request_definition != nil
|
73
|
+
end
|
74
|
+
|
75
|
+
# Merged path, query, body parameters.
|
76
|
+
# Here path has the highest precedence, then query, then body.
|
77
|
+
def parsed_params
|
78
|
+
@parsed_params ||= parsed_body.merge(parsed_query, parsed_path_parameters)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'delegate'
|
5
|
+
|
6
|
+
module OpenapiFirst
|
7
|
+
# A validated response. It can be valid or not.
|
8
|
+
class ValidatedResponse < SimpleDelegator
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
def initialize(original_response, error:, parsed_values: nil, response_definition: nil)
|
12
|
+
super(original_response)
|
13
|
+
@error = error
|
14
|
+
@parsed_values = parsed_values
|
15
|
+
@response_definition = response_definition
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :parsed_values, :error, :response_definition
|
19
|
+
|
20
|
+
def_delegator :parsed_values, :headers, :parsed_headers
|
21
|
+
def_delegator :parsed_values, :body, :parsed_body
|
22
|
+
|
23
|
+
# Checks if the response is valid.
|
24
|
+
# @return [Boolean] true if the response is valid, false otherwise.
|
25
|
+
def valid?
|
26
|
+
error.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def invalid?
|
30
|
+
!valid?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
module Validators
|
5
|
+
class RequestBody
|
6
|
+
def self.for(request_definition, openapi_version:, hooks: {})
|
7
|
+
schema = request_definition.content_schema
|
8
|
+
return unless schema
|
9
|
+
|
10
|
+
after_property_validation = hooks[:after_request_body_property_validation]
|
11
|
+
|
12
|
+
new(Schema.new(schema, after_property_validation:, openapi_version:),
|
13
|
+
required: request_definition.required_request_body?)
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(schema, required:)
|
17
|
+
@schema = schema
|
18
|
+
@required = required
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(request)
|
22
|
+
request_body = read_body(request)
|
23
|
+
if request_body.nil?
|
24
|
+
Failure.fail!(:invalid_body, message: 'Request body is not defined') if @required
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
validation = @schema.validate(request_body)
|
29
|
+
Failure.fail!(:invalid_body, errors: validation.errors) if validation.error?
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def read_body(request)
|
35
|
+
request[:body]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
module Validators
|
5
|
+
class RequestParameters
|
6
|
+
RequestHeaders = Data.define(:schema) do
|
7
|
+
def call(parsed_values)
|
8
|
+
validation = schema.validate(parsed_values[:headers])
|
9
|
+
Failure.fail!(:invalid_header, errors: validation.errors) if validation.error?
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
Path = Data.define(:schema) do
|
14
|
+
def call(parsed_values)
|
15
|
+
validation = schema.validate(parsed_values[:path])
|
16
|
+
Failure.fail!(:invalid_path, errors: validation.errors) if validation.error?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Query = Data.define(:schema) do
|
21
|
+
def call(parsed_values)
|
22
|
+
validation = schema.validate(parsed_values[:query])
|
23
|
+
Failure.fail!(:invalid_query, errors: validation.errors) if validation.error?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
RequestCookies = Data.define(:schema) do
|
28
|
+
def call(parsed_values)
|
29
|
+
validation = schema.validate(parsed_values[:cookies])
|
30
|
+
Failure.fail!(:invalid_cookie, errors: validation.errors) if validation.error?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
VALIDATORS = {
|
35
|
+
path_schema: Path,
|
36
|
+
query_schema: Query,
|
37
|
+
header_schema: RequestHeaders,
|
38
|
+
cookie_schema: RequestCookies
|
39
|
+
}.freeze
|
40
|
+
|
41
|
+
def self.for(operation, openapi_version:, hooks: {})
|
42
|
+
after_property_validation = hooks[:after_request_parameter_property_validation]
|
43
|
+
validators = VALIDATORS.filter_map do |key, klass|
|
44
|
+
schema = operation.send(key)
|
45
|
+
klass.new(Schema.new(schema, after_property_validation:, openapi_version:)) if schema
|
46
|
+
end
|
47
|
+
return if validators.empty?
|
48
|
+
|
49
|
+
new(validators)
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(validators)
|
53
|
+
@validators = validators
|
54
|
+
end
|
55
|
+
|
56
|
+
def call(parsed_values)
|
57
|
+
@validators.each { |validator| validator.call(parsed_values) }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
module Validators
|
5
|
+
class ResponseBody
|
6
|
+
def self.for(response_definition, openapi_version:)
|
7
|
+
schema = response_definition&.content_schema
|
8
|
+
return unless schema
|
9
|
+
|
10
|
+
new(Schema.new(schema, write: false, openapi_version:))
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(schema)
|
14
|
+
@schema = schema
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :schema
|
18
|
+
|
19
|
+
def call(response)
|
20
|
+
begin
|
21
|
+
parsed_body = response.body
|
22
|
+
rescue ParseError => e
|
23
|
+
Failure.fail!(:invalid_response_body, message: e.message)
|
24
|
+
end
|
25
|
+
validation = schema.validate(parsed_body)
|
26
|
+
Failure.fail!(:invalid_response_body, errors: validation.errors) if validation.error?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenapiFirst
|
4
|
+
module Validators
|
5
|
+
class ResponseHeaders
|
6
|
+
def self.for(response_definition, openapi_version:)
|
7
|
+
schema = response_definition&.headers_schema
|
8
|
+
return unless schema
|
9
|
+
|
10
|
+
new(Schema.new(schema, openapi_version:))
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(schema)
|
14
|
+
@schema = schema
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :schema
|
18
|
+
|
19
|
+
def call(parsed_request)
|
20
|
+
validation = schema.validate(parsed_request.headers)
|
21
|
+
Failure.fail!(:invalid_response_header, errors: validation.errors) if validation.error?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/openapi_first.rb
CHANGED
@@ -5,45 +5,63 @@ require 'multi_json'
|
|
5
5
|
require_relative 'openapi_first/json_refs'
|
6
6
|
require_relative 'openapi_first/errors'
|
7
7
|
require_relative 'openapi_first/configuration'
|
8
|
-
require_relative 'openapi_first/plugins'
|
9
8
|
require_relative 'openapi_first/definition'
|
10
9
|
require_relative 'openapi_first/version'
|
11
|
-
require_relative 'openapi_first/
|
10
|
+
require_relative 'openapi_first/schema'
|
12
11
|
require_relative 'openapi_first/middlewares/response_validation'
|
13
12
|
require_relative 'openapi_first/middlewares/request_validation'
|
14
13
|
|
15
14
|
# OpenapiFirst is a toolchain to build HTTP APIS based on OpenAPI API descriptions.
|
16
15
|
module OpenapiFirst
|
17
|
-
|
16
|
+
# Key in rack to find instance of Request
|
17
|
+
REQUEST = 'openapi.request'
|
18
|
+
FAILURE = :openapi_first_validation_failure
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
end
|
20
|
+
# @return [Configuration]
|
21
|
+
def self.configuration
|
22
|
+
@configuration ||= Configuration.new
|
23
|
+
end
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
25
|
+
# @return [Configuration]
|
26
|
+
# @yield [Configuration]
|
27
|
+
def self.configure
|
28
|
+
yield configuration
|
30
29
|
end
|
31
30
|
|
32
|
-
|
33
|
-
|
31
|
+
ERROR_RESPONSES = {} # rubocop:disable Style/MutableConstant
|
32
|
+
private_constant :ERROR_RESPONSES
|
33
|
+
|
34
|
+
# Register an error response class
|
35
|
+
# @param name [Symbol]
|
36
|
+
# @param klass [Class] A class that includes / implements OpenapiFirst::ErrorResponse
|
37
|
+
def self.register_error_response(name, klass)
|
38
|
+
ERROR_RESPONSES[name.to_sym] = klass
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param name [Symbol]
|
42
|
+
# @return [Class] The error response class
|
43
|
+
def self.find_error_response(name)
|
44
|
+
ERROR_RESPONSES.fetch(name) do
|
45
|
+
raise "Unknown error response: #{name}. " /
|
46
|
+
'Register your error response class via `OpenapiFirst.register_error_response(name, klass)`. ' /
|
47
|
+
"Registered error responses are: #{ERROR_RESPONSES.keys.join(', ')}."
|
48
|
+
end
|
49
|
+
end
|
34
50
|
|
35
51
|
# Load and dereference an OpenAPI spec file
|
36
52
|
# @return [Definition]
|
37
|
-
def self.load(filepath, only: nil)
|
53
|
+
def self.load(filepath, only: nil, &)
|
54
|
+
raise FileNotFoundError, "File not found: #{filepath}" unless File.exist?(filepath)
|
55
|
+
|
38
56
|
resolved = Bundle.resolve(filepath)
|
39
|
-
parse(resolved, only:, filepath
|
57
|
+
parse(resolved, only:, filepath:, &)
|
40
58
|
end
|
41
59
|
|
42
60
|
# Parse a dereferenced Hash
|
43
61
|
# @return [Definition]
|
44
|
-
def self.parse(resolved, only: nil, filepath: nil)
|
62
|
+
def self.parse(resolved, only: nil, filepath: nil, &)
|
45
63
|
resolved['paths'].filter!(&->(key, _) { only.call(key) }) if only
|
46
|
-
Definition.new(resolved, filepath)
|
64
|
+
Definition.new(resolved, filepath, &)
|
47
65
|
end
|
48
66
|
|
49
67
|
# @!visibility private
|
@@ -55,5 +73,6 @@ module OpenapiFirst
|
|
55
73
|
end
|
56
74
|
end
|
57
75
|
|
58
|
-
|
59
|
-
|
76
|
+
require_relative 'openapi_first/error_response'
|
77
|
+
require_relative 'openapi_first/error_responses/default'
|
78
|
+
require_relative 'openapi_first/error_responses/jsonapi'
|