openapi_first 1.0.0.beta6 → 1.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -3
  3. data/Gemfile +1 -0
  4. data/Gemfile.lock +11 -10
  5. data/Gemfile.rack2.lock +99 -0
  6. data/README.md +99 -130
  7. data/lib/openapi_first/body_parser.rb +5 -4
  8. data/lib/openapi_first/configuration.rb +20 -0
  9. data/lib/openapi_first/definition/operation.rb +84 -71
  10. data/lib/openapi_first/definition/parameters.rb +1 -1
  11. data/lib/openapi_first/definition/path_item.rb +21 -12
  12. data/lib/openapi_first/definition/path_parameters.rb +2 -3
  13. data/lib/openapi_first/definition/request_body.rb +22 -11
  14. data/lib/openapi_first/definition/response.rb +19 -31
  15. data/lib/openapi_first/definition/responses.rb +83 -0
  16. data/lib/openapi_first/definition.rb +50 -17
  17. data/lib/openapi_first/error_response.rb +22 -29
  18. data/lib/openapi_first/errors.rb +2 -14
  19. data/lib/openapi_first/failure.rb +55 -0
  20. data/lib/openapi_first/middlewares/request_validation.rb +52 -0
  21. data/lib/openapi_first/middlewares/response_validation.rb +35 -0
  22. data/lib/openapi_first/plugins/default/error_response.rb +74 -0
  23. data/lib/openapi_first/plugins/default.rb +11 -0
  24. data/lib/openapi_first/plugins/jsonapi/error_response.rb +58 -0
  25. data/lib/openapi_first/plugins/jsonapi.rb +11 -0
  26. data/lib/openapi_first/plugins.rb +9 -7
  27. data/lib/openapi_first/request_validation/request_body_validator.rb +41 -0
  28. data/lib/openapi_first/request_validation/validator.rb +81 -0
  29. data/lib/openapi_first/response_validation/validator.rb +101 -0
  30. data/lib/openapi_first/runtime_request.rb +84 -0
  31. data/lib/openapi_first/runtime_response.rb +31 -0
  32. data/lib/openapi_first/schema/validation_error.rb +18 -0
  33. data/lib/openapi_first/schema/validation_result.rb +32 -0
  34. data/lib/openapi_first/{definition/schema.rb → schema.rb} +8 -4
  35. data/lib/openapi_first/version.rb +1 -1
  36. data/lib/openapi_first.rb +32 -28
  37. data/openapi_first.gemspec +3 -5
  38. metadata +28 -20
  39. data/lib/openapi_first/config.rb +0 -20
  40. data/lib/openapi_first/definition/has_content.rb +0 -37
  41. data/lib/openapi_first/definition/schema/result.rb +0 -17
  42. data/lib/openapi_first/error_responses/default.rb +0 -58
  43. data/lib/openapi_first/error_responses/json_api.rb +0 -58
  44. data/lib/openapi_first/request_body_validator.rb +0 -37
  45. data/lib/openapi_first/request_validation.rb +0 -122
  46. data/lib/openapi_first/request_validation_error.rb +0 -31
  47. data/lib/openapi_first/response_validation.rb +0 -113
  48. data/lib/openapi_first/response_validator.rb +0 -21
  49. data/lib/openapi_first/router.rb +0 -68
  50. data/lib/openapi_first/use_router.rb +0 -18
@@ -1,113 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'multi_json'
4
- require_relative 'use_router'
5
-
6
- module OpenapiFirst
7
- class ResponseValidation
8
- prepend UseRouter
9
-
10
- def initialize(app, _options = {})
11
- @app = app
12
- end
13
-
14
- def call(env)
15
- operation = env[OPERATION]
16
- return @app.call(env) unless operation
17
-
18
- response = @app.call(env)
19
- validate(response, operation)
20
- response
21
- end
22
-
23
- def validate(response, operation)
24
- status, headers, body = response.to_a
25
- response_definition = response_for(operation, status)
26
-
27
- validate_response_headers(response_definition.headers, headers, openapi_version: operation.openapi_version)
28
-
29
- return if no_content?(response_definition)
30
-
31
- content_type = Rack::Response[status, headers, body].content_type
32
- raise ResponseInvalid, "Response has no content-type for '#{operation.name}'" unless content_type
33
-
34
- response_schema = response_definition.schema_for(content_type)
35
- unless response_schema
36
- message = "Response content type not found '#{content_type}' for '#{operation.name}'"
37
- raise ResponseContentTypeNotFoundError, message
38
- end
39
- validate_response_body(response_schema, body)
40
- end
41
-
42
- private
43
-
44
- def no_content?(response_definition)
45
- response_definition.status == 204 || !response_definition.content?
46
- end
47
-
48
- def response_for(operation, status)
49
- response = operation.response_for(status)
50
- return response if response
51
-
52
- message = "Response status code or default not found: #{status} for '#{operation.name}'"
53
- raise OpenapiFirst::ResponseCodeNotFoundError, message
54
- end
55
-
56
- def validate_status_only(operation, status)
57
- response_for(operation, status)
58
- end
59
-
60
- def validate_response_body(schema, response)
61
- full_body = +''
62
- response.each { |chunk| full_body << chunk }
63
- data = full_body.empty? ? {} : load_json(full_body)
64
- validation = schema.validate(data)
65
- raise ResponseBodyInvalidError, validation.message if validation.error?
66
- end
67
-
68
- def validate_response_headers(response_header_definitions, response_headers, openapi_version:)
69
- return unless response_header_definitions
70
-
71
- unpacked_headers = unpack_response_headers(response_header_definitions, response_headers)
72
- response_header_definitions.each do |name, definition|
73
- next if name == 'Content-Type'
74
-
75
- validate_response_header(name, definition, unpacked_headers, openapi_version:)
76
- end
77
- end
78
-
79
- def validate_response_header(name, definition, unpacked_headers, openapi_version:)
80
- unless unpacked_headers.key?(name)
81
- raise ResponseHeaderInvalidError, "Required response header '#{name}' is missing" if definition['required']
82
-
83
- return
84
- end
85
-
86
- return unless definition.key?('schema')
87
-
88
- validation = Schema.new(definition['schema'], openapi_version:)
89
- value = unpacked_headers[name]
90
- schema_validation = validation.validate(value)
91
- raise ResponseHeaderInvalidError, schema_validation.message if schema_validation.error?
92
- end
93
-
94
- def unpack_response_headers(response_header_definitions, response_headers)
95
- headers_as_parameters = response_header_definitions.map do |name, definition|
96
- definition.merge('name' => name, 'in' => 'header')
97
- end
98
- OpenapiParameters::Header.new(headers_as_parameters).unpack(response_headers)
99
- end
100
-
101
- def format_response_error(error)
102
- return "Write-only field appears in response: #{error['data_pointer']}" if error['type'] == 'writeOnly'
103
-
104
- JSONSchemer::Errors.pretty(error)
105
- end
106
-
107
- def load_json(string)
108
- MultiJson.load(string)
109
- rescue MultiJson::ParseError
110
- string
111
- end
112
- end
113
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'response_validation'
4
- require_relative 'router'
5
-
6
- module OpenapiFirst
7
- # A class to run manual response validation
8
- class ResponseValidator
9
- def initialize(spec)
10
- @spec = spec
11
- @router = Router.new(->(_env) {}, spec:, raise_error: true)
12
- @response_validation = ResponseValidation.new(->(response) { response.to_a })
13
- end
14
-
15
- def validate(request, response)
16
- env = request.env.dup
17
- @router.call(env)
18
- @response_validation.validate(response, env[OPERATION])
19
- end
20
- end
21
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rack'
4
- require 'multi_json'
5
- require 'mustermann'
6
- require_relative 'body_parser'
7
-
8
- module OpenapiFirst
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
-
13
- NOT_FOUND = Rack::Response.new('Not Found', 404).finish.freeze
14
- METHOD_NOT_ALLOWED = Rack::Response.new('Method Not Allowed', 405).finish.freeze
15
-
16
- def initialize(
17
- app,
18
- options
19
- )
20
- @app = app
21
- @raise = options.fetch(:raise_error, false)
22
- @not_found = options.fetch(:not_found, :halt)
23
- @error_response_class = options.fetch(:error_response, Config.default_options.error_response)
24
- spec = options.fetch(:spec)
25
- raise "You have to pass spec: when initializing #{self.class}" unless spec
26
-
27
- @definition = spec.is_a?(Definition) ? spec : OpenapiFirst.load(spec)
28
- @filepath = @definition.filepath
29
- end
30
-
31
- def call(env)
32
- env[OPERATION] = nil
33
- request = Rack::Request.new(env)
34
- path_item, path_params = @definition.find_path_item_and_params(request.path)
35
- operation = path_item&.find_operation(request.request_method.downcase)
36
-
37
- env[OPERATION] = operation
38
- env[RAW_PATH_PARAMS] = path_params
39
-
40
- if operation.nil?
41
- raise_error(env) if @raise
42
- return @app.call(env) if @not_found == :continue
43
- end
44
-
45
- return NOT_FOUND unless path_item
46
- return METHOD_NOT_ALLOWED unless operation
47
-
48
- @app.call(env)
49
- end
50
-
51
- ORIGINAL_PATH = 'openapi_first.path_info'
52
- private_constant :ORIGINAL_PATH
53
-
54
- ROUTER_PARSED_BODY = 'router.parsed_body'
55
- private_constant :ROUTER_PARSED_BODY
56
-
57
- private
58
-
59
- def raise_error(env)
60
- req = Rack::Request.new(env)
61
- msg =
62
- "Could not find definition for #{req.request_method} '#{
63
- req.path
64
- }' in API description #{@filepath}"
65
- raise NotFoundError, msg
66
- end
67
- end
68
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OpenapiFirst
4
- module UseRouter
5
- def initialize(app, options = {})
6
- @app = app
7
- @options = options
8
- super
9
- end
10
-
11
- def call(env)
12
- return super if env.key?(OPERATION)
13
-
14
- @router ||= Router.new(->(e) { super(e) }, @options)
15
- @router.call(env)
16
- end
17
- end
18
- end