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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +2 -1
- data/CHANGELOG.md +13 -0
- data/Gemfile +2 -1
- data/Gemfile.lock +17 -22
- data/Gemfile.rack2 +15 -0
- data/README.md +17 -7
- data/lib/openapi_first/body_parser.rb +28 -0
- data/lib/openapi_first/config.rb +4 -3
- data/lib/openapi_first/definition/cookie_parameters.rb +12 -0
- data/lib/openapi_first/definition/has_content.rb +37 -0
- data/lib/openapi_first/definition/header_parameters.rb +12 -0
- data/lib/openapi_first/definition/operation.rb +103 -0
- data/lib/openapi_first/definition/parameters.rb +47 -0
- data/lib/openapi_first/definition/path_item.rb +23 -0
- data/lib/openapi_first/definition/path_parameters.rb +13 -0
- data/lib/openapi_first/definition/query_parameters.rb +12 -0
- data/lib/openapi_first/definition/request_body.rb +32 -0
- data/lib/openapi_first/definition/response.rb +37 -0
- data/lib/openapi_first/definition/schema/result.rb +17 -0
- data/lib/openapi_first/{schema_validation.rb → definition/schema.rb} +6 -6
- data/lib/openapi_first/definition.rb +26 -6
- data/lib/openapi_first/error_response.rb +28 -12
- data/lib/openapi_first/error_responses/default.rb +58 -0
- data/lib/openapi_first/error_responses/json_api.rb +58 -0
- data/lib/openapi_first/request_body_validator.rb +18 -22
- data/lib/openapi_first/request_validation.rb +68 -58
- data/lib/openapi_first/request_validation_error.rb +31 -0
- data/lib/openapi_first/response_validation.rb +33 -13
- data/lib/openapi_first/response_validator.rb +1 -0
- data/lib/openapi_first/router.rb +20 -62
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +2 -13
- data/openapi_first.gemspec +8 -5
- metadata +44 -57
- data/.rspec +0 -3
- data/.rubocop.yml +0 -14
- data/Rakefile +0 -15
- data/benchmarks/Gemfile +0 -16
- data/benchmarks/Gemfile.lock +0 -131
- data/benchmarks/README.md +0 -29
- data/benchmarks/apps/committee_with_hanami_api.ru +0 -26
- data/benchmarks/apps/committee_with_response_validation.ru +0 -29
- data/benchmarks/apps/committee_with_sinatra.ru +0 -31
- data/benchmarks/apps/grape.ru +0 -21
- data/benchmarks/apps/hanami_api.ru +0 -21
- data/benchmarks/apps/hanami_router.ru +0 -14
- data/benchmarks/apps/openapi.yaml +0 -268
- data/benchmarks/apps/openapi_first_with_hanami_api.ru +0 -24
- data/benchmarks/apps/openapi_first_with_plain_rack.ru +0 -32
- data/benchmarks/apps/openapi_first_with_response_validation.ru +0 -25
- data/benchmarks/apps/openapi_first_with_sinatra.ru +0 -29
- data/benchmarks/apps/roda.ru +0 -27
- data/benchmarks/apps/sinatra.ru +0 -26
- data/benchmarks/apps/syro.ru +0 -25
- data/benchmarks/benchmark-wrk.sh +0 -3
- data/benchmarks/benchmarks.rb +0 -48
- data/benchmarks/post.lua +0 -3
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/examples/README.md +0 -13
- data/examples/app.rb +0 -18
- data/examples/config.ru +0 -7
- data/examples/openapi.yaml +0 -29
- data/lib/openapi_first/body_parser_middleware.rb +0 -53
- data/lib/openapi_first/default_error_response.rb +0 -47
- data/lib/openapi_first/operation.rb +0 -142
- data/lib/openapi_first/operation_schemas.rb +0 -52
- data/lib/openapi_first/string_keyed_hash.rb +0 -20
- 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
|