openapi_first 0.11.0.alpha → 0.12.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -0
- data/CHANGELOG.md +19 -1
- data/Gemfile.lock +8 -7
- data/README.md +122 -89
- data/benchmarks/Gemfile.lock +6 -6
- data/benchmarks/apps/openapi_first.ru +1 -1
- data/examples/app.rb +6 -1
- data/lib/openapi_first.rb +46 -8
- data/lib/openapi_first/app.rb +5 -8
- data/lib/openapi_first/definition.rb +0 -12
- data/lib/openapi_first/find_handler.rb +25 -21
- data/lib/openapi_first/operation.rb +12 -7
- data/lib/openapi_first/request_validation.rb +24 -13
- data/lib/openapi_first/{operation_resolver.rb → responder.rb} +18 -4
- data/lib/openapi_first/response_validation.rb +25 -78
- data/lib/openapi_first/response_validator.rb +6 -47
- data/lib/openapi_first/router.rb +29 -31
- data/lib/openapi_first/router_required.rb +13 -0
- data/lib/openapi_first/validation_format.rb +3 -1
- data/lib/openapi_first/version.rb +1 -1
- data/openapi_first.gemspec +7 -7
- metadata +6 -5
data/examples/app.rb
CHANGED
data/lib/openapi_first.rb
CHANGED
@@ -9,7 +9,7 @@ require 'openapi_first/router'
|
|
9
9
|
require 'openapi_first/request_validation'
|
10
10
|
require 'openapi_first/response_validator'
|
11
11
|
require 'openapi_first/response_validation'
|
12
|
-
require 'openapi_first/
|
12
|
+
require 'openapi_first/responder'
|
13
13
|
require 'openapi_first/app'
|
14
14
|
|
15
15
|
module OpenapiFirst
|
@@ -19,6 +19,10 @@ module OpenapiFirst
|
|
19
19
|
INBOX = 'openapi_first.inbox'
|
20
20
|
HANDLER = 'openapi_first.handler'
|
21
21
|
|
22
|
+
def self.env
|
23
|
+
ENV['RACK_ENV'] || ENV['HANAMI_ENV'] || ENV['RAILS_ENV']
|
24
|
+
end
|
25
|
+
|
22
26
|
def self.load(spec_path, only: nil)
|
23
27
|
content = YAML.load_file(spec_path)
|
24
28
|
raw = OasParser::Parser.new(spec_path, content).resolve
|
@@ -27,14 +31,14 @@ module OpenapiFirst
|
|
27
31
|
Definition.new(parsed)
|
28
32
|
end
|
29
33
|
|
30
|
-
def self.app(spec, namespace:)
|
34
|
+
def self.app(spec, namespace:, raise_error: false)
|
31
35
|
spec = OpenapiFirst.load(spec) if spec.is_a?(String)
|
32
|
-
App.new(nil, spec, namespace: namespace)
|
36
|
+
App.new(nil, spec, namespace: namespace, raise_error: raise_error)
|
33
37
|
end
|
34
38
|
|
35
|
-
def self.middleware(spec, namespace:)
|
39
|
+
def self.middleware(spec, namespace:, raise_error: false)
|
36
40
|
spec = OpenapiFirst.load(spec) if spec.is_a?(String)
|
37
|
-
AppWithOptions.new(spec, namespace: namespace)
|
41
|
+
AppWithOptions.new(spec, namespace: namespace, raise_error: raise_error)
|
38
42
|
end
|
39
43
|
|
40
44
|
class AppWithOptions
|
@@ -50,7 +54,41 @@ module OpenapiFirst
|
|
50
54
|
|
51
55
|
class Error < StandardError; end
|
52
56
|
class NotFoundError < Error; end
|
53
|
-
class
|
54
|
-
class
|
55
|
-
class
|
57
|
+
class NotImplementedError < RuntimeError; end
|
58
|
+
class ResponseInvalid < Error; end
|
59
|
+
class ResponseCodeNotFoundError < ResponseInvalid; end
|
60
|
+
class ResponseContentTypeNotFoundError < ResponseInvalid; end
|
61
|
+
class ResponseBodyInvalidError < ResponseInvalid; end
|
62
|
+
|
63
|
+
class RequestInvalidError < Error
|
64
|
+
def initialize(serialized_errors)
|
65
|
+
message = error_message(serialized_errors)
|
66
|
+
super message
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def error_message(errors)
|
72
|
+
errors.map do |error|
|
73
|
+
[human_source(error), human_error(error)].compact.join(' ')
|
74
|
+
end.join(', ')
|
75
|
+
end
|
76
|
+
|
77
|
+
def human_source(error)
|
78
|
+
return unless error[:source]
|
79
|
+
|
80
|
+
source_key = error[:source].keys.first
|
81
|
+
source = {
|
82
|
+
pointer: 'Request body invalid:',
|
83
|
+
parameter: 'Query parameter invalid:'
|
84
|
+
}.fetch(source_key, source_key)
|
85
|
+
name = error[:source].values.first
|
86
|
+
source += " #{name}" unless name.nil? || name.empty?
|
87
|
+
source
|
88
|
+
end
|
89
|
+
|
90
|
+
def human_error(error)
|
91
|
+
error[:title]
|
92
|
+
end
|
93
|
+
end
|
56
94
|
end
|
data/lib/openapi_first/app.rb
CHANGED
@@ -5,16 +5,13 @@ require 'logger'
|
|
5
5
|
|
6
6
|
module OpenapiFirst
|
7
7
|
class App
|
8
|
-
def initialize(
|
9
|
-
parent_app,
|
10
|
-
spec,
|
11
|
-
namespace:
|
12
|
-
)
|
8
|
+
def initialize(parent_app, spec, namespace:, raise_error:)
|
13
9
|
@stack = Rack::Builder.app do
|
14
10
|
freeze_app
|
15
|
-
use OpenapiFirst::Router, spec: spec, parent_app: parent_app
|
16
|
-
use OpenapiFirst::RequestValidation
|
17
|
-
|
11
|
+
use OpenapiFirst::Router, spec: spec, raise_error: raise_error, parent_app: parent_app
|
12
|
+
use OpenapiFirst::RequestValidation, raise_error: raise_error
|
13
|
+
use OpenapiFirst::ResponseValidation if raise_error
|
14
|
+
run OpenapiFirst::Responder.new(
|
18
15
|
spec: spec,
|
19
16
|
namespace: namespace
|
20
17
|
)
|
@@ -14,17 +14,5 @@ module OpenapiFirst
|
|
14
14
|
def operations
|
15
15
|
@spec.endpoints.map { |e| Operation.new(e) }
|
16
16
|
end
|
17
|
-
|
18
|
-
def find_operation!(request)
|
19
|
-
@spec
|
20
|
-
.path_by_path(request.path)
|
21
|
-
.endpoint_by_method(request.request_method.downcase)
|
22
|
-
end
|
23
|
-
|
24
|
-
def find_operation(request)
|
25
|
-
find_operation!(request)
|
26
|
-
rescue OasParser::PathNotFound, OasParser::MethodNotFound
|
27
|
-
nil
|
28
|
-
end
|
29
17
|
end
|
30
18
|
end
|
@@ -5,14 +5,10 @@ require_relative 'utils'
|
|
5
5
|
module OpenapiFirst
|
6
6
|
class FindHandler
|
7
7
|
def initialize(spec, namespace)
|
8
|
-
@spec = spec
|
9
8
|
@namespace = namespace
|
10
|
-
|
11
|
-
|
12
|
-
def all
|
13
|
-
@spec.operations.each_with_object({}) do |operation, hash|
|
9
|
+
@handlers = spec.operations.each_with_object({}) do |operation, hash|
|
14
10
|
operation_id = operation.operation_id
|
15
|
-
handler =
|
11
|
+
handler = find_handler(operation_id)
|
16
12
|
if handler.nil?
|
17
13
|
warn "#{self.class.name} cannot not find handler for '#{operation.operation_id}' (#{operation.method} #{operation.path}). This operation will be ignored." # rubocop:disable Layout/LineLength
|
18
14
|
next
|
@@ -21,22 +17,17 @@ module OpenapiFirst
|
|
21
17
|
end
|
22
18
|
end
|
23
19
|
|
24
|
-
def
|
20
|
+
def [](operation_id)
|
21
|
+
@handlers[operation_id]
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_handler(operation_id)
|
25
25
|
name = operation_id.match(/:*(.*)/)&.to_a&.at(1)
|
26
26
|
return if name.nil?
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
return klass&.method(Utils.underscore(method_name))
|
32
|
-
end
|
33
|
-
if name.include?('#')
|
34
|
-
module_name, klass_name = name.split('#')
|
35
|
-
const = find_const(@namespace, module_name)
|
36
|
-
klass = find_const(const, klass_name)
|
37
|
-
return ->(params, res) { klass.new.call(params, res) } if klass.instance_method(:initialize).arity.zero?
|
38
|
-
|
39
|
-
return ->(params, res) { klass.new(params.env).call(params, res) }
|
28
|
+
catch :halt do
|
29
|
+
return find_class_method_handler(name) if name.include?('.')
|
30
|
+
return find_instance_method_handler(name) if name.include?('#')
|
40
31
|
end
|
41
32
|
method_name = Utils.underscore(name)
|
42
33
|
return unless @namespace.respond_to?(method_name)
|
@@ -44,11 +35,24 @@ module OpenapiFirst
|
|
44
35
|
@namespace.method(method_name)
|
45
36
|
end
|
46
37
|
|
47
|
-
|
38
|
+
def find_class_method_handler(name)
|
39
|
+
module_name, method_name = name.split('.')
|
40
|
+
klass = find_const(@namespace, module_name)
|
41
|
+
klass.method(Utils.underscore(method_name))
|
42
|
+
end
|
43
|
+
|
44
|
+
def find_instance_method_handler(name)
|
45
|
+
module_name, klass_name = name.split('#')
|
46
|
+
const = find_const(@namespace, module_name)
|
47
|
+
klass = find_const(const, klass_name)
|
48
|
+
return ->(params, res) { klass.new.call(params, res) } if klass.instance_method(:initialize).arity.zero?
|
49
|
+
|
50
|
+
->(params, res) { klass.new(params.env).call(params, res) }
|
51
|
+
end
|
48
52
|
|
49
53
|
def find_const(parent, name)
|
50
54
|
name = Utils.classify(name)
|
51
|
-
|
55
|
+
throw :halt unless parent.const_defined?(name, false)
|
52
56
|
|
53
57
|
parent.const_get(name, false)
|
54
58
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'forwardable'
|
4
|
+
require 'json_schemer'
|
4
5
|
require_relative 'utils'
|
5
6
|
require_relative 'response_object'
|
6
7
|
|
@@ -25,6 +26,10 @@ module OpenapiFirst
|
|
25
26
|
@parameters_json_schema ||= build_parameters_json_schema
|
26
27
|
end
|
27
28
|
|
29
|
+
def parameters_schema
|
30
|
+
@parameters_schema ||= parameters_json_schema && JSONSchemer.schema(parameters_json_schema)
|
31
|
+
end
|
32
|
+
|
28
33
|
def content_type_for(status)
|
29
34
|
content = response_for(status)['content']
|
30
35
|
content.keys[0] if content
|
@@ -36,8 +41,8 @@ module OpenapiFirst
|
|
36
41
|
|
37
42
|
media_type = content[content_type]
|
38
43
|
unless media_type
|
39
|
-
message = "Response
|
40
|
-
raise
|
44
|
+
message = "Response content type not found '#{content_type}' for '#{name}'"
|
45
|
+
raise ResponseContentTypeNotFoundError, message
|
41
46
|
end
|
42
47
|
media_type['schema']
|
43
48
|
end
|
@@ -45,16 +50,16 @@ module OpenapiFirst
|
|
45
50
|
def response_for(status)
|
46
51
|
@operation.response_by_code(status.to_s, use_default: true).raw
|
47
52
|
rescue OasParser::ResponseCodeNotFound
|
48
|
-
message = "Response status code or default not found: #{status} for '#{
|
53
|
+
message = "Response status code or default not found: #{status} for '#{name}'"
|
49
54
|
raise OpenapiFirst::ResponseCodeNotFoundError, message
|
50
55
|
end
|
51
56
|
|
52
|
-
|
53
|
-
|
54
|
-
def operation_name
|
55
|
-
"#{method.upcase} #{path}"
|
57
|
+
def name
|
58
|
+
"#{method.upcase} #{path} (#{operation_id})"
|
56
59
|
end
|
57
60
|
|
61
|
+
private
|
62
|
+
|
58
63
|
def build_parameters_json_schema
|
59
64
|
return unless @operation.parameters&.any?
|
60
65
|
|
@@ -4,12 +4,16 @@ require 'rack'
|
|
4
4
|
require 'json_schemer'
|
5
5
|
require 'multi_json'
|
6
6
|
require_relative 'inbox'
|
7
|
+
require_relative 'router_required'
|
7
8
|
require_relative 'validation_format'
|
8
9
|
|
9
10
|
module OpenapiFirst
|
10
11
|
class RequestValidation # rubocop:disable Metrics/ClassLength
|
11
|
-
|
12
|
+
prepend RouterRequired
|
13
|
+
|
14
|
+
def initialize(app, raise_error: false)
|
12
15
|
@app = app
|
16
|
+
@raise = raise_error
|
13
17
|
end
|
14
18
|
|
15
19
|
def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
@@ -46,32 +50,32 @@ module OpenapiFirst
|
|
46
50
|
|
47
51
|
parsed_request_body = parse_request_body!(body)
|
48
52
|
errors = validate_json_schema(schema, parsed_request_body)
|
49
|
-
|
53
|
+
halt_with_error(400, serialize_request_body_errors(errors)) if errors.any?
|
50
54
|
env[INBOX].merge! env[REQUEST_BODY] = parsed_request_body
|
51
55
|
end
|
52
56
|
|
53
57
|
def parse_request_body!(body)
|
54
|
-
MultiJson.load(body)
|
58
|
+
MultiJson.load(body, symbolize_keys: true)
|
55
59
|
rescue MultiJson::ParseError => e
|
56
60
|
err = { title: 'Failed to parse body as JSON' }
|
57
61
|
err[:detail] = e.cause unless ENV['RACK_ENV'] == 'production'
|
58
|
-
|
62
|
+
halt_with_error(400, [err])
|
59
63
|
end
|
60
64
|
|
61
65
|
def validate_request_content_type!(content_type, operation)
|
62
66
|
return if operation.request_body.content[content_type]
|
63
67
|
|
64
|
-
|
68
|
+
halt_with_error(415)
|
65
69
|
end
|
66
70
|
|
67
71
|
def validate_request_body_presence!(body, operation)
|
68
72
|
return unless operation.request_body.required && body.empty?
|
69
73
|
|
70
|
-
|
74
|
+
halt_with_error(415, 'Request body is required')
|
71
75
|
end
|
72
76
|
|
73
77
|
def validate_json_schema(schema, object)
|
74
|
-
|
78
|
+
schema.validate(Utils.deep_stringify(object))
|
75
79
|
end
|
76
80
|
|
77
81
|
def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
|
@@ -81,8 +85,10 @@ module OpenapiFirst
|
|
81
85
|
}
|
82
86
|
end
|
83
87
|
|
84
|
-
def
|
85
|
-
|
88
|
+
def halt_with_error(status, errors = [default_error(status)])
|
89
|
+
raise RequestInvalidError, errors if @raise
|
90
|
+
|
91
|
+
halt Rack::Response.new(
|
86
92
|
MultiJson.dump(errors: errors),
|
87
93
|
status,
|
88
94
|
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
@@ -92,7 +98,8 @@ module OpenapiFirst
|
|
92
98
|
def request_body_schema(content_type, operation)
|
93
99
|
return unless operation
|
94
100
|
|
95
|
-
operation.request_body.content[content_type]&.fetch('schema')
|
101
|
+
schema = operation.request_body.content[content_type]&.fetch('schema')
|
102
|
+
JSONSchemer.schema(schema) if schema
|
96
103
|
end
|
97
104
|
|
98
105
|
def serialize_request_body_errors(validation_errors)
|
@@ -110,8 +117,11 @@ module OpenapiFirst
|
|
110
117
|
return unless json_schema
|
111
118
|
|
112
119
|
params = filtered_params(json_schema, params)
|
113
|
-
errors =
|
114
|
-
|
120
|
+
errors = validate_json_schema(
|
121
|
+
operation.parameters_schema,
|
122
|
+
params
|
123
|
+
)
|
124
|
+
halt_with_error(400, serialize_query_parameter_errors(errors)) if errors.any?
|
115
125
|
env[PARAMETERS] = params
|
116
126
|
env[INBOX].merge! params
|
117
127
|
end
|
@@ -119,7 +129,8 @@ module OpenapiFirst
|
|
119
129
|
def filtered_params(json_schema, params)
|
120
130
|
json_schema['properties']
|
121
131
|
.each_with_object({}) do |key_value, result|
|
122
|
-
parameter_name
|
132
|
+
parameter_name = key_value[0].to_sym
|
133
|
+
schema = key_value[1]
|
123
134
|
next unless params.key?(parameter_name)
|
124
135
|
|
125
136
|
value = params[parameter_name]
|
@@ -5,16 +5,16 @@ require_relative 'inbox'
|
|
5
5
|
require_relative 'find_handler'
|
6
6
|
|
7
7
|
module OpenapiFirst
|
8
|
-
class
|
9
|
-
def initialize(spec:, namespace:)
|
10
|
-
@
|
8
|
+
class Responder
|
9
|
+
def initialize(spec:, namespace:, resolver: FindHandler.new(spec, namespace))
|
10
|
+
@resolver = resolver
|
11
11
|
@namespace = namespace
|
12
12
|
end
|
13
13
|
|
14
14
|
def call(env)
|
15
15
|
operation = env[OpenapiFirst::OPERATION]
|
16
16
|
res = Rack::Response.new
|
17
|
-
handler =
|
17
|
+
handler = find_handler(operation)
|
18
18
|
result = handler.call(env[INBOX], res)
|
19
19
|
res.write serialize(result) if result && res.body.empty?
|
20
20
|
res[Rack::CONTENT_TYPE] ||= operation.content_type_for(res.status)
|
@@ -23,10 +23,24 @@ module OpenapiFirst
|
|
23
23
|
|
24
24
|
private
|
25
25
|
|
26
|
+
def find_handler(operation)
|
27
|
+
handler = @resolver[operation.operation_id]
|
28
|
+
raise NotImplementedError, "Could not find handler for #{operation.name}" unless handler
|
29
|
+
|
30
|
+
handler
|
31
|
+
end
|
32
|
+
|
26
33
|
def serialize(result)
|
27
34
|
return result if result.is_a?(String)
|
28
35
|
|
29
36
|
MultiJson.dump(result)
|
30
37
|
end
|
31
38
|
end
|
39
|
+
|
40
|
+
class OperationResolver < Responder
|
41
|
+
def initialize(spec:, namespace:)
|
42
|
+
warn "#{self.class.name} was renamed to #{OpenapiFirst::Responder.name}"
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
32
46
|
end
|
@@ -2,10 +2,13 @@
|
|
2
2
|
|
3
3
|
require 'json_schemer'
|
4
4
|
require 'multi_json'
|
5
|
+
require_relative 'router_required'
|
5
6
|
require_relative 'validation'
|
6
7
|
|
7
8
|
module OpenapiFirst
|
8
9
|
class ResponseValidation
|
10
|
+
prepend RouterRequired
|
11
|
+
|
9
12
|
def initialize(app)
|
10
13
|
@app = app
|
11
14
|
end
|
@@ -14,103 +17,47 @@ module OpenapiFirst
|
|
14
17
|
operation = env[OPERATION]
|
15
18
|
return @app.call(env) unless operation
|
16
19
|
|
17
|
-
|
20
|
+
response = @app.call(env)
|
21
|
+
validate(response, operation)
|
22
|
+
response
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate(response, operation)
|
26
|
+
status, headers, body = response.to_a
|
27
|
+
return validate_status_only(operation, status) if status == 204
|
28
|
+
|
18
29
|
content_type = headers[Rack::CONTENT_TYPE]
|
30
|
+
raise ResponseInvalid, "Response has no content-type for '#{operation.name}'" unless content_type
|
31
|
+
|
19
32
|
response_schema = operation.response_schema_for(status, content_type)
|
20
33
|
validate_response_body(response_schema, body) if response_schema
|
21
|
-
|
22
|
-
[status, headers, body]
|
23
34
|
end
|
24
35
|
|
25
36
|
private
|
26
37
|
|
27
|
-
def
|
28
|
-
|
29
|
-
end
|
30
|
-
|
31
|
-
def error(message)
|
32
|
-
{ title: message }
|
33
|
-
end
|
34
|
-
|
35
|
-
def error_response(status, errors)
|
36
|
-
Rack::Response.new(
|
37
|
-
MultiJson.dump(errors: errors),
|
38
|
-
status,
|
39
|
-
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
40
|
-
).finish
|
38
|
+
def validate_status_only(operation, status)
|
39
|
+
operation.response_for(status)
|
41
40
|
end
|
42
41
|
|
43
42
|
def validate_response_body(schema, response)
|
44
43
|
full_body = +''
|
45
44
|
response.each { |chunk| full_body << chunk }
|
46
|
-
data = full_body.empty? ? {} :
|
45
|
+
data = full_body.empty? ? {} : load_json(full_body)
|
47
46
|
errors = JSONSchemer.schema(schema).validate(data).to_a.map do |error|
|
48
|
-
|
47
|
+
error_message_for(error)
|
49
48
|
end
|
50
49
|
raise ResponseBodyInvalidError, errors.join(', ') if errors.any?
|
51
50
|
end
|
52
51
|
|
53
|
-
def
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
# frozen_string_literal: true
|
61
|
-
|
62
|
-
require 'json_schemer'
|
63
|
-
require 'multi_json'
|
64
|
-
require_relative 'validation'
|
65
|
-
|
66
|
-
module OpenapiFirst
|
67
|
-
class ResponseValidator
|
68
|
-
def initialize(spec)
|
69
|
-
@spec = spec
|
70
|
-
end
|
71
|
-
|
72
|
-
def validate(request, response)
|
73
|
-
errors = validation_errors(request, response)
|
74
|
-
Validation.new(errors || [])
|
75
|
-
rescue OasParser::ResponseCodeNotFound, OasParser::MethodNotFound => e
|
76
|
-
Validation.new([e.message])
|
52
|
+
def load_json(string)
|
53
|
+
MultiJson.load(string)
|
54
|
+
rescue MultiJson::ParseError
|
55
|
+
string
|
77
56
|
end
|
78
57
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
content = response_for(request, response)&.content
|
83
|
-
return unless content
|
84
|
-
|
85
|
-
content_type = content[response.content_type]
|
86
|
-
return ["Content type not found: '#{response.content_type}'"] unless content_type
|
87
|
-
|
88
|
-
response_schema = content_type['schema']
|
89
|
-
return unless response_schema
|
90
|
-
|
91
|
-
response_data = MultiJson.load(response.body)
|
92
|
-
validate_json_schema(response_schema, response_data)
|
93
|
-
end
|
94
|
-
|
95
|
-
def validate_json_schema(schema, data)
|
96
|
-
JSONSchemer.schema(schema).validate(data).to_a.map do |error|
|
97
|
-
format_error(error)
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
def format_error(error)
|
102
|
-
ValidationFormat.error_details(error)
|
103
|
-
.merge!(
|
104
|
-
data_pointer: error['data_pointer'],
|
105
|
-
schema_pointer: error['schema_pointer']
|
106
|
-
).tap do |formatted|
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
def response_for(request, response)
|
111
|
-
@spec
|
112
|
-
.find_operation!(request)
|
113
|
-
&.response_by_code(response.status.to_s, use_default: true)
|
58
|
+
def error_message_for(error)
|
59
|
+
err = ValidationFormat.error_details(error)
|
60
|
+
[err[:title], error['data_pointer'], err[:detail]].compact.join(' ')
|
114
61
|
end
|
115
62
|
end
|
116
63
|
end
|