openapi_first 1.4.3 → 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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -1
  3. data/README.md +105 -28
  4. data/lib/openapi_first/body_parser.rb +8 -11
  5. data/lib/openapi_first/builder.rb +81 -0
  6. data/lib/openapi_first/configuration.rb +24 -3
  7. data/lib/openapi_first/definition.rb +44 -100
  8. data/lib/openapi_first/error_response.rb +2 -2
  9. data/lib/openapi_first/error_responses/default.rb +73 -0
  10. data/lib/openapi_first/error_responses/jsonapi.rb +59 -0
  11. data/lib/openapi_first/errors.rb +26 -4
  12. data/lib/openapi_first/failure.rb +29 -26
  13. data/lib/openapi_first/json_refs.rb +1 -3
  14. data/lib/openapi_first/middlewares/request_validation.rb +2 -2
  15. data/lib/openapi_first/middlewares/response_validation.rb +4 -3
  16. data/lib/openapi_first/request.rb +92 -0
  17. data/lib/openapi_first/request_parser.rb +35 -0
  18. data/lib/openapi_first/request_validator.rb +25 -0
  19. data/lib/openapi_first/response.rb +57 -0
  20. data/lib/openapi_first/response_parser.rb +49 -0
  21. data/lib/openapi_first/response_validator.rb +27 -0
  22. data/lib/openapi_first/router/find_content.rb +17 -0
  23. data/lib/openapi_first/router/find_response.rb +45 -0
  24. data/lib/openapi_first/{definition → router}/path_template.rb +9 -1
  25. data/lib/openapi_first/router.rb +100 -0
  26. data/lib/openapi_first/schema/validation_error.rb +16 -10
  27. data/lib/openapi_first/schema/validation_result.rb +8 -6
  28. data/lib/openapi_first/schema.rb +4 -8
  29. data/lib/openapi_first/test/methods.rb +21 -0
  30. data/lib/openapi_first/test.rb +19 -0
  31. data/lib/openapi_first/validated_request.rb +81 -0
  32. data/lib/openapi_first/validated_response.rb +33 -0
  33. data/lib/openapi_first/validators/request_body.rb +39 -0
  34. data/lib/openapi_first/validators/request_parameters.rb +61 -0
  35. data/lib/openapi_first/validators/response_body.rb +30 -0
  36. data/lib/openapi_first/validators/response_headers.rb +25 -0
  37. data/lib/openapi_first/version.rb +1 -1
  38. data/lib/openapi_first.rb +40 -21
  39. metadata +25 -20
  40. data/lib/openapi_first/definition/operation.rb +0 -197
  41. data/lib/openapi_first/definition/path_item.rb +0 -40
  42. data/lib/openapi_first/definition/request_body.rb +0 -46
  43. data/lib/openapi_first/definition/response.rb +0 -32
  44. data/lib/openapi_first/definition/responses.rb +0 -87
  45. data/lib/openapi_first/plugins/default/error_response.rb +0 -74
  46. data/lib/openapi_first/plugins/default.rb +0 -11
  47. data/lib/openapi_first/plugins/jsonapi/error_response.rb +0 -60
  48. data/lib/openapi_first/plugins/jsonapi.rb +0 -11
  49. data/lib/openapi_first/plugins.rb +0 -25
  50. data/lib/openapi_first/request_validation/request_body_validator.rb +0 -41
  51. data/lib/openapi_first/request_validation/validator.rb +0 -82
  52. data/lib/openapi_first/response_validation/validator.rb +0 -98
  53. data/lib/openapi_first/runtime_request.rb +0 -166
  54. 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 Definition
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
- class ValidationError
7
- def initialize(json_schemer_error)
8
- @error = json_schemer_error
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
- def error = @error['error']
12
- alias message error
13
- def schemer_error = @error
14
- def instance_location = @error['data_pointer']
15
- def schema_location = @error['schema_pointer']
16
- def type = @error['type']
17
- def details = @error['details']
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, schema:, data:)
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(err)
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
@@ -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:, write: true)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '1.4.3'
4
+ VERSION = '2.0.0'
5
5
  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/error_response'
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
- extend Plugins
16
+ # Key in rack to find instance of Request
17
+ REQUEST = 'openapi.request'
18
+ FAILURE = :openapi_first_validation_failure
18
19
 
19
- class << self
20
- # @return [Configuration]
21
- def configuration
22
- @configuration ||= Configuration.new
23
- end
20
+ # @return [Configuration]
21
+ def self.configuration
22
+ @configuration ||= Configuration.new
23
+ end
24
24
 
25
- # @return [Configuration]
26
- # @yield [Configuration]
27
- def configure
28
- yield configuration
29
- end
25
+ # @return [Configuration]
26
+ # @yield [Configuration]
27
+ def self.configure
28
+ yield configuration
30
29
  end
31
30
 
32
- # Key in rack to find instance of RuntimeRequest
33
- REQUEST = 'openapi.request'
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
- OpenapiFirst.plugin(:default)
59
- OpenapiFirst.plugin(:jsonapi)
76
+ require_relative 'openapi_first/error_response'
77
+ require_relative 'openapi_first/error_responses/default'
78
+ require_relative 'openapi_first/error_responses/jsonapi'