openapi_first 0.7.0.alpha1 → 3.1.0.beta1

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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +585 -2
  3. data/LICENSE.txt +21 -0
  4. data/README.md +360 -114
  5. data/lib/openapi_first/builder.rb +214 -0
  6. data/lib/openapi_first/child_configuration.rb +26 -0
  7. data/lib/openapi_first/configuration.rb +76 -0
  8. data/lib/openapi_first/definition.rb +98 -13
  9. data/lib/openapi_first/error_response.rb +40 -0
  10. data/lib/openapi_first/error_responses/default.rb +74 -0
  11. data/lib/openapi_first/error_responses/jsonapi.rb +60 -0
  12. data/lib/openapi_first/errors.rb +40 -0
  13. data/lib/openapi_first/failure.rb +77 -0
  14. data/lib/openapi_first/file_loader.rb +22 -0
  15. data/lib/openapi_first/header.rb +9 -0
  16. data/lib/openapi_first/json.rb +25 -0
  17. data/lib/openapi_first/middlewares/request_validation.rb +58 -0
  18. data/lib/openapi_first/middlewares/response_validation.rb +41 -0
  19. data/lib/openapi_first/parsed_request.rb +5 -0
  20. data/lib/openapi_first/ref_resolver.rb +193 -0
  21. data/lib/openapi_first/registry.rb +44 -0
  22. data/lib/openapi_first/request.rb +79 -0
  23. data/lib/openapi_first/request_body_parsers.rb +64 -0
  24. data/lib/openapi_first/request_validator.rb +32 -0
  25. data/lib/openapi_first/response.rb +43 -0
  26. data/lib/openapi_first/response_body_parsers.rb +31 -0
  27. data/lib/openapi_first/response_parser.rb +50 -0
  28. data/lib/openapi_first/response_validator.rb +16 -48
  29. data/lib/openapi_first/router/find_content.rb +17 -0
  30. data/lib/openapi_first/router/find_response.rb +53 -0
  31. data/lib/openapi_first/router/path_template.rb +48 -0
  32. data/lib/openapi_first/router.rb +84 -57
  33. data/lib/openapi_first/schema/hash.rb +39 -0
  34. data/lib/openapi_first/schema/validation_error.rb +45 -0
  35. data/lib/openapi_first/schema/validation_result.rb +30 -0
  36. data/lib/openapi_first/test/app.rb +30 -0
  37. data/lib/openapi_first/test/callable.rb +25 -0
  38. data/lib/openapi_first/test/configuration.rb +76 -0
  39. data/lib/openapi_first/test/coverage/covered_request.rb +11 -0
  40. data/lib/openapi_first/test/coverage/covered_response.rb +11 -0
  41. data/lib/openapi_first/test/coverage/plan.rb +77 -0
  42. data/lib/openapi_first/test/coverage/request_task.rb +42 -0
  43. data/lib/openapi_first/test/coverage/response_task.rb +42 -0
  44. data/lib/openapi_first/test/coverage/route_task.rb +13 -0
  45. data/lib/openapi_first/test/coverage/terminal_formatter.rb +115 -0
  46. data/lib/openapi_first/test/coverage/tracker.rb +32 -0
  47. data/lib/openapi_first/test/coverage.rb +102 -0
  48. data/lib/openapi_first/test/methods.rb +62 -0
  49. data/lib/openapi_first/test/minitest_helpers.rb +28 -0
  50. data/lib/openapi_first/test/observe.rb +37 -0
  51. data/lib/openapi_first/test/observer_middleware.rb +23 -0
  52. data/lib/openapi_first/test/plain_helpers.rb +31 -0
  53. data/lib/openapi_first/test.rb +171 -0
  54. data/lib/openapi_first/validated_request.rb +79 -0
  55. data/lib/openapi_first/validated_response.rb +53 -0
  56. data/lib/openapi_first/validators/request_body.rb +25 -0
  57. data/lib/openapi_first/validators/request_parameters.rb +49 -0
  58. data/lib/openapi_first/validators/response_body.rb +27 -0
  59. data/lib/openapi_first/validators/response_headers.rb +40 -0
  60. data/lib/openapi_first/version.rb +1 -1
  61. data/lib/openapi_first.rb +67 -35
  62. metadata +99 -133
  63. data/.github/CODEOWNERS +0 -1
  64. data/.gitignore +0 -11
  65. data/.rspec +0 -3
  66. data/.rubocop.yml +0 -14
  67. data/.travis.yml +0 -8
  68. data/Gemfile +0 -11
  69. data/Gemfile.lock +0 -126
  70. data/Rakefile +0 -15
  71. data/benchmarks/Gemfile +0 -13
  72. data/benchmarks/Gemfile.lock +0 -144
  73. data/benchmarks/apps/committee.ru +0 -30
  74. data/benchmarks/apps/grape.ru +0 -21
  75. data/benchmarks/apps/hanami_router.ru +0 -14
  76. data/benchmarks/apps/openapi.yaml +0 -86
  77. data/benchmarks/apps/openapi_first.ru +0 -22
  78. data/benchmarks/apps/openapi_first_resolve_only.ru +0 -23
  79. data/benchmarks/apps/sinatra.ru +0 -26
  80. data/benchmarks/apps/syro.ru +0 -25
  81. data/benchmarks/benchmarks.rb +0 -46
  82. data/bin/console +0 -15
  83. data/bin/setup +0 -8
  84. data/examples/README.md +0 -13
  85. data/examples/app.rb +0 -12
  86. data/examples/config.ru +0 -7
  87. data/examples/openapi.yaml +0 -29
  88. data/lib/openapi_first/app.rb +0 -27
  89. data/lib/openapi_first/coverage.rb +0 -28
  90. data/lib/openapi_first/operation.rb +0 -48
  91. data/lib/openapi_first/operation_resolver.rb +0 -43
  92. data/lib/openapi_first/request_validation.rb +0 -144
  93. data/lib/openapi_first/utils.rb +0 -31
  94. data/lib/openapi_first/validation.rb +0 -15
  95. data/lib/openapi_first/validation_format.rb +0 -31
  96. data/openapi_first.gemspec +0 -46
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ module Test
5
+ # Assertion methods to use when no known test framework was found
6
+ # These methods just raise an exception if an error was found
7
+ module PlainHelpers
8
+ def assert_api_conform(status: nil, api: openapi_first_default_api)
9
+ api = OpenapiFirst::Test[api]
10
+ # :nocov:
11
+ request = respond_to?(:last_request) ? last_request : @request
12
+ response = respond_to?(:last_response) ? last_response : @response
13
+ # :nocov:
14
+
15
+ if status && status != response.status
16
+ raise OpenapiFirst::Error,
17
+ "Expected status #{status}, but got #{response.status} " \
18
+ "from #{request.request_method.upcase} #{request.path}."
19
+ end
20
+
21
+ validated = api.validate_request(request, raise_error: false)
22
+ # :nocov:
23
+ raise validated.error.exception if validated.invalid?
24
+
25
+ validated = api.validate_response(request, response, raise_error: false)
26
+ raise validated.error.exception if validated.invalid?
27
+ # :nocov:
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test/configuration'
4
+ require_relative 'registry'
5
+
6
+ module OpenapiFirst
7
+ # Test integration
8
+ module Test # rubocop:disable Metrics/ModuleLength
9
+ autoload :Coverage, 'openapi_first/test/coverage'
10
+ autoload :Methods, 'openapi_first/test/methods'
11
+ autoload :Observe, 'openapi_first/test/observe'
12
+ autoload :App, 'openapi_first/test/app'
13
+ extend Registry
14
+
15
+ class CoverageError < Error; end
16
+ class UnknownQueryParameterError < Error; end
17
+
18
+ # Inject request/response validation in a rack app class
19
+ def self.observe(app, api: :default)
20
+ Observe.observe(app, api:)
21
+ end
22
+
23
+ def self.minitest?(base)
24
+ base.include?(::Minitest::Assertions)
25
+ rescue NameError
26
+ false
27
+ end
28
+
29
+ def self.definitions
30
+ super.empty? ? OpenapiFirst.definitions : super
31
+ end
32
+
33
+ def self.configuration
34
+ @configuration ||= Configuration.new
35
+ end
36
+
37
+ def self.registered?(oad)
38
+ key = oad.key
39
+ definitions.any? { |(_name, registered)| registered.key == key }
40
+ end
41
+
42
+ # Sets up OpenAPI test coverage and OAD registration.
43
+ # @yieldparam [OpenapiFirst::Test::Configuration] configuration A configuration to setup test integration
44
+ def self.setup
45
+ install
46
+ yield configuration if block_given?
47
+
48
+ Coverage.start(skip_response: configuration.skip_response_coverage, skip_route: configuration.skip_coverage)
49
+
50
+ if definitions.empty?
51
+ raise NotRegisteredError,
52
+ 'No API descriptions have been registered. ' \
53
+ 'Please register your API description via ' \
54
+ "`OpenapiFirst.register('myopenapi.yaml)` or " \
55
+ 'in a block passed to `OpenapiFirst::Test.setup` like this: ' \
56
+ "`OpenapiFirst::Test.setup { |test| test.register('myopenapi.yaml') }` " \
57
+
58
+ end
59
+
60
+ @exit_handler = method(:handle_exit)
61
+
62
+ main_process = Process.pid
63
+ @setup ||= at_exit do
64
+ # :nocov:
65
+ # Only handle exit once in the main process
66
+ @exit_handler&.call if Process.pid == main_process
67
+ # :nocov:
68
+ end
69
+ end
70
+
71
+ def self.handle_exit
72
+ return unless configuration.report_coverage
73
+
74
+ report_coverage(
75
+ formatter: configuration.coverage_formatter,
76
+ **configuration.coverage_formatter_options
77
+ )
78
+ return unless configuration.report_coverage == true
79
+
80
+ coverage = Coverage.result.coverage
81
+ return if coverage >= configuration.minimum_coverage
82
+
83
+ puts "API Coverage fails with exit 2, because not all described requests and responses have been tested (#{coverage.round(4)}% covered)." # rubocop:disable Layout/LineLength
84
+
85
+ exit 2
86
+ end
87
+
88
+ # Print the coverage report
89
+ # @param formatter A formatter to define the report.
90
+ # @output [IO] An output where to puts the report.
91
+ def self.report_coverage(formatter: Coverage::TerminalFormatter, **)
92
+ puts formatter.new(**).format(Coverage.result)
93
+ end
94
+
95
+ # Returns the Rack app wrapped with silent request, response validation
96
+ # You can use this if you want to track coverage via Test::Coverage, but don't want to use
97
+ # the middlewares or manual request, response validation.
98
+ def self.app(app, spec: nil, api: :default)
99
+ spec ||= self[api]
100
+ App.new(app, api: spec)
101
+ end
102
+
103
+ def self.install
104
+ return if @installed
105
+
106
+ OpenapiFirst.configure do |config|
107
+ @after_request_validation = config.after_request_validation do |validated_request, oad|
108
+ next unless registered?(oad)
109
+ raise validated_request.error.exception if raise_request_error?(validated_request)
110
+
111
+ check_unknown_query_parameters(validated_request)
112
+
113
+ Coverage.track_request(validated_request, oad)
114
+ end
115
+
116
+ @after_response_validation = config.after_response_validation do |validated_response, rack_request, oad|
117
+ next unless registered?(oad)
118
+ if validated_response.invalid? && raise_response_error?(validated_response)
119
+ raise validated_response.error.exception
120
+ end
121
+
122
+ Coverage.track_response(validated_response, rack_request, oad)
123
+ end
124
+ end
125
+ @installed = true
126
+ end
127
+
128
+ def self.uninstall
129
+ configuration = OpenapiFirst.configuration
130
+ configuration.after_request_validation.delete(@after_request_validation)
131
+ configuration.after_response_validation.delete(@after_response_validation)
132
+ definitions.clear
133
+ @configuration = nil
134
+ @installed = nil
135
+ @exit_handler = nil
136
+ end
137
+
138
+ class << self
139
+ private
140
+
141
+ def check_unknown_query_parameters(validated_request)
142
+ return unless validated_request.known?
143
+
144
+ unknown_parameters = validated_request.unknown_query_parameters
145
+ return unless unknown_parameters
146
+
147
+ message = unknown_parameters_message(unknown_parameters, validated_request)
148
+ raise UnknownQueryParameterError, message
149
+ end
150
+
151
+ def unknown_parameters_message(unknown_parameters, validated_request)
152
+ s = 's' if many?(unknown_parameters)
153
+ list = unknown_parameters.keys.map(&:inspect).join(', ')
154
+ "Unknown query parameter#{s} #{list} for #{validated_request.fullpath}"
155
+ end
156
+
157
+ def raise_request_error?(validated_request)
158
+ return false if validated_request.valid?
159
+ return false if validated_request.known?
160
+
161
+ !configuration.ignore_unknown_requests
162
+ end
163
+
164
+ def many?(array) = array.length > 1
165
+
166
+ def raise_response_error?(invalid_response)
167
+ configuration.response_raise_error && !configuration.ignore_response?(invalid_response)
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,79 @@
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_request: nil, request_definition: nil, query_parser: nil)
12
+ super(original_request)
13
+ @parsed_request = parsed_request
14
+ @error = error
15
+ @request_definition = request_definition
16
+ @query_parser = query_parser
17
+ end
18
+
19
+ # A Failure object if the request is invalid
20
+ # @return [Failure, nil]
21
+ attr_reader :error
22
+
23
+ # The request definition if this request is defined in the API description
24
+ # @return [Request, nil]
25
+ attr_reader :request_definition
26
+
27
+ # @!method operation_id
28
+ # @return [String, nil] The OpenAPI 3 operationId
29
+ def_delegator :request_definition, :operation_id
30
+
31
+ # @!method operation
32
+ # @return [Hash] The raw OpenAPI 3 operation object
33
+ def_delegator :request_definition, :operation
34
+
35
+ # @return [Hash] Query parameters and values that are not defined in the OpenAPI spec.
36
+ def unknown_query_parameters
37
+ @query_parser&.unknown_values(query_string)
38
+ end
39
+
40
+ # Parsed path parameters
41
+ # @return [Hash<String, anything>]
42
+ def parsed_path_parameters = @parsed_request&.path || {}
43
+
44
+ # Parsed query parameters. This only returns the query parameters that are defined in the OpenAPI spec.
45
+ # @return [Hash<String, anything>]
46
+ def parsed_query = @parsed_request&.query || {}
47
+
48
+ # Parsed headers. This only returns the query parameters that are defined in the OpenAPI spec.
49
+ # @return [Hash<String, anything>]
50
+ def parsed_headers = @parsed_request&.headers || {}
51
+
52
+ # Parsed cookies. This only returns the query parameters that are defined in the OpenAPI spec.
53
+ # @return [Hash<String, anything>]
54
+ def parsed_cookies = @parsed_request&.cookies || {}
55
+
56
+ # Parsed body. This parses the body according to the content type.
57
+ # Note that this returns the hole body, not only the fields that are defined in the OpenAPI spec.
58
+ # You can use JSON Schemas `additionalProperties` or `unevaluatedProperties` to
59
+ # return a validation error if the body contains unknown fields.
60
+ # @return [Hash<String, anything>, anything]
61
+ def parsed_body = @parsed_request&.body
62
+
63
+ # Checks if the request is valid.
64
+ def valid? = error.nil?
65
+
66
+ # Checks if the request is invalid.
67
+ def invalid? = !valid?
68
+
69
+ # Returns true if the request is defined.
70
+ def known? = request_definition != nil
71
+
72
+ # Merged path, query, body parameters.
73
+ # Here path has the highest precedence, then query, then body.
74
+ # @return [Hash<String, anything>]
75
+ def parsed_params
76
+ @parsed_params ||= parsed_body.to_h.merge(parsed_query, parsed_path_parameters) || {}
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,53 @@
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
+ # A Failure object if the response is invalid
19
+ # @return [Failure, nil]
20
+ attr_reader :error
21
+
22
+ # The response definition if this response is defined in the API description
23
+ # @return [Response, nil]
24
+ attr_reader :response_definition
25
+
26
+ # The parsed headers
27
+ # @return [Hash<String,anything>, nil]
28
+ def parsed_headers
29
+ @parsed_values&.headers
30
+ end
31
+
32
+ # The parsed body
33
+ # @return [Hash<String,anything>, nil]
34
+ def parsed_body
35
+ @parsed_values&.body
36
+ end
37
+
38
+ # Checks if the response is valid.
39
+ # @return [Boolean] true if the response is valid, false otherwise.
40
+ def valid?
41
+ error.nil?
42
+ end
43
+
44
+ # Returns true if the response is defined.
45
+ def known? = response_definition != nil
46
+
47
+ # Checks if the response is invalid.
48
+ # @return [Boolean]
49
+ def invalid?
50
+ !valid?
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ module Validators
5
+ class RequestBody
6
+ def initialize(content_schema:, required_request_body:)
7
+ @schema = content_schema
8
+ @required = required_request_body
9
+ end
10
+
11
+ def call(parsed_request)
12
+ body = parsed_request.body
13
+ if body.nil?
14
+ Failure.fail!(:invalid_body, message: 'Request body must not be empty') if @required
15
+ return
16
+ end
17
+
18
+ validation = Schema::ValidationResult.new(
19
+ @schema.validate(body, access_mode: 'write')
20
+ )
21
+ Failure.fail!(:invalid_body, errors: validation.errors) if validation.error?
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ module Validators
5
+ module RequestParameters
6
+ RequestHeaders = Data.define(:schema) do
7
+ def call(parsed_request)
8
+ validation = schema.validate(parsed_request.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_request)
15
+ validation = schema.validate(parsed_request.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_request)
22
+ validation = schema.validate(parsed_request.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_request)
29
+ validation = schema.validate(parsed_request.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(args)
42
+ VALIDATORS.filter_map do |key, klass|
43
+ schema = args[key]
44
+ klass.new(schema) if schema
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../schema/validation_result'
4
+
5
+ module OpenapiFirst
6
+ module Validators
7
+ class ResponseBody
8
+ def initialize(schema)
9
+ @schema = schema
10
+ end
11
+
12
+ attr_reader :schema
13
+
14
+ def call(response)
15
+ begin
16
+ parsed_body = response.body
17
+ rescue ParseError => e
18
+ Failure.fail!(:invalid_response_body, message: e.message)
19
+ end
20
+ validation = Schema::ValidationResult.new(
21
+ schema.validate(parsed_body, access_mode: 'read')
22
+ )
23
+ Failure.fail!(:invalid_response_body, errors: validation.errors) if validation.error?
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ module Validators
5
+ class ResponseHeaders
6
+ def initialize(headers)
7
+ @headers = headers
8
+ end
9
+
10
+ attr_reader :headers
11
+
12
+ def call(parsed_response)
13
+ headers.each do |header|
14
+ header_value = parsed_response.headers[header.name]
15
+ next if header_value.nil? && !header.required?
16
+
17
+ validation_errors = header.schema.validate(header_value)
18
+ next unless validation_errors.any?
19
+
20
+ Failure.fail!(:invalid_response_header,
21
+ errors: [error_for(data_pointer: "/#{header.name}", value: header_value,
22
+ error: validation_errors.first)])
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def error_for(data_pointer:, value:, error:)
29
+ Schema::ValidationError.new(
30
+ value: value,
31
+ data_pointer:,
32
+ schema_pointer: error['schema_pointer'],
33
+ type: error['type'],
34
+ details: error['details'],
35
+ schema: error['schema']
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.7.0.alpha1'
4
+ VERSION = '3.1.0.beta1'
5
5
  end
data/lib/openapi_first.rb CHANGED
@@ -1,50 +1,82 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'yaml'
4
- require 'oas_parser'
5
- require 'openapi_first/definition'
6
- require 'openapi_first/version'
7
- require 'openapi_first/router'
8
- require 'openapi_first/request_validation'
9
- require 'openapi_first/response_validator'
10
- require 'openapi_first/operation_resolver'
11
- require 'openapi_first/app'
3
+ require_relative 'openapi_first/json'
4
+ require_relative 'openapi_first/file_loader'
5
+ require_relative 'openapi_first/errors'
6
+ require_relative 'openapi_first/registry'
7
+ require_relative 'openapi_first/configuration'
8
+ require_relative 'openapi_first/child_configuration'
9
+ require_relative 'openapi_first/definition'
10
+ require_relative 'openapi_first/version'
11
+ require_relative 'openapi_first/middlewares/response_validation'
12
+ require_relative 'openapi_first/middlewares/request_validation'
12
13
 
14
+ # OpenapiFirst is a toolchain to build HTTP APIS based on OpenAPI API descriptions.
13
15
  module OpenapiFirst
14
- OPERATION = 'openapi_first.operation'
15
- PARAMS = 'openapi_first.params'
16
- REQUEST_BODY = 'openapi_first.parsed_request_body'
17
- HANDLER = 'openapi_first.handler'
18
-
19
- def self.load(spec_path, only: nil)
20
- content = YAML.load_file(spec_path)
21
- raw = OasParser::Parser.new(spec_path, content).resolve
22
- raw['paths'].filter!(&->(key, _) { only.call(key) }) if only
23
- parsed = OasParser::Definition.new(raw, spec_path)
24
- Definition.new(parsed)
16
+ extend Registry
17
+
18
+ autoload :Test, 'openapi_first/test'
19
+
20
+ # Key in rack to find instance of Request
21
+ REQUEST = 'openapi.request'
22
+
23
+ FAILURE = :openapi_first_validation_failure
24
+
25
+ # @return [Configuration]
26
+ def self.configuration
27
+ @configuration ||= Configuration.new
25
28
  end
26
29
 
27
- def self.app(spec, namespace:)
28
- spec = OpenapiFirst.load(spec) if spec.is_a?(String)
29
- App.new(nil, spec, namespace: namespace)
30
+ # @return [Configuration]
31
+ # @yield [Configuration]
32
+ def self.configure
33
+ yield configuration if block_given?
30
34
  end
31
35
 
32
- def self.middleware(spec, namespace:)
33
- spec = OpenapiFirst.load(spec) if spec.is_a?(String)
34
- AppWithOptions.new(spec, namespace: namespace)
36
+ ERROR_RESPONSES = {} # rubocop:disable Style/MutableConstant
37
+ private_constant :ERROR_RESPONSES
38
+
39
+ # Register an error response class
40
+ # @param name [Symbol]
41
+ # @param klass [Class] A class that includes / implements OpenapiFirst::ErrorResponse
42
+ def self.register_error_response(name, klass)
43
+ ERROR_RESPONSES[name.to_sym] = klass
35
44
  end
36
45
 
37
- class AppWithOptions
38
- def initialize(spec, options)
39
- @spec = spec
40
- @options = options
46
+ # @param name [Symbol]
47
+ # @return [Class] The error response class
48
+ def self.find_error_response(name)
49
+ ERROR_RESPONSES.fetch(name) do
50
+ raise "Unknown error response: #{name}. " \
51
+ 'Register your error response class via `OpenapiFirst.register_error_response(name, klass)`. ' \
52
+ "Registered error responses are: #{ERROR_RESPONSES.keys.join(', ')}."
41
53
  end
54
+ end
42
55
 
43
- def new(app)
44
- App.new(app, @spec, **@options)
45
- end
56
+ # Load and dereference an OpenAPI spec file or return the Definition if it's already loaded
57
+ # @param filepath_or_definition [String, Definition] The path to the file or a Definition object
58
+ # @return [Definition]
59
+ def self.load(filepath_or_definition, only: nil, &)
60
+ return filepath_or_definition if filepath_or_definition.is_a?(Definition)
61
+ return self[filepath_or_definition] if filepath_or_definition.is_a?(Symbol)
62
+
63
+ filepath = filepath_or_definition
64
+ raise FileNotFoundError, "File not found: #{filepath}" unless File.exist?(filepath)
65
+
66
+ contents = FileLoader.load(filepath)
67
+ parse(contents, only:, filepath:, &)
46
68
  end
47
69
 
48
- class Error < StandardError; end
49
- # Your code goes here...
70
+ # Parse a dereferenced Hash
71
+ # @return [Definition]
72
+ # TODO: This needs to work with unresolved contents as well
73
+ def self.parse(contents, only: nil, filepath: nil, &)
74
+ contents = ::JSON.parse(::JSON.generate(contents)) # Deeply stringify keys, because of YAML. See https://github.com/ahx/openapi_first/issues/367
75
+ contents['paths'].filter!(&->(key, _) { only.call(key) }) if only
76
+ Definition.new(contents, filepath, &)
77
+ end
50
78
  end
79
+
80
+ require_relative 'openapi_first/error_response'
81
+ require_relative 'openapi_first/error_responses/default'
82
+ require_relative 'openapi_first/error_responses/jsonapi'