openapi_first 1.0.0.beta5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +2 -1
  3. data/CHANGELOG.md +23 -2
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +16 -18
  6. data/Gemfile.rack2 +15 -0
  7. data/Gemfile.rack2.lock +99 -0
  8. data/README.md +99 -130
  9. data/lib/openapi_first/body_parser.rb +29 -0
  10. data/lib/openapi_first/configuration.rb +20 -0
  11. data/lib/openapi_first/definition/cookie_parameters.rb +12 -0
  12. data/lib/openapi_first/definition/header_parameters.rb +12 -0
  13. data/lib/openapi_first/definition/operation.rb +116 -0
  14. data/lib/openapi_first/definition/parameters.rb +47 -0
  15. data/lib/openapi_first/definition/path_item.rb +32 -0
  16. data/lib/openapi_first/definition/path_parameters.rb +12 -0
  17. data/lib/openapi_first/definition/query_parameters.rb +12 -0
  18. data/lib/openapi_first/definition/request_body.rb +43 -0
  19. data/lib/openapi_first/definition/response.rb +25 -0
  20. data/lib/openapi_first/definition/responses.rb +83 -0
  21. data/lib/openapi_first/definition.rb +61 -8
  22. data/lib/openapi_first/error_response.rb +22 -27
  23. data/lib/openapi_first/errors.rb +2 -14
  24. data/lib/openapi_first/failure.rb +55 -0
  25. data/lib/openapi_first/middlewares/request_validation.rb +52 -0
  26. data/lib/openapi_first/middlewares/response_validation.rb +35 -0
  27. data/lib/openapi_first/plugins/default/error_response.rb +74 -0
  28. data/lib/openapi_first/plugins/default.rb +11 -0
  29. data/lib/openapi_first/plugins/jsonapi/error_response.rb +58 -0
  30. data/lib/openapi_first/plugins/jsonapi.rb +11 -0
  31. data/lib/openapi_first/plugins.rb +9 -7
  32. data/lib/openapi_first/request_validation/request_body_validator.rb +41 -0
  33. data/lib/openapi_first/request_validation/validator.rb +81 -0
  34. data/lib/openapi_first/response_validation/validator.rb +101 -0
  35. data/lib/openapi_first/runtime_request.rb +84 -0
  36. data/lib/openapi_first/runtime_response.rb +31 -0
  37. data/lib/openapi_first/schema/validation_error.rb +18 -0
  38. data/lib/openapi_first/schema/validation_result.rb +32 -0
  39. data/lib/openapi_first/{json_schema.rb → schema.rb} +9 -5
  40. data/lib/openapi_first/version.rb +1 -1
  41. data/lib/openapi_first.rb +32 -28
  42. data/openapi_first.gemspec +10 -9
  43. metadata +55 -67
  44. data/.rspec +0 -3
  45. data/.rubocop.yml +0 -14
  46. data/Rakefile +0 -15
  47. data/benchmarks/Gemfile +0 -16
  48. data/benchmarks/Gemfile.lock +0 -142
  49. data/benchmarks/README.md +0 -29
  50. data/benchmarks/apps/committee_with_hanami_api.ru +0 -26
  51. data/benchmarks/apps/committee_with_response_validation.ru +0 -29
  52. data/benchmarks/apps/committee_with_sinatra.ru +0 -31
  53. data/benchmarks/apps/grape.ru +0 -21
  54. data/benchmarks/apps/hanami_api.ru +0 -21
  55. data/benchmarks/apps/hanami_router.ru +0 -14
  56. data/benchmarks/apps/openapi.yaml +0 -268
  57. data/benchmarks/apps/openapi_first_with_hanami_api.ru +0 -24
  58. data/benchmarks/apps/openapi_first_with_plain_rack.ru +0 -32
  59. data/benchmarks/apps/openapi_first_with_response_validation.ru +0 -25
  60. data/benchmarks/apps/openapi_first_with_sinatra.ru +0 -29
  61. data/benchmarks/apps/roda.ru +0 -27
  62. data/benchmarks/apps/sinatra.ru +0 -26
  63. data/benchmarks/apps/syro.ru +0 -25
  64. data/benchmarks/benchmark-wrk.sh +0 -3
  65. data/benchmarks/benchmarks.rb +0 -48
  66. data/benchmarks/post.lua +0 -3
  67. data/bin/console +0 -15
  68. data/bin/setup +0 -8
  69. data/examples/README.md +0 -13
  70. data/examples/app.rb +0 -18
  71. data/examples/config.ru +0 -7
  72. data/examples/openapi.yaml +0 -29
  73. data/lib/openapi_first/body_parser_middleware.rb +0 -40
  74. data/lib/openapi_first/config.rb +0 -20
  75. data/lib/openapi_first/error_responses/default.rb +0 -58
  76. data/lib/openapi_first/error_responses/json_api.rb +0 -58
  77. data/lib/openapi_first/json_schema/result.rb +0 -17
  78. data/lib/openapi_first/operation.rb +0 -170
  79. data/lib/openapi_first/request_body_validator.rb +0 -41
  80. data/lib/openapi_first/request_validation.rb +0 -118
  81. data/lib/openapi_first/request_validation_error.rb +0 -31
  82. data/lib/openapi_first/response_validation.rb +0 -93
  83. data/lib/openapi_first/response_validator.rb +0 -21
  84. data/lib/openapi_first/router.rb +0 -102
  85. data/lib/openapi_first/string_keyed_hash.rb +0 -20
  86. data/lib/openapi_first/use_router.rb +0 -18
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ module OpenapiFirst
5
+ module Middlewares
6
+ # A Rack middleware to validate requests against an OpenAPI API description
7
+ class ResponseValidation
8
+ # @param app The parent Rack application
9
+ # @param options Hash
10
+ # :spec Path to the OpenAPI file or an instance of Definition
11
+ def initialize(app, options = {})
12
+ @app = app
13
+
14
+ spec = options.fetch(:spec)
15
+ raise "You have to pass spec: when initializing #{self.class}" unless spec
16
+
17
+ @definition = spec.is_a?(Definition) ? spec : OpenapiFirst.load(spec)
18
+ end
19
+
20
+ def call(env)
21
+ request = find_request(env)
22
+ response = @app.call(env)
23
+ request.response(response).validate!
24
+
25
+ response
26
+ end
27
+
28
+ private
29
+
30
+ def find_request(env)
31
+ env[REQUEST] ||= @definition.request(Rack::Request.new(env))
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ module Plugins
5
+ module Default
6
+ # An error reponse that returns application/problem+json with a list of "errors"
7
+ # See also https://www.rfc-editor.org/rfc/rfc9457.html
8
+ class ErrorResponse
9
+ include OpenapiFirst::ErrorResponse
10
+
11
+ TITLES = {
12
+ not_found: 'Not Found',
13
+ method_not_allowed: 'Request Method Not Allowed',
14
+ unsupported_media_type: 'Unsupported Media Type',
15
+ invalid_body: 'Bad Request Body',
16
+ invalid_query: 'Bad Query Parameter',
17
+ invalid_header: 'Bad Request Header',
18
+ invalid_path: 'Bad Request Path',
19
+ invalid_cookie: 'Bod Request Cookie'
20
+ }.freeze
21
+ private_constant :TITLES
22
+
23
+ def body
24
+ result = {
25
+ title:,
26
+ status:
27
+ }
28
+ result[:errors] = errors if failure.errors
29
+ MultiJson.dump(result)
30
+ end
31
+
32
+ def error_type = failure.error_type
33
+
34
+ def title
35
+ TITLES.fetch(error_type)
36
+ end
37
+
38
+ def content_type
39
+ 'application/problem+json'
40
+ end
41
+
42
+ def errors
43
+ key = pointer_key
44
+ failure.errors.map do |error|
45
+ {
46
+ message: error.error,
47
+ key => pointer(error.instance_location),
48
+ code: error.type
49
+ }
50
+ end
51
+ end
52
+
53
+ def pointer_key
54
+ case error_type
55
+ when :invalid_body
56
+ :pointer
57
+ when :invalid_query, :invalid_path
58
+ :parameter
59
+ when :invalid_header
60
+ :header
61
+ when :invalid_cookie
62
+ :cookie
63
+ end
64
+ end
65
+
66
+ def pointer(data_pointer)
67
+ return data_pointer if error_type == :invalid_body
68
+
69
+ data_pointer.delete_prefix('/')
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'default/error_response'
4
+
5
+ module OpenapiFirst
6
+ module Plugins
7
+ module Default
8
+ OpenapiFirst.register(:default, self)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ module Plugins
5
+ module Jsonapi
6
+ class ErrorResponse
7
+ include OpenapiFirst::ErrorResponse
8
+
9
+ def body
10
+ MultiJson.dump({ errors: serialized_errors })
11
+ end
12
+
13
+ def content_type
14
+ 'application/vnd.api+json'
15
+ end
16
+
17
+ def serialized_errors
18
+ return default_errors unless failure.errors
19
+
20
+ key = pointer_key
21
+ failure.errors.map do |error|
22
+ {
23
+ status: status.to_s,
24
+ source: { key => pointer(error.instance_location) },
25
+ title: error.error
26
+ }
27
+ end
28
+ end
29
+
30
+ def default_errors
31
+ [{
32
+ status: status.to_s,
33
+ title: Rack::Utils::HTTP_STATUS_CODES[status]
34
+ }]
35
+ end
36
+
37
+ def pointer_key
38
+ case failure.error_type
39
+ when :invalid_body
40
+ :pointer
41
+ when :invalid_query, :invalid_path
42
+ :parameter
43
+ when :invalid_header
44
+ :header
45
+ when :invalid_cookie
46
+ :cookie
47
+ end
48
+ end
49
+
50
+ def pointer(data_pointer)
51
+ return data_pointer if failure.error_type == :invalid_body
52
+
53
+ data_pointer.delete_prefix('/')
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'jsonapi/error_response'
4
+
5
+ module OpenapiFirst
6
+ module Plugins
7
+ module Jsonapi
8
+ OpenapiFirst.register(:jsonapi, self)
9
+ end
10
+ end
11
+ end
@@ -1,17 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
+ # Plugin System adapted from
5
+ # Polished Ruby Programming by Jeremy Evans
6
+ # https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewBook?id=0
4
7
  module Plugins
5
- ERROR_RESPONSES = {} # rubocop:disable Style/MutableConstant
8
+ PLUGINS = {} # rubocop:disable Style/MutableConstant
6
9
 
7
- def self.register_error_response(name, klass)
8
- ERROR_RESPONSES[name] = klass
10
+ def register(name, klass)
11
+ PLUGINS[name] = klass
9
12
  end
10
13
 
11
- def self.find_error_response(name)
12
- return name if name.is_a?(Class)
13
-
14
- ERROR_RESPONSES.fetch(name)
14
+ def plugin(name)
15
+ require "openapi_first/plugins/#{name}"
16
+ PLUGINS.fetch(name)
15
17
  end
16
18
  end
17
19
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../failure'
4
+
5
+ module OpenapiFirst
6
+ module RequestValidation
7
+ class RequestBodyValidator
8
+ def initialize(operation)
9
+ @operation = operation
10
+ end
11
+
12
+ def validate!(parsed_request_body, request_content_type)
13
+ request_body = operation.request_body
14
+ schema = request_body.schema_for(request_content_type)
15
+ unless schema
16
+ Failure.fail!(:unsupported_media_type,
17
+ message: "Unsupported Media Type '#{request_content_type}'")
18
+ end
19
+
20
+ if request_body.required? && parsed_request_body.nil?
21
+ Failure.fail!(:invalid_body,
22
+ message: 'Request body is not defined')
23
+ end
24
+
25
+ validate_body!(parsed_request_body, schema)
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :operation
31
+
32
+ def validate_body!(parsed_request_body, schema)
33
+ request_body_schema = schema
34
+ return unless request_body_schema
35
+
36
+ validation = request_body_schema.validate(parsed_request_body)
37
+ Failure.fail!(:invalid_body, errors: validation.errors) if validation.error?
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../failure'
4
+ require_relative 'request_body_validator'
5
+
6
+ module OpenapiFirst
7
+ module RequestValidation
8
+ class Validator
9
+ def initialize(operation)
10
+ @operation = operation
11
+ end
12
+
13
+ def validate(runtime_request)
14
+ catch Failure::FAILURE do
15
+ validate_defined(runtime_request)
16
+ validate_parameters!(runtime_request)
17
+ validate_request_body!(runtime_request)
18
+ nil
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :operation, :raw_path_params
25
+
26
+ def validate_defined(request)
27
+ return if request.known?
28
+ return Failure.fail!(:not_found) unless request.known_path?
29
+
30
+ Failure.fail!(:method_not_allowed) unless request.known_request_method?
31
+ end
32
+
33
+ def validate_parameters!(request)
34
+ validate_query_params!(request)
35
+ validate_path_params!(request)
36
+ validate_cookie_params!(request)
37
+ validate_header_params!(request)
38
+ end
39
+
40
+ def validate_path_params!(request)
41
+ parameters = operation.path_parameters
42
+ return unless parameters
43
+
44
+ validation = parameters.schema.validate(request.path_parameters)
45
+ Failure.fail!(:invalid_path, errors: validation.errors) if validation.error?
46
+ end
47
+
48
+ def validate_query_params!(request)
49
+ parameters = operation.query_parameters
50
+ return unless parameters
51
+
52
+ validation = parameters.schema.validate(request.query)
53
+ Failure.fail!(:invalid_query, errors: validation.errors) if validation.error?
54
+ end
55
+
56
+ def validate_cookie_params!(request)
57
+ parameters = operation.cookie_parameters
58
+ return unless parameters
59
+
60
+ validation = parameters.schema.validate(request.cookies)
61
+ Failure.fail!(:invalid_cookie, errors: validation.errors) if validation.error?
62
+ end
63
+
64
+ def validate_header_params!(request)
65
+ parameters = operation.header_parameters
66
+ return unless parameters
67
+
68
+ validation = parameters.schema.validate(request.headers)
69
+ Failure.fail!(:invalid_header, errors: validation.errors) if validation.error?
70
+ end
71
+
72
+ def validate_request_body!(request)
73
+ return unless operation.request_body
74
+
75
+ RequestBodyValidator.new(operation).validate!(request.body, request.content_type)
76
+ rescue BodyParser::ParsingError => e
77
+ Failure.fail!(:invalid_body, message: e.message)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../failure'
4
+
5
+ module OpenapiFirst
6
+ module ResponseValidation
7
+ class Validator
8
+ def initialize(operation)
9
+ @operation = operation
10
+ end
11
+
12
+ def validate(rack_response)
13
+ return unless operation
14
+
15
+ response = Rack::Response[*rack_response.to_a]
16
+ catch Failure::FAILURE do
17
+ response_definition = response_for(operation, response.status, response.content_type)
18
+ validate_response_body(response_definition.content_schema, response.body)
19
+ validate_response_headers(response_definition.headers, response.headers)
20
+ nil
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :operation
27
+
28
+ def response_for(operation, status, content_type)
29
+ response = operation.response_for(status, content_type)
30
+ return response if response
31
+
32
+ unless operation.response_status_defined?(status)
33
+ message = "Response status '#{status}' not found for '#{operation.name}'"
34
+ Failure.fail!(:response_not_found, message:)
35
+ end
36
+ if content_type.nil? || content_type.empty?
37
+ message = "Content-Type for '#{operation.name}' must not be empty"
38
+ Failure.fail!(:invalid_response_header, message:)
39
+ end
40
+
41
+ message = "Content-Type '#{content_type}' is not defined for '#{operation.name}'"
42
+ Failure.fail!(:invalid_response_header, message:)
43
+ end
44
+
45
+ def validate_response_body(schema, response)
46
+ return unless schema
47
+
48
+ full_body = +''
49
+ response.each { |chunk| full_body << chunk }
50
+ data = full_body.empty? ? {} : load_json(full_body)
51
+ validation = schema.validate(data)
52
+ Failure.fail!(:invalid_response_body, errors: validation.errors) if validation.error?
53
+ end
54
+
55
+ def validate_response_headers(response_header_definitions, response_headers)
56
+ return unless response_header_definitions
57
+
58
+ unpacked_headers = unpack_response_headers(response_header_definitions, response_headers)
59
+ response_header_definitions.each do |name, definition|
60
+ next if name == 'Content-Type'
61
+
62
+ validate_response_header(name, definition, unpacked_headers, openapi_version: operation.openapi_version)
63
+ end
64
+ end
65
+
66
+ def validate_response_header(name, definition, unpacked_headers, openapi_version:)
67
+ unless unpacked_headers.key?(name)
68
+ if definition['required']
69
+ Failure.fail!(:invalid_response_header,
70
+ message: "Required response header '#{name}' is missing")
71
+ end
72
+
73
+ return
74
+ end
75
+
76
+ return unless definition.key?('schema')
77
+
78
+ validation = Schema.new(definition['schema'], openapi_version:)
79
+ value = unpacked_headers[name]
80
+ validation_result = validation.validate(value)
81
+ return unless validation_result.error?
82
+
83
+ Failure.fail!(:invalid_response_header,
84
+ errors: validation_result.errors)
85
+ end
86
+
87
+ def unpack_response_headers(response_header_definitions, response_headers)
88
+ headers_as_parameters = response_header_definitions.map do |name, definition|
89
+ definition.merge('name' => name, 'in' => 'header')
90
+ end
91
+ OpenapiParameters::Header.new(headers_as_parameters).unpack(response_headers)
92
+ end
93
+
94
+ def load_json(string)
95
+ MultiJson.load(string)
96
+ rescue MultiJson::ParseError
97
+ string
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require_relative 'runtime_response'
5
+ require_relative 'body_parser'
6
+ require_relative 'request_validation/validator'
7
+
8
+ module OpenapiFirst
9
+ # RuntimeRequest represents how an incoming request (Rack::Request) matches a request definition.
10
+ class RuntimeRequest
11
+ extend Forwardable
12
+
13
+ def initialize(request:, path_item:, operation:, path_params:)
14
+ @request = request
15
+ @path_item = path_item
16
+ @operation = operation
17
+ @original_path_params = path_params
18
+ end
19
+
20
+ def_delegators :@request, :content_type, :media_type
21
+ def_delegators :@operation, :operation_id
22
+
23
+ def known?
24
+ known_path? && known_request_method?
25
+ end
26
+
27
+ def known_path?
28
+ !!path_item
29
+ end
30
+
31
+ def known_request_method?
32
+ !!operation
33
+ end
34
+
35
+ # Merged path and query parameters
36
+ def params
37
+ @params ||= query.merge(path_parameters)
38
+ end
39
+
40
+ def path_parameters
41
+ @path_parameters ||=
42
+ operation.path_parameters&.unpack(@original_path_params) || {}
43
+ end
44
+
45
+ def query
46
+ @query ||=
47
+ operation.query_parameters&.unpack(request.env) || {}
48
+ end
49
+
50
+ alias query_parameters query
51
+
52
+ def headers
53
+ @headers ||=
54
+ operation.header_parameters&.unpack(request.env) || {}
55
+ end
56
+
57
+ def cookies
58
+ @cookies ||=
59
+ operation.cookie_parameters&.unpack(request.env) || {}
60
+ end
61
+
62
+ def body
63
+ @body ||= BodyParser.new.parse(request, request.media_type)
64
+ end
65
+ alias parsed_body body
66
+
67
+ def validate
68
+ RequestValidation::Validator.new(operation).validate(self)
69
+ end
70
+
71
+ def validate!
72
+ error = validate
73
+ error&.raise!
74
+ end
75
+
76
+ def response(rack_response)
77
+ RuntimeResponse.new(operation, rack_response)
78
+ end
79
+
80
+ private
81
+
82
+ attr_reader :request, :operation, :path_item
83
+ end
84
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'response_validation/validator'
4
+
5
+ module OpenapiFirst
6
+ class RuntimeResponse
7
+ def initialize(operation, rack_response)
8
+ @operation = operation
9
+ @rack_response = rack_response
10
+ end
11
+
12
+ def description
13
+ response_definition&.description
14
+ end
15
+
16
+ def validate
17
+ ResponseValidation::Validator.new(@operation).validate(@rack_response)
18
+ end
19
+
20
+ def validate!
21
+ error = validate
22
+ error&.raise!
23
+ end
24
+
25
+ private
26
+
27
+ def response_definition
28
+ @response_definition ||= @operation.response_for(@rack_response.status, @rack_response.content_type)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ class Schema
5
+ class ValidationError
6
+ def initialize(json_schemer_error)
7
+ @error = json_schemer_error
8
+ end
9
+
10
+ def error = @error['error']
11
+ def schemer_error = @error
12
+ def instance_location = @error['data_pointer']
13
+ def schema_location = @error['schema_pointer']
14
+ def type = @error['type']
15
+ def details = @error['details']
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'validation_error'
4
+
5
+ module OpenapiFirst
6
+ class Schema
7
+ class ValidationResult
8
+ def initialize(validation, schema:, data:)
9
+ @validation = validation
10
+ @schema = schema
11
+ @data = data
12
+ end
13
+
14
+ attr_reader :schema, :data
15
+
16
+ def error? = @validation.any?
17
+
18
+ def errors
19
+ @errors ||= @validation.map do |err|
20
+ ValidationError.new(err)
21
+ end
22
+ end
23
+
24
+ # Returns a message that is used in exception messages.
25
+ def message
26
+ return unless error?
27
+
28
+ errors.map(&:error).join('. ')
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json_schemer'
4
- require_relative 'json_schema/result'
4
+ require_relative 'schema/validation_result'
5
5
 
6
6
  module OpenapiFirst
7
- class JsonSchema
7
+ class Schema
8
8
  attr_reader :schema
9
9
 
10
10
  SCHEMAS = {
@@ -19,19 +19,23 @@ module OpenapiFirst
19
19
  access_mode: write ? 'write' : 'read',
20
20
  meta_schema: SCHEMAS.fetch(openapi_version),
21
21
  insert_property_defaults: true,
22
- output_format: 'detailed',
22
+ output_format: 'classic',
23
23
  before_property_validation: method(:before_property_validation)
24
24
  )
25
25
  end
26
26
 
27
27
  def validate(data)
28
- Result.new(
29
- output: @schemer.validate(data),
28
+ ValidationResult.new(
29
+ @schemer.validate(data),
30
30
  schema:,
31
31
  data:
32
32
  )
33
33
  end
34
34
 
35
+ def [](key)
36
+ @schema[key]
37
+ end
38
+
35
39
  private
36
40
 
37
41
  def before_property_validation(data, property, property_schema, parent)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '1.0.0.beta5'
4
+ VERSION = '1.0.0'
5
5
  end