openapi_first 0.19.0 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'multi_json'
4
- require_relative 'router_required'
4
+ require_relative 'use_router'
5
5
  require_relative 'validation_format'
6
6
 
7
7
  module OpenapiFirst
8
8
  class ResponseValidation
9
- prepend RouterRequired
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
- error_message_for(error)
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
@@ -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 build_router(operations) # rubocop:disable Metrics/AbcSize
57
- router = Hanami::Router.new
58
- operations.each do |operation|
59
- normalized_path = operation.path.gsub('{', ':').gsub('}', '')
60
- if operation.operation_id.nil?
61
- warn "operationId is missing in '#{operation.method} #{operation.path}'. I am ignoring this operation."
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
- router
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.19.0'
4
+ VERSION = '0.21.0'
5
5
  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) if spec.is_a?(String)
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) if spec.is_a?(String)
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
@@ -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 = '>= 2.6.0'
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.alpha5'
39
- spec.add_runtime_dependency 'hanami-utils', '2.0.alpha3'
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.19.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: 2022-08-02 00:00:00.000000000 Z
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.alpha5
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.alpha5
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.alpha3
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.alpha3
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: 2.6.0
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