openapi_first 0.13.2 → 0.13.3

Sign up to get free protection for your applications and to get access to all the features.
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: []