openapi_first 1.0.0.beta4 → 1.0.0.beta6

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +2 -1
  3. data/CHANGELOG.md +13 -0
  4. data/Gemfile +2 -1
  5. data/Gemfile.lock +17 -22
  6. data/Gemfile.rack2 +15 -0
  7. data/README.md +17 -7
  8. data/lib/openapi_first/body_parser.rb +28 -0
  9. data/lib/openapi_first/config.rb +4 -3
  10. data/lib/openapi_first/definition/cookie_parameters.rb +12 -0
  11. data/lib/openapi_first/definition/has_content.rb +37 -0
  12. data/lib/openapi_first/definition/header_parameters.rb +12 -0
  13. data/lib/openapi_first/definition/operation.rb +103 -0
  14. data/lib/openapi_first/definition/parameters.rb +47 -0
  15. data/lib/openapi_first/definition/path_item.rb +23 -0
  16. data/lib/openapi_first/definition/path_parameters.rb +13 -0
  17. data/lib/openapi_first/definition/query_parameters.rb +12 -0
  18. data/lib/openapi_first/definition/request_body.rb +32 -0
  19. data/lib/openapi_first/definition/response.rb +37 -0
  20. data/lib/openapi_first/definition/schema/result.rb +17 -0
  21. data/lib/openapi_first/{schema_validation.rb → definition/schema.rb} +6 -6
  22. data/lib/openapi_first/definition.rb +26 -6
  23. data/lib/openapi_first/error_response.rb +28 -12
  24. data/lib/openapi_first/error_responses/default.rb +58 -0
  25. data/lib/openapi_first/error_responses/json_api.rb +58 -0
  26. data/lib/openapi_first/request_body_validator.rb +18 -22
  27. data/lib/openapi_first/request_validation.rb +68 -58
  28. data/lib/openapi_first/request_validation_error.rb +31 -0
  29. data/lib/openapi_first/response_validation.rb +33 -13
  30. data/lib/openapi_first/response_validator.rb +1 -0
  31. data/lib/openapi_first/router.rb +20 -62
  32. data/lib/openapi_first/version.rb +1 -1
  33. data/lib/openapi_first.rb +2 -13
  34. data/openapi_first.gemspec +8 -5
  35. metadata +44 -57
  36. data/.rspec +0 -3
  37. data/.rubocop.yml +0 -14
  38. data/Rakefile +0 -15
  39. data/benchmarks/Gemfile +0 -16
  40. data/benchmarks/Gemfile.lock +0 -131
  41. data/benchmarks/README.md +0 -29
  42. data/benchmarks/apps/committee_with_hanami_api.ru +0 -26
  43. data/benchmarks/apps/committee_with_response_validation.ru +0 -29
  44. data/benchmarks/apps/committee_with_sinatra.ru +0 -31
  45. data/benchmarks/apps/grape.ru +0 -21
  46. data/benchmarks/apps/hanami_api.ru +0 -21
  47. data/benchmarks/apps/hanami_router.ru +0 -14
  48. data/benchmarks/apps/openapi.yaml +0 -268
  49. data/benchmarks/apps/openapi_first_with_hanami_api.ru +0 -24
  50. data/benchmarks/apps/openapi_first_with_plain_rack.ru +0 -32
  51. data/benchmarks/apps/openapi_first_with_response_validation.ru +0 -25
  52. data/benchmarks/apps/openapi_first_with_sinatra.ru +0 -29
  53. data/benchmarks/apps/roda.ru +0 -27
  54. data/benchmarks/apps/sinatra.ru +0 -26
  55. data/benchmarks/apps/syro.ru +0 -25
  56. data/benchmarks/benchmark-wrk.sh +0 -3
  57. data/benchmarks/benchmarks.rb +0 -48
  58. data/benchmarks/post.lua +0 -3
  59. data/bin/console +0 -15
  60. data/bin/setup +0 -8
  61. data/examples/README.md +0 -13
  62. data/examples/app.rb +0 -18
  63. data/examples/config.ru +0 -7
  64. data/examples/openapi.yaml +0 -29
  65. data/lib/openapi_first/body_parser_middleware.rb +0 -53
  66. data/lib/openapi_first/default_error_response.rb +0 -47
  67. data/lib/openapi_first/operation.rb +0 -142
  68. data/lib/openapi_first/operation_schemas.rb +0 -52
  69. data/lib/openapi_first/string_keyed_hash.rb +0 -20
  70. data/lib/openapi_first/validation_result.rb +0 -15
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OpenapiFirst
4
- class DefaultErrorResponse < ErrorResponse
5
- OpenapiFirst::Plugins.register_error_response(:default, self)
6
-
7
- def body
8
- MultiJson.dump({ errors: serialized_errors })
9
- end
10
-
11
- def serialized_errors
12
- return default_errors unless validation_output
13
-
14
- key = pointer_key
15
- [
16
- {
17
- source: { key => pointer(validation_output['instanceLocation']) },
18
- title: validation_output['error']
19
- }
20
- ]
21
- end
22
-
23
- def default_errors
24
- [{
25
- status: status.to_s,
26
- title:
27
- }]
28
- end
29
-
30
- def pointer_key
31
- case location
32
- when :request_body
33
- :pointer
34
- when :query, :path
35
- :parameter
36
- else
37
- location
38
- end
39
- end
40
-
41
- def pointer(data_pointer)
42
- return data_pointer if location == :request_body
43
-
44
- data_pointer.delete_prefix('/')
45
- end
46
- end
47
- end
@@ -1,142 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'forwardable'
4
- require 'set'
5
- require_relative 'schema_validation'
6
- require_relative 'operation_schemas'
7
-
8
- module OpenapiFirst
9
- class Operation # rubocop:disable Metrics/ClassLength
10
- extend Forwardable
11
- def_delegators :operation_object,
12
- :[],
13
- :dig
14
-
15
- WRITE_METHODS = Set.new(%w[post put patch delete]).freeze
16
- private_constant :WRITE_METHODS
17
-
18
- attr_reader :path, :method, :openapi_version
19
-
20
- def initialize(path, request_method, path_item_object, openapi_version:)
21
- @path = path
22
- @method = request_method
23
- @path_item_object = path_item_object
24
- @openapi_version = openapi_version
25
- end
26
-
27
- def operation_id
28
- operation_object['operationId']
29
- end
30
-
31
- def read?
32
- !write?
33
- end
34
-
35
- def write?
36
- WRITE_METHODS.include?(method)
37
- end
38
-
39
- def request_body
40
- operation_object['requestBody']
41
- end
42
-
43
- def response_body_schema(status, content_type)
44
- content = response_for(status)['content']
45
- return if content.nil? || content.empty?
46
-
47
- raise ResponseInvalid, "Response has no content-type for '#{name}'" unless content_type
48
-
49
- media_type = find_content_for_content_type(content, content_type)
50
-
51
- unless media_type
52
- message = "Response content type not found '#{content_type}' for '#{name}'"
53
- raise ResponseContentTypeNotFoundError, message
54
- end
55
- schema = media_type['schema']
56
- return unless schema
57
-
58
- SchemaValidation.new(schema, write: false, openapi_version:)
59
- end
60
-
61
- def request_body_schema(request_content_type)
62
- (@request_body_schema ||= {})[request_content_type] ||= begin
63
- content = operation_object.dig('requestBody', 'content')
64
- media_type = find_content_for_content_type(content, request_content_type)
65
- schema = media_type&.fetch('schema', nil)
66
- SchemaValidation.new(schema, write: write?, openapi_version:) if schema
67
- end
68
- end
69
-
70
- def response_for(status)
71
- response_content = response_by_code(status)
72
- return response_content if response_content
73
-
74
- message = "Response status code or default not found: #{status} for '#{name}'"
75
- raise OpenapiFirst::ResponseCodeNotFoundError, message
76
- end
77
-
78
- def name
79
- @name ||= "#{method.upcase} #{path} (#{operation_id})"
80
- end
81
-
82
- def valid_request_content_type?(request_content_type)
83
- content = operation_object.dig('requestBody', 'content')
84
- return false unless content
85
-
86
- !!find_content_for_content_type(content, request_content_type)
87
- end
88
-
89
- def query_parameters
90
- @query_parameters ||= all_parameters.filter { |p| p['in'] == 'query' }
91
- end
92
-
93
- def path_parameters
94
- @path_parameters ||= all_parameters.filter { |p| p['in'] == 'path' }
95
- end
96
-
97
- IGNORED_HEADERS = Set['Content-Type', 'Accept', 'Authorization'].freeze
98
- private_constant :IGNORED_HEADERS
99
-
100
- def header_parameters
101
- @header_parameters ||= all_parameters.filter { |p| p['in'] == 'header' && !IGNORED_HEADERS.include?(p['name']) }
102
- end
103
-
104
- def cookie_parameters
105
- @cookie_parameters ||= all_parameters.filter { |p| p['in'] == 'cookie' }
106
- end
107
-
108
- def all_parameters
109
- @all_parameters ||= begin
110
- parameters = @path_item_object['parameters']&.dup || []
111
- parameters_on_operation = operation_object['parameters']
112
- parameters.concat(parameters_on_operation) if parameters_on_operation
113
- parameters
114
- end
115
- end
116
-
117
- # visibility: private
118
- def schemas
119
- @schemas ||= OperationSchemas.new(self)
120
- end
121
-
122
- private
123
-
124
- def response_by_code(status)
125
- operation_object.dig('responses', status.to_s) ||
126
- operation_object.dig('responses', "#{status / 100}XX") ||
127
- operation_object.dig('responses', "#{status / 100}xx") ||
128
- operation_object.dig('responses', 'default')
129
- end
130
-
131
- def operation_object
132
- @path_item_object[method]
133
- end
134
-
135
- def find_content_for_content_type(content, request_content_type)
136
- content.fetch(request_content_type) do |_|
137
- type = request_content_type.split(';')[0]
138
- content[type] || content["#{type.split('/')[0]}/*"] || content['*/*']
139
- end
140
- end
141
- end
142
- end
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'openapi_parameters/parameter'
4
- require_relative 'schema_validation'
5
-
6
- module OpenapiFirst
7
- # This class is basically a cache for JSON Schemas of parameters
8
- class OperationSchemas
9
- # @operation [OpenapiFirst::Operation]
10
- def initialize(operation)
11
- @operation = operation
12
- end
13
-
14
- attr_reader :operation
15
-
16
- # Return JSON Schema of for all query parameters
17
- def query_parameters_schema
18
- @query_parameters_schema ||= build_json_schema(operation.query_parameters)
19
- end
20
-
21
- # Return JSON Schema of for all path parameters
22
- def path_parameters_schema
23
- @path_parameters_schema ||= build_json_schema(operation.path_parameters)
24
- end
25
-
26
- def header_parameters_schema
27
- @header_parameters_schema ||= build_json_schema(operation.header_parameters)
28
- end
29
-
30
- def cookie_parameters_schema
31
- @cookie_parameters_schema ||= build_json_schema(operation.cookie_parameters)
32
- end
33
-
34
- private
35
-
36
- # Build JSON Schema for given parameter definitions
37
- # @parameter_defs [Array<Hash>] Parameter definitions
38
- def build_json_schema(parameter_defs)
39
- init_schema = {
40
- 'type' => 'object',
41
- 'properties' => {},
42
- 'required' => []
43
- }
44
- schema = parameter_defs.each_with_object(init_schema) do |parameter_def, result|
45
- parameter = OpenapiParameters::Parameter.new(parameter_def)
46
- result['properties'][parameter.name] = parameter.schema if parameter.schema
47
- result['required'] << parameter.name if parameter.required?
48
- end
49
- SchemaValidation.new(schema, openapi_version: operation.openapi_version)
50
- end
51
- end
52
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OpenapiFirst
4
- class StringKeyedHash
5
- extend Forwardable
6
- def_delegators :@orig, :empty?
7
-
8
- def initialize(original)
9
- @orig = original
10
- end
11
-
12
- def key?(key)
13
- @orig.key?(key.to_sym)
14
- end
15
-
16
- def [](key)
17
- @orig[key.to_sym]
18
- end
19
- end
20
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OpenapiFirst
4
- ValidationResult = Struct.new(:output, :schema, :data, keyword_init: true) do
5
- def valid? = output['valid']
6
- def error? = !output['valid']
7
-
8
- # Returns a message that is used in exception messages.
9
- def message
10
- return if valid?
11
-
12
- (output['errors']&.map { |e| e['error'] }&.join('. ') || output['error'])&.concat('.')
13
- end
14
- end
15
- end