openapi_first 1.4.3 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -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 +25 -20
- 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'
|