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.
@@ -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