openapi_first 0.13.2 → 0.13.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 579d36678fe6b41e050a370017165899c82be5e64b352e45410107e7302d26af
4
- data.tar.gz: 0ddd3ab2f5ed7bb9aadc41cba158ea3fc9f2e59a93054c4f76f3766109e03192
3
+ metadata.gz: 94cbe97158c8482c40dd1521e1f83cef287ad2e7e32f82746f24adcf45f60c4e
4
+ data.tar.gz: 1aff02e97e51b67334c34bc6ee967559def9218c4e81dc6295204bb5447f6715
5
5
  SHA512:
6
- metadata.gz: fb1ca083cce9a636bf42f33c308dcd5c1e19b14b31bebe0534646baf0af8c43e6ce1d5b1e20b0657872a12c90e2446122a33c015ce42ee96ee16c8b1f4cc8b79
7
- data.tar.gz: 8a2f1af143f23f21381691d158d0991b1e055d726c8e0c46579bd1d88383f3c4f66ca3a8b1554a47bdab5e6278340b6e6e8ace1d983e95b01b84c1f822a5249b
6
+ metadata.gz: 4ca9c77f8b70024083b831a02d824c2fd0321ff3598234f085133b161ba46a2177e9279e80e22f2ca56da466b087c0c7ac3a62c6dec9a7f906bf03fd44f2eb47
7
+ data.tar.gz: bcffa9f70194081780a0348f17e695b05f879eb029e4932baa224c3ca1d03e7fb531931f111bfdbcf452f772c0729e725cd3101779aa363408f859e63fd989f8
@@ -1,5 +1,6 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.6
3
+ NewCops: enable
3
4
  Style/Documentation:
4
5
  Enabled: false
5
6
  Style/ExponentialNotation:
@@ -8,47 +9,5 @@ Metrics/BlockLength:
8
9
  Exclude:
9
10
  - 'spec/**/*.rb'
10
11
  - '*.gemspec'
11
- Layout/EmptyLinesAroundAttributeAccessor:
12
- Enabled: true
13
- Layout/SpaceAroundMethodCallOperator:
14
- Enabled: true
15
- Lint/DeprecatedOpenSSLConstant:
16
- Enabled: true
17
- Lint/DuplicateElsifCondition:
18
- Enabled: true
19
- Lint/RaiseException:
20
- Enabled: true
21
- Lint/MixedRegexpCaptureTypes:
22
- Enabled: true
23
- Style/RedundantRegexpCharacterClass:
24
- Enabled: true
25
- Style/RedundantRegexpEscape:
26
- Enabled: true
27
- Style/SlicingWithRange:
28
- Enabled: true
29
- Lint/StructNewOverride:
30
- Enabled: true
31
- Style/HashEachMethods:
32
- Enabled: false
33
- Style/AccessorGrouping:
34
- Enabled: true
35
- Style/ArrayCoercion:
36
- Enabled: true
37
- Style/BisectedAttrAccessor:
38
- Enabled: true
39
- Style/CaseLikeIf:
40
- Enabled: true
41
- Style/HashAsLastArrayItem:
42
- Enabled: true
43
- Style/HashLikeCase:
44
- Enabled: true
45
- Style/HashTransformKeys:
46
- Enabled: true
47
- Style/HashTransformValues:
48
- Enabled: true
49
- Style/RedundantAssignment:
50
- Enabled: true
51
- Style/RedundantFetchBlock:
52
- Enabled: true
53
- Style/RedundantFileExtensionInRequire:
54
- Enabled: true
12
+ Metrics/MethodLength:
13
+ Max: 20
@@ -1,5 +1,8 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.3
4
+ - Better error message if string does not match format
5
+
3
6
  ## 0.13.2
4
7
  - Return indicator (`source: { parameter: 'list/1' }`) in error response body when array item in query parameter is invalid
5
8
 
@@ -22,7 +25,7 @@
22
25
 
23
26
  ## 0.12.1
24
27
  - Fix response when handler returns 404 or 405
25
- - Don't validate the response content if status is 205 (no content)
28
+ - Don't validate the response content if status is 204 (no content)
26
29
 
27
30
  ## 0.12.0
28
31
  - Change `ResponseValidator` to raise an exception if it found a problem
@@ -1,11 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (0.13.2)
4
+ openapi_first (0.13.3)
5
5
  deep_merge (>= 1.2.1)
6
6
  hanami-router (~> 2.0.alpha3)
7
7
  hanami-utils (~> 2.0.alpha1)
8
- json_schemer (~> 0.2)
8
+ json_schemer (~> 0.2.16)
9
9
  multi_json (~> 1.14)
10
10
  oas_parser (~> 0.25.1)
11
11
  rack (~> 2.2)
@@ -13,7 +13,7 @@ PATH
13
13
  GEM
14
14
  remote: https://rubygems.org/
15
15
  specs:
16
- activesupport (6.0.3.2)
16
+ activesupport (6.0.3.4)
17
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
18
  i18n (>= 0.7, < 2)
19
19
  minitest (~> 5.1)
@@ -41,14 +41,14 @@ GEM
41
41
  hash-deep-merge (0.1.1)
42
42
  i18n (1.8.5)
43
43
  concurrent-ruby (~> 1.0)
44
- json_schemer (0.2.13)
44
+ json_schemer (0.2.16)
45
45
  ecma-re-validator (~> 0.2)
46
46
  hana (~> 1.3)
47
47
  regexp_parser (~> 1.5)
48
48
  uri_template (~> 0.7)
49
49
  method_source (1.0.0)
50
50
  mini_portile2 (2.4.0)
51
- minitest (5.14.1)
51
+ minitest (5.14.2)
52
52
  multi_json (1.15.0)
53
53
  mustermann (1.1.1)
54
54
  ruby2_keywords (~> 0.0.1)
@@ -66,24 +66,24 @@ GEM
66
66
  mustermann-contrib (~> 1.1.1)
67
67
  nokogiri
68
68
  parallel (1.19.2)
69
- parser (2.7.1.4)
69
+ parser (2.7.2.0)
70
70
  ast (~> 2.4.1)
71
71
  pry (0.13.1)
72
72
  coderay (~> 1.1)
73
73
  method_source (~> 1.0)
74
- public_suffix (4.0.5)
74
+ public_suffix (4.0.6)
75
75
  rack (2.2.3)
76
76
  rack-test (1.1.0)
77
77
  rack (>= 1.0, < 3)
78
78
  rainbow (3.0.0)
79
79
  rake (13.0.1)
80
- regexp_parser (1.7.1)
80
+ regexp_parser (1.8.2)
81
81
  rexml (3.2.4)
82
82
  rspec (3.9.0)
83
83
  rspec-core (~> 3.9.0)
84
84
  rspec-expectations (~> 3.9.0)
85
85
  rspec-mocks (~> 3.9.0)
86
- rspec-core (3.9.2)
86
+ rspec-core (3.9.3)
87
87
  rspec-support (~> 3.9.3)
88
88
  rspec-expectations (3.9.2)
89
89
  diff-lcs (>= 1.2.0, < 2.0)
@@ -92,26 +92,26 @@ GEM
92
92
  diff-lcs (>= 1.2.0, < 2.0)
93
93
  rspec-support (~> 3.9.0)
94
94
  rspec-support (3.9.3)
95
- rubocop (0.88.0)
95
+ rubocop (0.93.1)
96
96
  parallel (~> 1.10)
97
- parser (>= 2.7.1.1)
97
+ parser (>= 2.7.1.5)
98
98
  rainbow (>= 2.2.2, < 4.0)
99
- regexp_parser (>= 1.7)
99
+ regexp_parser (>= 1.8)
100
100
  rexml
101
- rubocop-ast (>= 0.1.0, < 1.0)
101
+ rubocop-ast (>= 0.6.0)
102
102
  ruby-progressbar (~> 1.7)
103
103
  unicode-display_width (>= 1.4.0, < 2.0)
104
- rubocop-ast (0.2.0)
105
- parser (>= 2.7.0.1)
104
+ rubocop-ast (0.8.0)
105
+ parser (>= 2.7.1.5)
106
106
  ruby-progressbar (1.10.1)
107
107
  ruby2_keywords (0.0.2)
108
108
  thread_safe (0.3.6)
109
109
  transproc (1.1.1)
110
- tzinfo (1.2.7)
110
+ tzinfo (1.2.8)
111
111
  thread_safe (~> 0.1)
112
112
  unicode-display_width (1.7.0)
113
113
  uri_template (0.7.0)
114
- zeitwerk (2.4.0)
114
+ zeitwerk (2.4.1)
115
115
 
116
116
  PLATFORMS
117
117
  ruby
data/README.md CHANGED
@@ -19,7 +19,7 @@ OpenapiFirst consists of these Rack middlewares:
19
19
 
20
20
  - [`OpenapiFirst::Router`](#OpenapiFirst::Router) – Finds the OpenAPI operation for the current request or returns 404 if no operation was found. This can be customized.
21
21
  - [`OpenapiFirst::RequestValidation`](#OpenapiFirst::RequestValidation) – Validates the request against the API description and returns 400 if the request is invalid.
22
- - [`OpenapiFirst::Responder`](#OpenapiFirst::Responder) calls the [handler](#handlers) found for the operation.
22
+ - [`OpenapiFirst::Responder`](#OpenapiFirst::Responder) calls the [handler](#handlers) found for the operation, sets the correct content-type and serialized the response body to json if needed.
23
23
  - [`OpenapiFirst::ResponseValidation`](#OpenapiFirst::ResponseValidation) Validates the response and raises an exception if the response body is invalid.
24
24
 
25
25
  ## OpenapiFirst::Router
@@ -99,6 +99,12 @@ This will also add the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
99
99
 
100
100
  tbd.
101
101
 
102
+ ### readOnly / writeOnly properties
103
+
104
+ Request validation fails if request includes a property with `readOnly: true`.
105
+
106
+ Response validation fails if response body includes a property with `writeOnly: true`.
107
+
102
108
  ## OpenapiFirst::Responder
103
109
 
104
110
  This Rack endpoint maps the HTTP request to a method call based on the [operationId](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operation-object) in your API description and calls it. Responder also adds a content-type to the response.
@@ -6,7 +6,8 @@ gem 'benchmark-ips'
6
6
  gem 'benchmark-memory'
7
7
  gem 'committee'
8
8
  gem 'grape'
9
- gem 'hanami-router', '~> 2.0.0.alpha2'
9
+ gem 'hanami-api'
10
+ gem 'hanami-router', '~> 2.0.0.alpha3'
10
11
  gem 'multi_json'
11
12
  gem 'openapi_first', path: '../'
12
13
  gem 'sinatra'
@@ -1,11 +1,11 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- openapi_first (0.13.2)
4
+ openapi_first (0.13.3)
5
5
  deep_merge (>= 1.2.1)
6
6
  hanami-router (~> 2.0.alpha3)
7
7
  hanami-utils (~> 2.0.alpha1)
8
- json_schemer (~> 0.2)
8
+ json_schemer (~> 0.2.16)
9
9
  multi_json (~> 1.14)
10
10
  oas_parser (~> 0.25.1)
11
11
  rack (~> 2.2)
@@ -13,7 +13,7 @@ PATH
13
13
  GEM
14
14
  remote: https://rubygems.org/
15
15
  specs:
16
- activesupport (6.0.3.2)
16
+ activesupport (6.0.3.4)
17
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
18
  i18n (>= 0.7, < 2)
19
19
  minitest (~> 5.1)
@@ -21,7 +21,7 @@ GEM
21
21
  zeitwerk (~> 2.2, >= 2.2.2)
22
22
  addressable (2.7.0)
23
23
  public_suffix (>= 2.0.2, < 5.0)
24
- benchmark-ips (2.8.2)
24
+ benchmark-ips (2.8.3)
25
25
  benchmark-memory (0.1.2)
26
26
  memory_profiler (~> 0.9)
27
27
  builder (3.2.4)
@@ -42,7 +42,7 @@ GEM
42
42
  concurrent-ruby (~> 1.0)
43
43
  dry-equalizer (0.3.0)
44
44
  dry-inflector (0.2.0)
45
- dry-logic (1.0.7)
45
+ dry-logic (1.0.8)
46
46
  concurrent-ruby (~> 1.0)
47
47
  dry-core (~> 0.2)
48
48
  dry-equalizer (~> 0.2)
@@ -55,7 +55,7 @@ GEM
55
55
  dry-logic (~> 1.0, >= 1.0.2)
56
56
  ecma-re-validator (0.2.1)
57
57
  regexp_parser (~> 1.2)
58
- grape (1.4.0)
58
+ grape (1.5.0)
59
59
  activesupport
60
60
  builder
61
61
  dry-types (>= 1.1)
@@ -63,6 +63,8 @@ GEM
63
63
  rack (>= 1.3.0)
64
64
  rack-accept
65
65
  hana (1.3.6)
66
+ hanami-api (0.1.1)
67
+ hanami-router (~> 2.0.alpha)
66
68
  hanami-router (2.0.0.alpha3)
67
69
  mustermann (~> 1.0)
68
70
  mustermann-contrib (~> 1.0)
@@ -75,14 +77,14 @@ GEM
75
77
  i18n (1.8.5)
76
78
  concurrent-ruby (~> 1.0)
77
79
  json_schema (0.20.9)
78
- json_schemer (0.2.13)
80
+ json_schemer (0.2.16)
79
81
  ecma-re-validator (~> 0.2)
80
82
  hana (~> 1.3)
81
83
  regexp_parser (~> 1.5)
82
84
  uri_template (~> 0.7)
83
85
  memory_profiler (0.9.14)
84
86
  mini_portile2 (2.4.0)
85
- minitest (5.14.1)
87
+ minitest (5.14.2)
86
88
  multi_json (1.15.0)
87
89
  mustermann (1.1.1)
88
90
  ruby2_keywords (~> 0.0.1)
@@ -93,7 +95,7 @@ GEM
93
95
  mustermann (>= 1.0.0)
94
96
  nokogiri (1.10.10)
95
97
  mini_portile2 (~> 2.4.0)
96
- oas_parser (0.25.1)
98
+ oas_parser (0.25.2)
97
99
  activesupport (>= 4.0.0)
98
100
  addressable (~> 2.3)
99
101
  builder (~> 3.2.3)
@@ -102,19 +104,19 @@ GEM
102
104
  mustermann-contrib (~> 1.1.1)
103
105
  nokogiri
104
106
  openapi_parser (0.12.1)
105
- public_suffix (4.0.5)
107
+ public_suffix (4.0.6)
106
108
  rack (2.2.3)
107
109
  rack-accept (0.4.5)
108
110
  rack (>= 0.4)
109
- rack-protection (2.0.8.1)
111
+ rack-protection (2.1.0)
110
112
  rack
111
- regexp_parser (1.7.1)
113
+ regexp_parser (1.8.2)
112
114
  ruby2_keywords (0.0.2)
113
115
  seg (1.2.0)
114
- sinatra (2.0.8.1)
116
+ sinatra (2.1.0)
115
117
  mustermann (~> 1.0)
116
- rack (~> 2.0)
117
- rack-protection (= 2.0.8.1)
118
+ rack (~> 2.2)
119
+ rack-protection (= 2.1.0)
118
120
  tilt (~> 2.0)
119
121
  syro (3.2.0)
120
122
  rack (>= 1.6.0)
@@ -135,7 +137,8 @@ DEPENDENCIES
135
137
  benchmark-memory
136
138
  committee
137
139
  grape
138
- hanami-router (~> 2.0.0.alpha2)
140
+ hanami-api
141
+ hanami-router (~> 2.0.0.alpha3)
139
142
  multi_json
140
143
  openapi_first!
141
144
  sinatra
@@ -1,30 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'committee'
4
- require 'syro'
5
3
  require 'multi_json'
4
+ require 'committee'
5
+ require 'hanami/api'
6
6
 
7
- app = Syro.new do
8
- on 'hello' do
9
- on :id do
10
- get do
11
- res.json MultiJson.dump(hello: 'world', id: inbox[:id])
12
- end
13
- end
7
+ app = Class.new(Hanami::API) do
8
+ get '/hello/:id' do
9
+ json(hello: 'world', id: params.fetch(:id))
10
+ end
14
11
 
15
- get do
16
- res.json [MultiJson.dump(hello: 'world')]
17
- end
12
+ get '/hello' do
13
+ json([{ hello: 'world' }])
14
+ end
18
15
 
19
- post do
20
- res.status = 201
21
- res.json MultiJson.dump(hello: 'world')
22
- end
16
+ post '/hello' do
17
+ status 201
18
+ json(hello: 'world')
23
19
  end
24
- end
20
+ end.new
25
21
 
26
- use Committee::Middleware::RequestValidation,
27
- schema_path: './apps/openapi.yaml',
28
- coerce_date_times: false
22
+ use Committee::Middleware::RequestValidation, schema_path: './apps/openapi.yaml'
29
23
 
30
24
  run app
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'multi_json'
4
+ require 'hanami/api'
5
+
6
+ app = Class.new(Hanami::API) do
7
+ get '/hello/:id' do
8
+ json(hello: 'world', id: params.fetch(:id))
9
+ end
10
+
11
+ get '/hello' do
12
+ json([{ hello: 'world' }])
13
+ end
14
+
15
+ post '/hello' do
16
+ status 201
17
+ json(hello: 'world')
18
+ end
19
+ end.new
20
+
21
+ run app
@@ -4,7 +4,7 @@ require 'hanami/router'
4
4
  require 'multi_json'
5
5
 
6
6
  app = Hanami::Router.new do
7
- get '/hello', to: ->(_env) { [200, {}, [MultiJson.dump(hello: 'world')]] }
7
+ get '/hello', to: ->(_env) { [200, {}, [MultiJson.dump([{ hello: 'world' }])]] }
8
8
  get '/hello/:id', to: lambda { |env|
9
9
  [200, {}, [MultiJson.dump(hello: 'world', id: env['router.params'][:id])]]
10
10
  }
@@ -13,7 +13,7 @@ class SinatraExample < Sinatra::Base
13
13
 
14
14
  get '/hello' do
15
15
  content_type :json
16
- [MultiJson.dump(hello: 'world')]
16
+ MultiJson.dump([{ hello: 'world' }])
17
17
  end
18
18
 
19
19
  post '/hello' do
@@ -7,17 +7,17 @@ app = Syro.new do
7
7
  on 'hello' do
8
8
  on :id do
9
9
  get do
10
- res.json MultiJson.dump(hello: 'world', id: inbox[:id])
10
+ res.json({ hello: 'world', id: inbox[:id] })
11
11
  end
12
12
  end
13
13
 
14
14
  get do
15
- res.json [MultiJson.dump(hello: 'world')]
15
+ res.json([{ hello: 'world' }])
16
16
  end
17
17
 
18
18
  post do
19
19
  res.status = 201
20
- res.json MultiJson.dump(hello: 'world')
20
+ res.json({ hello: 'world' })
21
21
  end
22
22
  end
23
23
  end
@@ -4,15 +4,11 @@ require_relative 'operation'
4
4
 
5
5
  module OpenapiFirst
6
6
  class Definition
7
- attr_reader :filepath
7
+ attr_reader :filepath, :operations
8
8
 
9
9
  def initialize(parsed)
10
10
  @filepath = parsed.path
11
- @spec = parsed
12
- end
13
-
14
- def operations
15
- @spec.endpoints.map { |e| Operation.new(e) }
11
+ @operations = parsed.endpoints.map { |e| Operation.new(e) }
16
12
  end
17
13
  end
18
14
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'forwardable'
4
4
  require 'json_schemer'
5
+ require_relative 'schema_validation'
5
6
  require_relative 'utils'
6
7
  require_relative 'response_object'
7
8
 
@@ -14,6 +15,9 @@ module OpenapiFirst
14
15
  :request_body,
15
16
  :operation_id
16
17
 
18
+ WRITE_METHODS = Set.new(%w[post put patch delete]).freeze
19
+ private_constant :WRITE_METHODS
20
+
17
21
  def initialize(parsed)
18
22
  @operation = parsed
19
23
  end
@@ -22,12 +26,19 @@ module OpenapiFirst
22
26
  @operation.path.path
23
27
  end
24
28
 
25
- def parameters_json_schema
26
- @parameters_json_schema ||= build_parameters_json_schema
29
+ def read?
30
+ !write?
31
+ end
32
+
33
+ def write?
34
+ WRITE_METHODS.include?(method)
27
35
  end
28
36
 
29
37
  def parameters_schema
30
- @parameters_schema ||= parameters_json_schema && JSONSchemer.schema(parameters_json_schema)
38
+ @parameters_schema ||= begin
39
+ parameters_json_schema = build_parameters_json_schema
40
+ parameters_json_schema && SchemaValidation.new(parameters_json_schema)
41
+ end
31
42
  end
32
43
 
33
44
  def content_type_for(status)
@@ -46,13 +57,17 @@ module OpenapiFirst
46
57
  message = "Response content type not found '#{content_type}' for '#{name}'"
47
58
  raise ResponseContentTypeNotFoundError, message
48
59
  end
49
- media_type['schema']
60
+ schema = media_type['schema']
61
+ SchemaValidation.new(schema, write: false) if schema
50
62
  end
51
63
 
52
- def request_body_schema_for(request_content_type)
64
+ def request_body_schema(request_content_type)
53
65
  content = @operation.request_body.content
54
66
  media_type = find_content_for_content_type(content, request_content_type)
55
- media_type&.fetch('schema', nil)
67
+ schema = media_type&.fetch('schema', nil)
68
+ return unless schema
69
+
70
+ SchemaValidation.new(schema, write: write?)
56
71
  end
57
72
 
58
73
  def response_for(status)
@@ -84,7 +99,7 @@ module OpenapiFirst
84
99
  end
85
100
  end
86
101
 
87
- def generate_schema(schema, params, parameter) # rubocop:disable Metrics/MethodLength
102
+ def generate_schema(schema, params, parameter)
88
103
  required = Set.new(schema['required'])
89
104
  params.each do |key, value|
90
105
  required << key if parameter.required
@@ -16,7 +16,7 @@ module OpenapiFirst
16
16
  @raise = raise_error
17
17
  end
18
18
 
19
- def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
19
+ def call(env) # rubocop:disable Metrics/AbcSize
20
20
  operation = env[OpenapiFirst::OPERATION]
21
21
  return @app.call(env) unless operation
22
22
 
@@ -45,17 +45,17 @@ module OpenapiFirst
45
45
  validate_request_body_presence!(body, operation)
46
46
  return if body.empty?
47
47
 
48
- schema = request_body_schema(content_type, operation)
48
+ schema = operation&.request_body_schema(content_type)
49
49
  return unless schema
50
50
 
51
51
  parsed_request_body = parse_request_body!(body)
52
- errors = validate_json_schema(schema, parsed_request_body)
52
+ errors = schema.validate(parsed_request_body)
53
53
  halt_with_error(400, serialize_request_body_errors(errors)) if errors.any?
54
- env[INBOX].merge! env[REQUEST_BODY] = parsed_request_body
54
+ env[INBOX].merge! env[REQUEST_BODY] = Utils.deep_symbolize(parsed_request_body)
55
55
  end
56
56
 
57
57
  def parse_request_body!(body)
58
- MultiJson.load(body, symbolize_keys: true)
58
+ MultiJson.load(body)
59
59
  rescue MultiJson::ParseError => e
60
60
  err = { title: 'Failed to parse body as JSON' }
61
61
  err[:detail] = e.cause unless ENV['RACK_ENV'] == 'production'
@@ -74,10 +74,6 @@ module OpenapiFirst
74
74
  halt_with_error(415, 'Request body is required')
75
75
  end
76
76
 
77
- def validate_json_schema(schema, object)
78
- schema.validate(Utils.deep_stringify(object))
79
- end
80
-
81
77
  def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
82
78
  {
83
79
  status: status.to_s,
@@ -95,14 +91,6 @@ module OpenapiFirst
95
91
  ).finish
96
92
  end
97
93
 
98
- def request_body_schema(content_type, operation)
99
- return unless operation
100
-
101
- schema = operation.request_body_schema_for(content_type)
102
-
103
- JSONSchemer.schema(schema) if schema
104
- end
105
-
106
94
  def serialize_request_body_errors(validation_errors)
107
95
  validation_errors.map do |error|
108
96
  {
@@ -114,14 +102,11 @@ module OpenapiFirst
114
102
  end
115
103
 
116
104
  def validate_query_parameters!(env, operation, params)
117
- json_schema = operation.parameters_json_schema
118
- return unless json_schema
119
-
120
- params = filtered_params(json_schema, params)
121
- errors = validate_json_schema(
122
- operation.parameters_schema,
123
- params
124
- )
105
+ schema = operation.parameters_schema
106
+ return unless schema
107
+
108
+ params = filtered_params(schema.raw_schema, params)
109
+ errors = schema.validate(Utils.deep_stringify(params))
125
110
  halt_with_error(400, serialize_query_parameter_errors(errors)) if errors.any?
126
111
  env[PARAMETERS] = params
127
112
  env[INBOX].merge! params
@@ -41,7 +41,8 @@ module OpenapiFirst
41
41
  full_body = +''
42
42
  response.each { |chunk| full_body << chunk }
43
43
  data = full_body.empty? ? {} : load_json(full_body)
44
- errors = JSONSchemer.schema(schema).validate(data).to_a.map do |error|
44
+ errors = schema.validate(data)
45
+ errors = errors.to_a.map! do |error|
45
46
  error_message_for(error)
46
47
  end
47
48
  raise ResponseBodyInvalidError, errors.join(', ') if errors.any?
@@ -53,13 +53,12 @@ module OpenapiFirst
53
53
  env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
54
54
  end
55
55
 
56
- def build_router(operations) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
56
+ def build_router(operations) # rubocop:disable Metrics/AbcSize
57
57
  router = Hanami::Router.new {}
58
58
  operations.each do |operation|
59
59
  normalized_path = operation.path.gsub('{', ':').gsub('}', '')
60
60
  if operation.operation_id.nil?
61
61
  warn "operationId is missing in '#{operation.method} #{operation.path}'. I am ignoring this operation."
62
- next
63
62
  end
64
63
  router.public_send(
65
64
  operation.method,
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json_schemer'
4
+
5
+ module OpenapiFirst
6
+ class SchemaValidation
7
+ attr_reader :raw_schema
8
+
9
+ def initialize(schema, write: true)
10
+ @raw_schema = schema
11
+ custom_keywords = {}
12
+ custom_keywords['writeOnly'] = proc { |data| !data } unless write
13
+ custom_keywords['readOnly'] = proc { |data| !data } if write
14
+ @schemer = JSONSchemer.schema(
15
+ schema,
16
+ keywords: custom_keywords,
17
+ before_property_validation: proc do |data, property, property_schema, parent|
18
+ convert_nullable(data, property, property_schema, parent)
19
+ end
20
+ )
21
+ end
22
+
23
+ def validate(input)
24
+ @schemer.validate(input)
25
+ end
26
+
27
+ private
28
+
29
+ def convert_nullable(_data, _property, property_schema, _parent)
30
+ return unless property_schema.is_a?(Hash) && property_schema['nullable'] && property_schema['type']
31
+
32
+ property_schema['type'] = [*property_schema['type'], 'null']
33
+ property_schema.delete('nullable')
34
+ end
35
+ end
36
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'hanami/utils/string'
4
+ require 'hanami/utils/hash'
4
5
  require 'deep_merge/core'
5
6
 
6
7
  module OpenapiFirst
@@ -17,20 +18,12 @@ module OpenapiFirst
17
18
  Hanami::Utils::String.classify(string)
18
19
  end
19
20
 
20
- def self.deep_stringify(params) # rubocop:disable Metrics/MethodLength
21
- params.each_with_object({}) do |(key, value), output|
22
- output[key.to_s] =
23
- case value
24
- when ::Hash
25
- deep_stringify(value)
26
- when Array
27
- value.map do |item|
28
- item.is_a?(::Hash) ? deep_stringify(item) : item
29
- end
30
- else
31
- value
32
- end
33
- end
21
+ def self.deep_symbolize(hash)
22
+ Hanami::Utils::Hash.deep_symbolize(hash)
23
+ end
24
+
25
+ def self.deep_stringify(hash)
26
+ Hanami::Utils::Hash.deep_stringify(hash)
34
27
  end
35
28
  end
36
29
  end
@@ -6,17 +6,32 @@ module OpenapiFirst
6
6
 
7
7
  # rubocop:disable Metrics/MethodLength
8
8
  # rubocop:disable Metrics/AbcSize
9
+ # rubocop:disable Metrics/CyclomaticComplexity
10
+ # rubocop:disable Metrics/PerceivedComplexity
9
11
  def self.error_details(error)
10
12
  if error['type'] == 'pattern'
11
13
  {
12
14
  title: 'is not valid',
13
15
  detail: "does not match pattern '#{error['schema']['pattern']}'"
14
16
  }
17
+ elsif error['type'] == 'format'
18
+ {
19
+ title: "has not a valid #{error.dig('schema', 'format')} format",
20
+ detail: "#{error['data'].inspect} is not a valid #{error.dig('schema', 'format')} format"
21
+ }
15
22
  elsif error['type'] == 'required'
16
23
  missing_keys = error['details']['missing_keys']
17
24
  {
18
25
  title: "is missing required properties: #{missing_keys.join(', ')}"
19
26
  }
27
+ elsif error['type'] == 'readOnly'
28
+ {
29
+ title: 'appears in request, but is read-only'
30
+ }
31
+ elsif error['type'] == 'writeOnly'
32
+ {
33
+ title: 'write-only field appears in response:'
34
+ }
20
35
  elsif SIMPLE_TYPES.include?(error['type'])
21
36
  {
22
37
  title: "should be a #{error['type']}"
@@ -29,5 +44,7 @@ module OpenapiFirst
29
44
  end
30
45
  # rubocop:enable Metrics/MethodLength
31
46
  # rubocop:enable Metrics/AbcSize
47
+ # rubocop:enable Metrics/CyclomaticComplexity
48
+ # rubocop:enable Metrics/PerceivedComplexity
32
49
  end
33
50
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '0.13.2'
4
+ VERSION = '0.13.3'
5
5
  end
@@ -32,10 +32,12 @@ 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'
36
+
35
37
  spec.add_runtime_dependency 'deep_merge', '>= 1.2.1'
36
38
  spec.add_runtime_dependency 'hanami-router', '~> 2.0.alpha3'
37
39
  spec.add_runtime_dependency 'hanami-utils', '~> 2.0.alpha1'
38
- spec.add_runtime_dependency 'json_schemer', '~> 0.2'
40
+ spec.add_runtime_dependency 'json_schemer', '~> 0.2.16'
39
41
  spec.add_runtime_dependency 'multi_json', '~> 1.14'
40
42
  spec.add_runtime_dependency 'oas_parser', '~> 0.25.1'
41
43
  spec.add_runtime_dependency 'rack', '~> 2.2'
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.13.2
4
+ version: 0.13.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-08-27 00:00:00.000000000 Z
11
+ date: 2020-11-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deep_merge
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.2'
61
+ version: 0.2.16
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0.2'
68
+ version: 0.2.16
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: multi_json
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -164,7 +164,7 @@ dependencies:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
166
  version: '3'
167
- description:
167
+ description:
168
168
  email:
169
169
  - andreas.haller@posteo.de
170
170
  executables: []
@@ -187,6 +187,7 @@ files:
187
187
  - benchmarks/Gemfile.lock
188
188
  - benchmarks/apps/committee.ru
189
189
  - benchmarks/apps/grape.ru
190
+ - benchmarks/apps/hanami_api.ru
190
191
  - benchmarks/apps/hanami_router.ru
191
192
  - benchmarks/apps/openapi.yaml
192
193
  - benchmarks/apps/openapi_first.ru
@@ -213,6 +214,7 @@ files:
213
214
  - lib/openapi_first/response_validator.rb
214
215
  - lib/openapi_first/router.rb
215
216
  - lib/openapi_first/router_required.rb
217
+ - lib/openapi_first/schema_validation.rb
216
218
  - lib/openapi_first/utils.rb
217
219
  - lib/openapi_first/validation.rb
218
220
  - lib/openapi_first/validation_format.rb
@@ -225,7 +227,7 @@ metadata:
225
227
  https://github.com/ahx/openapi_first: https://github.com/ahx/openapi_first
226
228
  source_code_uri: https://github.com/ahx/openapi_first
227
229
  changelog_uri: https://github.com/ahx/openapi_first/blob/master/CHANGELOG.md
228
- post_install_message:
230
+ post_install_message:
229
231
  rdoc_options: []
230
232
  require_paths:
231
233
  - lib
@@ -233,7 +235,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
233
235
  requirements:
234
236
  - - ">="
235
237
  - !ruby/object:Gem::Version
236
- version: '0'
238
+ version: 2.6.0
237
239
  required_rubygems_version: !ruby/object:Gem::Requirement
238
240
  requirements:
239
241
  - - ">="
@@ -241,7 +243,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
241
243
  version: '0'
242
244
  requirements: []
243
245
  rubygems_version: 3.1.2
244
- signing_key:
246
+ signing_key:
245
247
  specification_version: 4
246
248
  summary: Implement REST APIs based on OpenApi.
247
249
  test_files: []