openapi_first 0.19.0 → 0.21.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +21 -1
- data/Gemfile.lock +43 -37
- data/README.md +47 -40
- data/benchmarks/Gemfile +1 -0
- data/benchmarks/Gemfile.lock +55 -52
- data/benchmarks/README.md +29 -0
- data/benchmarks/apps/openapi.yaml +182 -0
- data/benchmarks/apps/openapi_first_with_hanami_api.ru +1 -2
- data/benchmarks/benchmark-wrk.sh +3 -0
- data/benchmarks/benchmarks.rb +8 -3
- data/benchmarks/post.lua +3 -0
- data/lib/openapi_first/body_parser_middleware.rb +53 -0
- data/lib/openapi_first/default_operation_resolver.rb +16 -3
- data/lib/openapi_first/errors.rb +58 -0
- data/lib/openapi_first/inbox.rb +1 -1
- data/lib/openapi_first/operation.rb +25 -0
- data/lib/openapi_first/rack_responder.rb +2 -25
- data/lib/openapi_first/request_validation.rb +46 -44
- data/lib/openapi_first/responder.rb +5 -8
- data/lib/openapi_first/response_validation.rb +10 -9
- data/lib/openapi_first/router.rb +45 -17
- data/lib/openapi_first/schema_validation.rb +9 -0
- data/lib/openapi_first/use_router.rb +18 -0
- data/lib/openapi_first/version.rb +1 -1
- data/lib/openapi_first.rb +3 -48
- data/openapi_first.gemspec +3 -3
- metadata +17 -12
- data/lib/openapi_first/router_required.rb +0 -13
@@ -1,14 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'multi_json'
|
4
|
-
require_relative '
|
4
|
+
require_relative 'use_router'
|
5
5
|
require_relative 'validation_format'
|
6
6
|
|
7
7
|
module OpenapiFirst
|
8
8
|
class ResponseValidation
|
9
|
-
prepend
|
9
|
+
prepend UseRouter
|
10
10
|
|
11
|
-
def initialize(app)
|
11
|
+
def initialize(app, _options = {})
|
12
12
|
@app = app
|
13
13
|
end
|
14
14
|
|
@@ -42,20 +42,21 @@ module OpenapiFirst
|
|
42
42
|
data = full_body.empty? ? {} : load_json(full_body)
|
43
43
|
errors = schema.validate(data)
|
44
44
|
errors = errors.to_a.map! do |error|
|
45
|
-
|
45
|
+
format_error(error)
|
46
46
|
end
|
47
47
|
raise ResponseBodyInvalidError, errors.join(', ') if errors.any?
|
48
48
|
end
|
49
49
|
|
50
|
+
def format_error(error)
|
51
|
+
return "Write-only field appears in response: #{error['data_pointer']}" if error['type'] == 'writeOnly'
|
52
|
+
|
53
|
+
JSONSchemer::Errors.pretty(error)
|
54
|
+
end
|
55
|
+
|
50
56
|
def load_json(string)
|
51
57
|
MultiJson.load(string)
|
52
58
|
rescue MultiJson::ParseError
|
53
59
|
string
|
54
60
|
end
|
55
|
-
|
56
|
-
def error_message_for(error)
|
57
|
-
err = ValidationFormat.error_details(error)
|
58
|
-
[err[:title], error['data_pointer'], err[:detail]].compact.join(' ')
|
59
|
-
end
|
60
61
|
end
|
61
62
|
end
|
data/lib/openapi_first/router.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rack'
|
4
|
+
require 'multi_json'
|
4
5
|
require 'hanami/router'
|
6
|
+
require_relative 'body_parser_middleware'
|
5
7
|
|
6
8
|
module OpenapiFirst
|
7
9
|
class Router
|
@@ -14,6 +16,10 @@ module OpenapiFirst
|
|
14
16
|
@raise = options.fetch(:raise_error, false)
|
15
17
|
@not_found = options.fetch(:not_found, :halt)
|
16
18
|
spec = options.fetch(:spec)
|
19
|
+
raise "You have to pass spec: when initializing #{self.class}" unless spec
|
20
|
+
|
21
|
+
spec = OpenapiFirst.load(spec) unless spec.is_a?(Definition)
|
22
|
+
|
17
23
|
@filepath = spec.filepath
|
18
24
|
@router = build_router(spec.operations)
|
19
25
|
end
|
@@ -28,6 +34,7 @@ module OpenapiFirst
|
|
28
34
|
|
29
35
|
return @app.call(env) if @not_found == :continue
|
30
36
|
end
|
37
|
+
|
31
38
|
response
|
32
39
|
end
|
33
40
|
|
@@ -49,29 +56,50 @@ module OpenapiFirst
|
|
49
56
|
env[ORIGINAL_PATH] = env[Rack::PATH_INFO]
|
50
57
|
env[Rack::PATH_INFO] = Rack::Request.new(env).path
|
51
58
|
@router.call(env)
|
59
|
+
rescue BodyParsingError => e
|
60
|
+
handle_body_parsing_error(e)
|
52
61
|
ensure
|
53
62
|
env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
|
54
63
|
end
|
55
64
|
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
65
|
+
def handle_body_parsing_error(exception)
|
66
|
+
err = { title: 'Failed to parse body as application/json', status: '400' }
|
67
|
+
err[:detail] = exception.cause unless ENV['RACK_ENV'] == 'production'
|
68
|
+
errors = [err]
|
69
|
+
raise RequestInvalidError, errors if @raise
|
70
|
+
|
71
|
+
Rack::Response.new(
|
72
|
+
MultiJson.dump(errors: errors),
|
73
|
+
400,
|
74
|
+
Rack::CONTENT_TYPE => 'application/vnd.api+json'
|
75
|
+
).finish
|
76
|
+
end
|
77
|
+
|
78
|
+
def build_router(operations)
|
79
|
+
router = Hanami::Router.new.tap do |r|
|
80
|
+
operations.each do |operation|
|
81
|
+
normalized_path = operation.path.gsub('{', ':').gsub('}', '')
|
82
|
+
r.public_send(
|
83
|
+
operation.method,
|
84
|
+
normalized_path,
|
85
|
+
to: build_route(operation)
|
86
|
+
)
|
62
87
|
end
|
63
|
-
router.public_send(
|
64
|
-
operation.method,
|
65
|
-
normalized_path,
|
66
|
-
to: lambda do |env|
|
67
|
-
env[OPERATION] = operation
|
68
|
-
env[PARAMETERS] = env['router.params']
|
69
|
-
env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH)
|
70
|
-
@app.call(env)
|
71
|
-
end
|
72
|
-
)
|
73
88
|
end
|
74
|
-
|
89
|
+
raise_error = @raise
|
90
|
+
Rack::Builder.app do
|
91
|
+
use BodyParserMiddleware, raise_error: raise_error
|
92
|
+
run router
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def build_route(operation)
|
97
|
+
lambda do |env|
|
98
|
+
env[OPERATION] = operation
|
99
|
+
env[PARAMETERS] = env['router.params']
|
100
|
+
env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH)
|
101
|
+
@app.call(env)
|
102
|
+
end
|
75
103
|
end
|
76
104
|
end
|
77
105
|
end
|
@@ -17,6 +17,7 @@ module OpenapiFirst
|
|
17
17
|
insert_property_defaults: true,
|
18
18
|
before_property_validation: proc do |data, property, property_schema, parent|
|
19
19
|
convert_nullable(data, property, property_schema, parent)
|
20
|
+
binary_format(data, property, property_schema, parent)
|
20
21
|
end
|
21
22
|
)
|
22
23
|
end
|
@@ -27,6 +28,14 @@ module OpenapiFirst
|
|
27
28
|
|
28
29
|
private
|
29
30
|
|
31
|
+
def binary_format(data, property, property_schema, _parent)
|
32
|
+
return unless property_schema.is_a?(Hash) && property_schema['format'] == 'binary'
|
33
|
+
|
34
|
+
property_schema['type'] = 'object'
|
35
|
+
property_schema.delete('format')
|
36
|
+
data[property].transform_keys!(&:to_s)
|
37
|
+
end
|
38
|
+
|
30
39
|
def convert_nullable(_data, _property, property_schema, _parent)
|
31
40
|
return unless property_schema.is_a?(Hash) && property_schema['nullable'] && property_schema['type']
|
32
41
|
|
@@ -0,0 +1,18 @@
|
|
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
|
data/lib/openapi_first.rb
CHANGED
@@ -4,6 +4,7 @@ require 'yaml'
|
|
4
4
|
require 'json_refs'
|
5
5
|
require_relative 'openapi_first/definition'
|
6
6
|
require_relative 'openapi_first/version'
|
7
|
+
require_relative 'openapi_first/errors'
|
7
8
|
require_relative 'openapi_first/inbox'
|
8
9
|
require_relative 'openapi_first/router'
|
9
10
|
require_relative 'openapi_first/request_validation'
|
@@ -39,7 +40,7 @@ module OpenapiFirst
|
|
39
40
|
request_validation_raise_error: false,
|
40
41
|
response_validation: false
|
41
42
|
)
|
42
|
-
spec = OpenapiFirst.load(spec)
|
43
|
+
spec = OpenapiFirst.load(spec) unless spec.is_a?(Definition)
|
43
44
|
App.new(
|
44
45
|
nil,
|
45
46
|
spec,
|
@@ -57,7 +58,7 @@ module OpenapiFirst
|
|
57
58
|
request_validation_raise_error: false,
|
58
59
|
response_validation: false
|
59
60
|
)
|
60
|
-
spec = OpenapiFirst.load(spec)
|
61
|
+
spec = OpenapiFirst.load(spec) unless spec.is_a?(Definition)
|
61
62
|
AppWithOptions.new(
|
62
63
|
spec,
|
63
64
|
namespace: namespace,
|
@@ -77,50 +78,4 @@ module OpenapiFirst
|
|
77
78
|
App.new(app, @spec, **@options)
|
78
79
|
end
|
79
80
|
end
|
80
|
-
|
81
|
-
class Error < StandardError; end
|
82
|
-
|
83
|
-
class NotFoundError < Error; end
|
84
|
-
|
85
|
-
class NotImplementedError < RuntimeError; end
|
86
|
-
|
87
|
-
class ResponseInvalid < Error; end
|
88
|
-
|
89
|
-
class ResponseCodeNotFoundError < ResponseInvalid; end
|
90
|
-
|
91
|
-
class ResponseContentTypeNotFoundError < ResponseInvalid; end
|
92
|
-
|
93
|
-
class ResponseBodyInvalidError < ResponseInvalid; end
|
94
|
-
|
95
|
-
class RequestInvalidError < Error
|
96
|
-
def initialize(serialized_errors)
|
97
|
-
message = error_message(serialized_errors)
|
98
|
-
super message
|
99
|
-
end
|
100
|
-
|
101
|
-
private
|
102
|
-
|
103
|
-
def error_message(errors)
|
104
|
-
errors.map do |error|
|
105
|
-
[human_source(error), human_error(error)].compact.join(' ')
|
106
|
-
end.join(', ')
|
107
|
-
end
|
108
|
-
|
109
|
-
def human_source(error)
|
110
|
-
return unless error[:source]
|
111
|
-
|
112
|
-
source_key = error[:source].keys.first
|
113
|
-
source = {
|
114
|
-
pointer: 'Request body invalid:',
|
115
|
-
parameter: 'Query parameter invalid:'
|
116
|
-
}.fetch(source_key, source_key)
|
117
|
-
name = error[:source].values.first
|
118
|
-
source += " #{name}" unless name.nil? || name.empty?
|
119
|
-
source
|
120
|
-
end
|
121
|
-
|
122
|
-
def human_error(error)
|
123
|
-
error[:title]
|
124
|
-
end
|
125
|
-
end
|
126
81
|
end
|
data/openapi_first.gemspec
CHANGED
@@ -32,11 +32,11 @@ Gem::Specification.new do |spec|
|
|
32
32
|
spec.bindir = 'exe'
|
33
33
|
spec.require_paths = ['lib']
|
34
34
|
|
35
|
-
spec.required_ruby_version = '>=
|
35
|
+
spec.required_ruby_version = '>= 3.0.5'
|
36
36
|
|
37
37
|
spec.add_runtime_dependency 'deep_merge', '>= 1.2.1'
|
38
|
-
spec.add_runtime_dependency 'hanami-router', '2.0.
|
39
|
-
spec.add_runtime_dependency 'hanami-utils', '2.0.
|
38
|
+
spec.add_runtime_dependency 'hanami-router', '~> 2.0.0'
|
39
|
+
spec.add_runtime_dependency 'hanami-utils', '~> 2.0.0'
|
40
40
|
spec.add_runtime_dependency 'json_refs', '~> 0.1', '>= 0.1.7'
|
41
41
|
spec.add_runtime_dependency 'json_schemer', '~> 0.2.16'
|
42
42
|
spec.add_runtime_dependency 'multi_json', '~> 1.14'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: openapi_first
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.21.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andreas Haller
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-03-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deep_merge
|
@@ -28,30 +28,30 @@ dependencies:
|
|
28
28
|
name: hanami-router
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 2.0.
|
33
|
+
version: 2.0.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 2.0.
|
40
|
+
version: 2.0.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: hanami-utils
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 2.0.
|
47
|
+
version: 2.0.0
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 2.0.
|
54
|
+
version: 2.0.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: json_refs
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -190,6 +190,7 @@ files:
|
|
190
190
|
- Rakefile
|
191
191
|
- benchmarks/Gemfile
|
192
192
|
- benchmarks/Gemfile.lock
|
193
|
+
- benchmarks/README.md
|
193
194
|
- benchmarks/apps/committee.ru
|
194
195
|
- benchmarks/apps/committee_with_response_validation.ru
|
195
196
|
- benchmarks/apps/committee_with_sinatra.ru
|
@@ -203,7 +204,9 @@ files:
|
|
203
204
|
- benchmarks/apps/roda.ru
|
204
205
|
- benchmarks/apps/sinatra.ru
|
205
206
|
- benchmarks/apps/syro.ru
|
207
|
+
- benchmarks/benchmark-wrk.sh
|
206
208
|
- benchmarks/benchmarks.rb
|
209
|
+
- benchmarks/post.lua
|
207
210
|
- bin/console
|
208
211
|
- bin/setup
|
209
212
|
- examples/README.md
|
@@ -212,9 +215,11 @@ files:
|
|
212
215
|
- examples/openapi.yaml
|
213
216
|
- lib/openapi_first.rb
|
214
217
|
- lib/openapi_first/app.rb
|
218
|
+
- lib/openapi_first/body_parser_middleware.rb
|
215
219
|
- lib/openapi_first/coverage.rb
|
216
220
|
- lib/openapi_first/default_operation_resolver.rb
|
217
221
|
- lib/openapi_first/definition.rb
|
222
|
+
- lib/openapi_first/errors.rb
|
218
223
|
- lib/openapi_first/inbox.rb
|
219
224
|
- lib/openapi_first/operation.rb
|
220
225
|
- lib/openapi_first/rack_responder.rb
|
@@ -224,8 +229,8 @@ files:
|
|
224
229
|
- lib/openapi_first/response_validation.rb
|
225
230
|
- lib/openapi_first/response_validator.rb
|
226
231
|
- lib/openapi_first/router.rb
|
227
|
-
- lib/openapi_first/router_required.rb
|
228
232
|
- lib/openapi_first/schema_validation.rb
|
233
|
+
- lib/openapi_first/use_router.rb
|
229
234
|
- lib/openapi_first/utils.rb
|
230
235
|
- lib/openapi_first/validation.rb
|
231
236
|
- lib/openapi_first/validation_format.rb
|
@@ -244,7 +249,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
244
249
|
requirements:
|
245
250
|
- - ">="
|
246
251
|
- !ruby/object:Gem::Version
|
247
|
-
version:
|
252
|
+
version: 3.0.5
|
248
253
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
249
254
|
requirements:
|
250
255
|
- - ">="
|
@@ -1,13 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module OpenapiFirst
|
4
|
-
module RouterRequired
|
5
|
-
def call(env)
|
6
|
-
unless env.key?(OPERATION)
|
7
|
-
raise 'OpenapiFirst::Router missing in middleware stack. Did you forget adding OpenapiFirst::Router?'
|
8
|
-
end
|
9
|
-
|
10
|
-
super
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|