openapi_first 1.0.0.beta6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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