openapi_first 1.0.0.beta3 → 1.0.0.beta4

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,10 +3,10 @@
3
3
  require 'forwardable'
4
4
  require 'set'
5
5
  require_relative 'schema_validation'
6
- require_relative 'utils'
6
+ require_relative 'operation_schemas'
7
7
 
8
8
  module OpenapiFirst
9
- class Operation
9
+ class Operation # rubocop:disable Metrics/ClassLength
10
10
  extend Forwardable
11
11
  def_delegators :operation_object,
12
12
  :[],
@@ -15,12 +15,13 @@ module OpenapiFirst
15
15
  WRITE_METHODS = Set.new(%w[post put patch delete]).freeze
16
16
  private_constant :WRITE_METHODS
17
17
 
18
- attr_reader :path, :method
18
+ attr_reader :path, :method, :openapi_version
19
19
 
20
- def initialize(path, request_method, path_item_object)
20
+ def initialize(path, request_method, path_item_object, openapi_version:)
21
21
  @path = path
22
22
  @method = request_method
23
23
  @path_item_object = path_item_object
24
+ @openapi_version = openapi_version
24
25
  end
25
26
 
26
27
  def operation_id
@@ -39,7 +40,7 @@ module OpenapiFirst
39
40
  operation_object['requestBody']
40
41
  end
41
42
 
42
- def response_schema_for(status, content_type)
43
+ def response_body_schema(status, content_type)
43
44
  content = response_for(status)['content']
44
45
  return if content.nil? || content.empty?
45
46
 
@@ -52,16 +53,18 @@ module OpenapiFirst
52
53
  raise ResponseContentTypeNotFoundError, message
53
54
  end
54
55
  schema = media_type['schema']
55
- SchemaValidation.new(schema, write: false) if schema
56
+ return unless schema
57
+
58
+ SchemaValidation.new(schema, write: false, openapi_version:)
56
59
  end
57
60
 
58
61
  def request_body_schema(request_content_type)
59
- content = operation_object.dig('requestBody', 'content')
60
- media_type = find_content_for_content_type(content, request_content_type)
61
- schema = media_type&.fetch('schema', nil)
62
- return unless schema
63
-
64
- SchemaValidation.new(schema, write: write?)
62
+ (@request_body_schema ||= {})[request_content_type] ||= begin
63
+ content = operation_object.dig('requestBody', 'content')
64
+ media_type = find_content_for_content_type(content, request_content_type)
65
+ schema = media_type&.fetch('schema', nil)
66
+ SchemaValidation.new(schema, write: write?, openapi_version:) if schema
67
+ end
65
68
  end
66
69
 
67
70
  def response_for(status)
@@ -73,12 +76,12 @@ module OpenapiFirst
73
76
  end
74
77
 
75
78
  def name
76
- "#{method.upcase} #{path} (#{operation_id})"
79
+ @name ||= "#{method.upcase} #{path} (#{operation_id})"
77
80
  end
78
81
 
79
82
  def valid_request_content_type?(request_content_type)
80
83
  content = operation_object.dig('requestBody', 'content')
81
- return unless content
84
+ return false unless content
82
85
 
83
86
  !!find_content_for_content_type(content, request_content_type)
84
87
  end
@@ -91,6 +94,17 @@ module OpenapiFirst
91
94
  @path_parameters ||= all_parameters.filter { |p| p['in'] == 'path' }
92
95
  end
93
96
 
97
+ IGNORED_HEADERS = Set['Content-Type', 'Accept', 'Authorization'].freeze
98
+ private_constant :IGNORED_HEADERS
99
+
100
+ def header_parameters
101
+ @header_parameters ||= all_parameters.filter { |p| p['in'] == 'header' && !IGNORED_HEADERS.include?(p['name']) }
102
+ end
103
+
104
+ def cookie_parameters
105
+ @cookie_parameters ||= all_parameters.filter { |p| p['in'] == 'cookie' }
106
+ end
107
+
94
108
  def all_parameters
95
109
  @all_parameters ||= begin
96
110
  parameters = @path_item_object['parameters']&.dup || []
@@ -100,6 +114,11 @@ module OpenapiFirst
100
114
  end
101
115
  end
102
116
 
117
+ # visibility: private
118
+ def schemas
119
+ @schemas ||= OperationSchemas.new(self)
120
+ end
121
+
103
122
  private
104
123
 
105
124
  def response_by_code(status)
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openapi_parameters/parameter'
4
+ require_relative 'schema_validation'
5
+
6
+ module OpenapiFirst
7
+ # This class is basically a cache for JSON Schemas of parameters
8
+ class OperationSchemas
9
+ # @operation [OpenapiFirst::Operation]
10
+ def initialize(operation)
11
+ @operation = operation
12
+ end
13
+
14
+ attr_reader :operation
15
+
16
+ # Return JSON Schema of for all query parameters
17
+ def query_parameters_schema
18
+ @query_parameters_schema ||= build_json_schema(operation.query_parameters)
19
+ end
20
+
21
+ # Return JSON Schema of for all path parameters
22
+ def path_parameters_schema
23
+ @path_parameters_schema ||= build_json_schema(operation.path_parameters)
24
+ end
25
+
26
+ def header_parameters_schema
27
+ @header_parameters_schema ||= build_json_schema(operation.header_parameters)
28
+ end
29
+
30
+ def cookie_parameters_schema
31
+ @cookie_parameters_schema ||= build_json_schema(operation.cookie_parameters)
32
+ end
33
+
34
+ private
35
+
36
+ # Build JSON Schema for given parameter definitions
37
+ # @parameter_defs [Array<Hash>] Parameter definitions
38
+ def build_json_schema(parameter_defs)
39
+ init_schema = {
40
+ 'type' => 'object',
41
+ 'properties' => {},
42
+ 'required' => []
43
+ }
44
+ schema = parameter_defs.each_with_object(init_schema) do |parameter_def, result|
45
+ parameter = OpenapiParameters::Parameter.new(parameter_def)
46
+ result['properties'][parameter.name] = parameter.schema if parameter.schema
47
+ result['required'] << parameter.name if parameter.required?
48
+ end
49
+ SchemaValidation.new(schema, openapi_version: operation.openapi_version)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ module Plugins
5
+ ERROR_RESPONSES = {} # rubocop:disable Style/MutableConstant
6
+
7
+ def self.register_error_response(name, klass)
8
+ ERROR_RESPONSES[name] = klass
9
+ end
10
+
11
+ def self.find_error_response(name)
12
+ return name if name.is_a?(Class)
13
+
14
+ ERROR_RESPONSES.fetch(name)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ class RequestBodyValidator
5
+ def initialize(operation, env)
6
+ @operation = operation
7
+ @env = env
8
+ @parsed_request_body = env[REQUEST_BODY]
9
+ end
10
+
11
+ def validate!
12
+ content_type = Rack::Request.new(@env).content_type
13
+ validate_request_content_type!(@operation, content_type)
14
+ validate_request_body!(@operation, @parsed_request_body, content_type)
15
+ end
16
+
17
+ private
18
+
19
+ def validate_request_content_type!(operation, content_type)
20
+ operation.valid_request_content_type?(content_type) || OpenapiFirst.error!(415)
21
+ end
22
+
23
+ def validate_request_body!(operation, body, content_type)
24
+ validate_request_body_presence!(body, operation)
25
+ return if content_type.nil?
26
+
27
+ schema = operation&.request_body_schema(content_type)
28
+ return unless schema
29
+
30
+ validation_result = schema.validate(body)
31
+ OpenapiFirst.error!(400, :request_body, validation_result:) if validation_result.error?
32
+ body
33
+ end
34
+
35
+ def validate_request_body_presence!(body, operation)
36
+ return unless operation.request_body['required'] && body.nil?
37
+
38
+ OpenapiFirst.error!(400, :request_body, title: 'Request body is required')
39
+ end
40
+ end
41
+ end
@@ -3,7 +3,9 @@
3
3
  require 'rack'
4
4
  require 'multi_json'
5
5
  require_relative 'use_router'
6
- require_relative 'validation_format'
6
+ require_relative 'error_response'
7
+ require_relative 'request_body_validator'
8
+ require_relative 'string_keyed_hash'
7
9
  require 'openapi_parameters'
8
10
 
9
11
  module OpenapiFirst
@@ -13,118 +15,98 @@ module OpenapiFirst
13
15
  def initialize(app, options = {})
14
16
  @app = app
15
17
  @raise = options.fetch(:raise_error, false)
18
+ @error_response_class =
19
+ Plugins.find_error_response(options.fetch(:error_response, Config.default_options.error_response))
16
20
  end
17
21
 
18
- def call(env) # rubocop:disable Metrics/AbcSize
22
+ def call(env)
19
23
  operation = env[OPERATION]
20
24
  return @app.call(env) unless operation
21
25
 
22
- error = catch(:error) do
23
- query_params = OpenapiParameters::Query.new(operation.query_parameters).unpack(env['QUERY_STRING'])
24
- validate_query_parameters!(operation, query_params)
25
- env[PARAMS].merge!(query_params)
26
-
27
- return @app.call(env) unless operation.request_body
28
-
29
- content_type = Rack::Request.new(env).content_type
30
- validate_request_content_type!(operation, content_type)
31
- parsed_request_body = env[REQUEST_BODY]
32
- validate_request_body!(operation, parsed_request_body, content_type)
33
- nil
34
- end
26
+ error = validate_request(operation, env)
35
27
  if error
36
- raise RequestInvalidError, error[:errors] if @raise
28
+ location, title = error.values_at(:location, :title)
29
+ raise RequestInvalidError, error_message(title, location) if @raise
37
30
 
38
- return validation_error_response(error[:status], error[:errors])
31
+ return error_response(error).render
39
32
  end
40
33
  @app.call(env)
41
34
  end
42
35
 
43
36
  private
44
37
 
45
- def validate_request_body!(operation, body, content_type)
46
- validate_request_body_presence!(body, operation)
47
- return if content_type.nil?
38
+ def error_message(title, location)
39
+ return title unless location
48
40
 
49
- schema = operation&.request_body_schema(content_type)
50
- return unless schema
51
-
52
- errors = schema.validate(body)
53
- throw_error(400, serialize_request_body_errors(errors)) if errors.any?
54
- body
55
- end
56
-
57
- def validate_request_content_type!(operation, content_type)
58
- operation.valid_request_content_type?(content_type) || throw_error(415)
41
+ "#{TOPICS.fetch(location)} #{title}"
59
42
  end
60
43
 
61
- def validate_request_body_presence!(body, operation)
62
- return unless operation.request_body['required'] && body.nil?
63
-
64
- throw_error(415, 'Request body is required')
44
+ TOPICS = {
45
+ request_body: 'Request body invalid:',
46
+ query: 'Query parameter invalid:',
47
+ header: 'Header parameter invalid:',
48
+ path: 'Path segment invalid:',
49
+ cookie: 'Cookie value invalid:'
50
+ }.freeze
51
+ private_constant :TOPICS
52
+
53
+ def error_response(error_object)
54
+ @error_response_class.new(**error_object)
65
55
  end
66
56
 
67
- def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
68
- {
69
- status: status.to_s,
70
- title: title
71
- }
57
+ def validate_request(operation, env)
58
+ catch(:error) do
59
+ env[PARAMS] = {}
60
+ validate_query_params!(operation, env)
61
+ validate_path_params!(operation, env)
62
+ validate_cookie_params!(operation, env)
63
+ validate_header_params!(operation, env)
64
+ RequestBodyValidator.new(operation, env).validate! if operation.request_body
65
+ nil
66
+ end
72
67
  end
73
68
 
74
- def throw_error(status, errors = [default_error(status)])
75
- throw :error, {
76
- status: status,
77
- errors: errors
78
- }
79
- end
69
+ def validate_path_params!(operation, env)
70
+ path_parameters = operation.path_parameters
71
+ return if path_parameters.empty?
80
72
 
81
- def validation_error_response(status, errors)
82
- Rack::Response.new(
83
- MultiJson.dump(errors: errors),
84
- status,
85
- Rack::CONTENT_TYPE => 'application/vnd.api+json'
86
- ).finish
73
+ hashy = StringKeyedHash.new(env[Router::RAW_PATH_PARAMS])
74
+ unpacked_path_params = OpenapiParameters::Path.new(path_parameters).unpack(hashy)
75
+ validation_result = operation.schemas.path_parameters_schema.validate(unpacked_path_params)
76
+ OpenapiFirst.error!(400, :path, validation_result:) if validation_result.error?
77
+ env[PATH_PARAMS] = unpacked_path_params
78
+ env[PARAMS].merge!(unpacked_path_params)
87
79
  end
88
80
 
89
- def serialize_request_body_errors(validation_errors)
90
- validation_errors.map do |error|
91
- {
92
- source: {
93
- pointer: error['data_pointer']
94
- }
95
- }.update(ValidationFormat.error_details(error))
96
- end
97
- end
81
+ def validate_query_params!(operation, env)
82
+ query_parameters = operation.query_parameters
83
+ return if operation.query_parameters.empty?
98
84
 
99
- def build_json_schema(parameter_defs)
100
- init_schema = {
101
- 'type' => 'object',
102
- 'properties' => {},
103
- 'required' => []
104
- }
105
- parameter_defs.each_with_object(init_schema) do |parameter_def, schema|
106
- parameter = OpenapiParameters::Parameter.new(parameter_def)
107
- schema['properties'][parameter.name] = parameter.schema if parameter.schema
108
- schema['required'] << parameter.name if parameter.required?
109
- end
85
+ unpacked_query_params = OpenapiParameters::Query.new(query_parameters).unpack(env['QUERY_STRING'])
86
+ validation_result = operation.schemas.query_parameters_schema.validate(unpacked_query_params)
87
+ OpenapiFirst.error!(400, :query, validation_result:) if validation_result.error?
88
+ env[QUERY_PARAMS] = unpacked_query_params
89
+ env[PARAMS].merge!(unpacked_query_params)
110
90
  end
111
91
 
112
- def validate_query_parameters!(operation, params)
113
- parameter_defs = operation.query_parameters
114
- return unless parameter_defs&.any?
92
+ def validate_cookie_params!(operation, env)
93
+ cookie_parameters = operation.cookie_parameters
94
+ return unless cookie_parameters&.any?
115
95
 
116
- json_schema = build_json_schema(parameter_defs)
117
- errors = SchemaValidation.new(json_schema).validate(params)
118
- throw_error(400, serialize_parameter_errors(errors)) if errors.any?
96
+ unpacked_params = OpenapiParameters::Cookie.new(cookie_parameters).unpack(env['HTTP_COOKIE'])
97
+ validation_result = operation.schemas.cookie_parameters_schema.validate(unpacked_params)
98
+ OpenapiFirst.error!(400, :cookie, validation_result:) if validation_result.error?
99
+ env[COOKIE_PARAMS] = unpacked_params
119
100
  end
120
101
 
121
- def serialize_parameter_errors(validation_errors)
122
- validation_errors.map do |error|
123
- pointer = error['data_pointer'][1..].to_s
124
- {
125
- source: { parameter: pointer }
126
- }.update(ValidationFormat.error_details(error))
127
- end
102
+ def validate_header_params!(operation, env)
103
+ header_parameters = operation.header_parameters
104
+ return if header_parameters.empty?
105
+
106
+ unpacked_header_params = OpenapiParameters::Header.new(header_parameters).unpack_env(env)
107
+ validation_result = operation.schemas.header_parameters_schema.validate(unpacked_header_params)
108
+ OpenapiFirst.error!(400, :header, validation_result:) if validation_result.error?
109
+ env[HEADER_PARAMS] = unpacked_header_params
128
110
  end
129
111
  end
130
112
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'multi_json'
4
4
  require_relative 'use_router'
5
- require_relative 'validation_format'
6
5
 
7
6
  module OpenapiFirst
8
7
  class ResponseValidation
@@ -26,8 +25,9 @@ module OpenapiFirst
26
25
  return validate_status_only(operation, status) if status == 204
27
26
 
28
27
  content_type = headers[Rack::CONTENT_TYPE]
29
- response_schema = operation.response_schema_for(status, content_type)
28
+ response_schema = operation.response_body_schema(status, content_type)
30
29
  validate_response_body(response_schema, body) if response_schema
30
+ validate_response_headers(operation, status, headers)
31
31
  end
32
32
 
33
33
  private
@@ -40,14 +40,45 @@ module OpenapiFirst
40
40
  full_body = +''
41
41
  response.each { |chunk| full_body << chunk }
42
42
  data = full_body.empty? ? {} : load_json(full_body)
43
- errors = schema.validate(data)
44
- errors = errors.to_a.map! do |error|
45
- format_error(error)
43
+ validation = schema.validate(data)
44
+ raise ResponseBodyInvalidError, validation.message if validation.error?
45
+ end
46
+
47
+ def validate_response_headers(operation, status, response_headers)
48
+ response_header_definitions = operation.response_for(status)&.dig('headers')
49
+ return unless response_header_definitions
50
+
51
+ unpacked_headers = unpack_response_headers(response_header_definitions, response_headers)
52
+ response_header_definitions.each do |name, definition|
53
+ next if name == 'Content-Type'
54
+
55
+ validate_response_header(name, definition, unpacked_headers, openapi_version: operation.openapi_version)
56
+ end
57
+ end
58
+
59
+ def validate_response_header(name, definition, unpacked_headers, openapi_version:)
60
+ unless unpacked_headers.key?(name)
61
+ raise ResponseHeaderInvalidError, "Required response header '#{name}' is missing" if definition['required']
62
+
63
+ return
64
+ end
65
+
66
+ return unless definition.key?('schema')
67
+
68
+ validation = SchemaValidation.new(definition['schema'], openapi_version:)
69
+ value = unpacked_headers[name]
70
+ validation_result = validation.validate(value)
71
+ raise ResponseHeaderInvalidError, validation_result.message if validation_result.error?
72
+ end
73
+
74
+ def unpack_response_headers(response_header_definitions, response_headers)
75
+ headers_as_parameters = response_header_definitions.map do |name, definition|
76
+ definition.merge('name' => name)
46
77
  end
47
- raise ResponseBodyInvalidError, errors.join(', ') if errors.any?
78
+ OpenapiParameters::Header.new(headers_as_parameters).unpack(response_headers)
48
79
  end
49
80
 
50
- def format_error(error)
81
+ def format_response_error(error)
51
82
  return "Write-only field appears in response: #{error['data_pointer']}" if error['type'] == 'writeOnly'
52
83
 
53
84
  JSONSchemer::Errors.pretty(error)
@@ -7,7 +7,7 @@ module OpenapiFirst
7
7
  class ResponseValidator
8
8
  def initialize(spec)
9
9
  @spec = spec
10
- @router = Router.new(->(_env) {}, spec: spec, raise_error: true)
10
+ @router = Router.new(->(_env) {}, spec:, raise_error: true)
11
11
  @response_validation = ResponseValidation.new(->(response) { response.to_a })
12
12
  end
13
13
 
@@ -7,6 +7,9 @@ require_relative 'body_parser_middleware'
7
7
 
8
8
  module OpenapiFirst
9
9
  class Router
10
+ # The unconverted path parameters before they are converted to the types defined in the API description
11
+ RAW_PATH_PARAMS = 'openapi.raw_path_params'
12
+
10
13
  def initialize(
11
14
  app,
12
15
  options
@@ -63,17 +66,16 @@ module OpenapiFirst
63
66
  env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
64
67
  end
65
68
 
66
- def handle_body_parsing_error(exception)
67
- err = { title: 'Failed to parse body as application/json', status: '400' }
68
- err[:detail] = exception.cause unless ENV['RACK_ENV'] == 'production'
69
- errors = [err]
70
- raise RequestInvalidError, errors if @raise
71
-
72
- Rack::Response.new(
73
- MultiJson.dump(errors: errors),
74
- 400,
75
- Rack::CONTENT_TYPE => 'application/vnd.api+json'
76
- ).finish
69
+ def handle_body_parsing_error(_exception)
70
+ message = 'Failed to parse body as application/json'
71
+ raise RequestInvalidError, message if @raise
72
+
73
+ error = {
74
+ status: 400,
75
+ title: message
76
+ }
77
+
78
+ ErrorResponse::Default.new(**error).finish
77
79
  end
78
80
 
79
81
  def build_router(operations)
@@ -89,7 +91,7 @@ module OpenapiFirst
89
91
  end
90
92
  raise_error = @raise
91
93
  Rack::Builder.app do
92
- use BodyParserMiddleware, raise_error: raise_error
94
+ use(BodyParserMiddleware, raise_error:)
93
95
  run router
94
96
  end
95
97
  end
@@ -99,8 +101,7 @@ module OpenapiFirst
99
101
  env[OPERATION] = operation
100
102
  path_info = env.delete(ORIGINAL_PATH)
101
103
  env[REQUEST_BODY] = env.delete(ROUTER_PARSED_BODY) if env.key?(ROUTER_PARSED_BODY)
102
- route_params = Utils::StringKeyedHash.new(env['router.params'])
103
- env[PARAMS] = OpenapiParameters::Path.new(operation.path_parameters).unpack(route_params)
104
+ env[RAW_PATH_PARAMS] = env['router.params']
104
105
  env[Rack::PATH_INFO] = path_info
105
106
  @app.call(env)
106
107
  end
@@ -1,46 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json_schemer'
4
+ require_relative 'validation_result'
4
5
 
5
6
  module OpenapiFirst
6
7
  class SchemaValidation
7
8
  attr_reader :raw_schema
8
9
 
9
- def initialize(schema, write: true)
10
+ SCHEMAS = {
11
+ '3.1' => 'https://spec.openapis.org/oas/3.1/dialect/base',
12
+ '3.0' => 'json-schemer://openapi30/schema'
13
+ }.freeze
14
+
15
+ def initialize(schema, openapi_version:, write: true)
10
16
  @raw_schema = schema
11
- custom_keywords = {}
12
- custom_keywords['writeOnly'] = proc { |data| !data } unless write
13
- custom_keywords['readOnly'] = proc { |data| !data } if write
14
17
  @schemer = JSONSchemer.schema(
15
18
  schema,
16
- keywords: custom_keywords,
19
+ access_mode: write ? 'write' : 'read',
20
+ meta_schema: SCHEMAS.fetch(openapi_version),
17
21
  insert_property_defaults: true,
18
- before_property_validation: proc do |data, property, property_schema, parent|
19
- convert_nullable(data, property, property_schema, parent)
20
- binary_format(data, property, property_schema, parent)
21
- end
22
+ output_format: 'detailed',
23
+ before_property_validation: method(:before_property_validation)
22
24
  )
23
25
  end
24
26
 
25
- def validate(input)
26
- @schemer.validate(input)
27
+ def validate(data)
28
+ ValidationResult.new(
29
+ output: @schemer.validate(data),
30
+ schema: raw_schema,
31
+ data:
32
+ )
27
33
  end
28
34
 
29
35
  private
30
36
 
31
- def binary_format(data, property, property_schema, _parent)
32
- return unless property_schema.is_a?(Hash) && property_schema['format'] == 'binary'
33
-
34
- property_schema['type'] = 'object'
35
- property_schema.delete('format')
36
- data[property].transform_keys!(&:to_s)
37
+ def before_property_validation(data, property, property_schema, parent)
38
+ binary_format(data, property, property_schema, parent)
37
39
  end
38
40
 
39
- def convert_nullable(_data, _property, property_schema, _parent)
40
- return unless property_schema.is_a?(Hash) && property_schema['nullable'] && property_schema['type']
41
+ def binary_format(data, property, property_schema, _parent)
42
+ return unless property_schema.is_a?(Hash) && property_schema['format'] == 'binary'
41
43
 
42
- property_schema['type'] = [*property_schema['type'], 'null']
43
- property_schema.delete('nullable')
44
+ data[property] = data[property][:tempfile].read
44
45
  end
45
46
  end
46
47
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ class StringKeyedHash
5
+ extend Forwardable
6
+ def_delegators :@orig, :empty?
7
+
8
+ def initialize(original)
9
+ @orig = original
10
+ end
11
+
12
+ def key?(key)
13
+ @orig.key?(key.to_sym)
14
+ end
15
+
16
+ def [](key)
17
+ @orig[key.to_sym]
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ ValidationResult = Struct.new(:output, :schema, :data, keyword_init: true) do
5
+ def valid? = output['valid']
6
+ def error? = !output['valid']
7
+
8
+ # Returns a message that is used in exception messages.
9
+ def message
10
+ return if valid?
11
+
12
+ (output['errors']&.map { |e| e['error'] }&.join('. ') || output['error'])&.concat('.')
13
+ end
14
+ end
15
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '1.0.0.beta3'
4
+ VERSION = '1.0.0.beta4'
5
5
  end