openapi_contracts 0.9.1 → 0.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 989040d95a13df9bd1c3138c908071ff04593a4de03422e13ae4262dd6414cdb
4
- data.tar.gz: 1d5202b6e194132c185ed6476e169b4678dbc9e234225c30de65cf2decbeb53b
3
+ metadata.gz: f5da42f028abf33be62403ea88cea635c1593f9ee8519a5f589285e1dfdda0ad
4
+ data.tar.gz: e0dbff76da2693ca9ded40d219616a62ee9cc0271679b6e46398d79cec3ab6f2
5
5
  SHA512:
6
- metadata.gz: 8c4f45d3aa218538737b24630b952fe90c91d824063b2a18573d681809edb6766d28dc0d3503f126b204d48a92ee7120d83f1f73c96dfc94df678d282e275f08
7
- data.tar.gz: 2782dbc72660c32caf5dc8b23d7719df2b1176c2cb2c7feeef838f54f6e306ef3574abb836b26ee66c25f5f8e394f1462d660712f56de90ab37f0aac1e873990
6
+ metadata.gz: 702a9649cf01d4c15525e908d896a4f90cfb12ac927222c4a934950e88dca9d758ef89669e48462706307bae4f87382f0084fa183fa77279ea0ad7b898741355
7
+ data.tar.gz: d2e4d6c7280f8d36e0b2e5beaafeef3c6ea76e85d8fb77b44944d0397aa8da08814af2051e4fd84dd430d182ea418890ff3728a9ff0a6d5ee11514736bb70461
data/README.md CHANGED
@@ -74,6 +74,33 @@ result = OpenapiContracts.match($doc, response, options = {})
74
74
  raise result.errors.merge("/n") unless result.valid?
75
75
  ```
76
76
 
77
+ ## Coverage reporting
78
+
79
+ You can generate a coverage report, giving an indication how many of your OpenApi operations and
80
+ responses are verified.
81
+
82
+ To enable the report, set the configuration `OpenapiContracts.collect_coverage = true`.
83
+
84
+ After the tests completed, you can generate the JSON file, for example:
85
+
86
+ ```ruby
87
+ RSpec.configure do |c|
88
+ c.after(:suite) do
89
+ $your_api_doc.coverage.report.generate(Rails.root.join("openapi_coverage.json"))
90
+ end
91
+ end
92
+ ```
93
+
94
+ In case you run tests on multiple nodes and need to merge reports:
95
+
96
+ ```ruby
97
+ OpenapiContracts::Coverage.merge_reports(
98
+ $your_api_doc,
99
+ *Dir[Rails.root.join("openapi_coverage_*.json")]
100
+ ).generate(Rails.root.join("openapi_coverage.json"))
101
+
102
+ ```
103
+
77
104
  ## How it works
78
105
 
79
106
  It uses the `request.path`, `request.method`, `status` and `headers` on the test subject
@@ -88,16 +115,7 @@ Then it does the following checks:
88
115
 
89
116
  ## Known Issues
90
117
 
91
- ### OpenApi 3.0
92
-
93
- For openapi schemas < 3.1, data is validated using JSON Schema Draft 04, even tho OpenApi 3.0 is a super+subset of Draft 05.
94
- This is due to the fact that we validate the data using json-schemer which does not support 05 and even then would not be fully compatible.
95
- However compatibility issues should be fairly rare and there might be workarounds by describing the data slightly different.
96
-
97
- ### OpenAPi 3.1
98
-
99
- Here exists a similar problem. OpenApi 3.1 is finally fully compatible with JSON Draft 2020-12, but there is no support yet in json-schemer,
100
- so we use the closest draft which is 07.
118
+ None at the moment :)
101
119
 
102
120
  ## Future plans
103
121
 
@@ -0,0 +1,63 @@
1
+ class OpenapiContracts::Coverage
2
+ class Report
3
+ def self.merge(doc, *reports)
4
+ reports.each_with_object(Report.new(doc)) do |r, m|
5
+ m.merge!(r)
6
+ end
7
+ end
8
+
9
+ attr_reader :data
10
+
11
+ def as_json(*)
12
+ report
13
+ end
14
+
15
+ def initialize(doc, data = {})
16
+ @doc = doc
17
+ @data = data
18
+ end
19
+
20
+ def generate(pathname)
21
+ File.write(pathname, JSON.pretty_generate(report))
22
+ end
23
+
24
+ def merge!(data)
25
+ @data.deep_merge!(data) { |_key, val1, val2| val1 + val2 }
26
+ end
27
+
28
+ def meta
29
+ {
30
+ 'operations' => {
31
+ 'covered' => total_covered_operations,
32
+ 'total' => @doc.operations.count
33
+ },
34
+ 'responses' => {
35
+ 'covered' => total_covered_responses,
36
+ 'total' => @doc.responses.count
37
+ }
38
+ }.tap do |d|
39
+ d['operations']['quota'] = d['operations']['covered'].to_f / d['operations']['total']
40
+ d['responses']['quota'] = d['responses']['covered'].to_f / d['responses']['total']
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def report
47
+ {
48
+ 'meta' => meta,
49
+ 'paths' => @data
50
+ }
51
+ end
52
+
53
+ def total_covered_operations
54
+ @doc.operations.select { |o| @data.dig(o.path.to_s, o.verb).present? }.count
55
+ end
56
+
57
+ def total_covered_responses
58
+ @doc.responses.select { |r|
59
+ @data.dig(r.operation.path.to_s, r.operation.verb, r.status).present?
60
+ }.count
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,19 @@
1
+ class OpenapiContracts::Coverage
2
+ class Store
3
+ attr_accessor :data
4
+
5
+ def initialize
6
+ @data = {}
7
+ end
8
+
9
+ def clear!
10
+ @data = {}
11
+ end
12
+
13
+ def increment!(path, method, status, media_type)
14
+ keys = [path, method, status]
15
+ val = @data.dig(*keys) || Hash.new(0).tap { |h| OpenapiContracts.hash_bury!(@data, keys, h) }
16
+ val[media_type] += 1
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ module OpenapiContracts
2
+ class Coverage
3
+ autoload :Report, 'openapi_contracts/coverage/report'
4
+ autoload :Store, 'openapi_contracts/coverage/store'
5
+
6
+ def self.merge_reports(doc, *others)
7
+ reports = others.map { |fp| JSON(File.read(fp))['paths'] }
8
+ Report.merge(doc, *reports)
9
+ end
10
+
11
+ attr_reader :store
12
+
13
+ def initialize(doc)
14
+ @store = Store.new
15
+ @doc = doc
16
+ end
17
+
18
+ delegate :clear!, :data, :increment!, to: :store
19
+
20
+ def report
21
+ Report.new(@doc, store.data)
22
+ end
23
+ end
24
+ end
@@ -2,14 +2,20 @@ module OpenapiContracts
2
2
  class Doc::Operation
3
3
  include Doc::WithParameters
4
4
 
5
+ attr_reader :path
6
+
5
7
  def initialize(path, spec)
6
8
  @path = path
7
9
  @spec = spec
8
- @responses = spec.navigate('responses').each.to_h do |status, subspec| # rubocop:disable Style/HashTransformValues
9
- [status, Doc::Response.new(subspec)]
10
+ @responses = spec.navigate('responses').each.to_h do |status, subspec|
11
+ [status, Doc::Response.new(self, status, subspec)]
10
12
  end
11
13
  end
12
14
 
15
+ def verb
16
+ @spec.pointer[2]
17
+ end
18
+
13
19
  def request_body
14
20
  return @request_body if instance_variable_defined?(:@request_body)
15
21
 
@@ -28,10 +28,7 @@ module OpenapiContracts
28
28
  private
29
29
 
30
30
  def schemer
31
- @schemer ||= begin
32
- schema = @spec.navigate('schema')
33
- JSONSchemer.schema(Validators::SchemaValidation.build_validation_schema(schema))
34
- end
31
+ @schemer ||= Validators::SchemaValidation.validation_schemer(@spec.navigate('schema'))
35
32
  end
36
33
 
37
34
  def integer_parameter_matches?(value)
@@ -10,6 +10,9 @@ module OpenapiContracts
10
10
  @path = path
11
11
  @spec = spec
12
12
  @supported_methods = HTTP_METHODS & @spec.keys
13
+ @operations = @supported_methods.to_h do |verb|
14
+ [verb, Doc::Operation.new(self, @spec.navigate(verb))]
15
+ end
13
16
  end
14
17
 
15
18
  def dynamic?
@@ -17,15 +20,15 @@ module OpenapiContracts
17
20
  end
18
21
 
19
22
  def operations
20
- @supported_methods.each.lazy.map { |m| Doc::Operation.new(self, @spec.navigate(m)) }
23
+ @operations.each_value
21
24
  end
22
25
 
23
26
  def path_regexp
24
27
  @path_regexp ||= begin
25
- re = /\{(\S+)\}/
28
+ re = /\{([^\}]+)\}/
26
29
  @path.gsub(re) { |placeholder|
27
30
  placeholder.match(re) { |m| "(?<#{m[1]}>[^/]*)" }
28
- }.then { |str| Regexp.new(str) }
31
+ }.then { |str| Regexp.new("^#{str}$") }
29
32
  end
30
33
  end
31
34
 
@@ -34,13 +37,15 @@ module OpenapiContracts
34
37
  end
35
38
 
36
39
  def supports_method?(method)
37
- @supported_methods.include?(method)
40
+ @operations.key?(method)
38
41
  end
39
42
 
40
- def with_method(method)
41
- return unless supports_method?(method)
43
+ def to_s
44
+ @path
45
+ end
42
46
 
43
- Doc::Operation.new(self, @spec.navigate(method))
47
+ def with_method(method)
48
+ @operations[method]
44
49
  end
45
50
  end
46
51
  end
@@ -22,7 +22,7 @@ module OpenapiContracts
22
22
  "<#{self.class.name}#{to_a}>"
23
23
  end
24
24
 
25
- delegate :empty?, to: :@segments
25
+ delegate :any?, :empty?, :[], to: :@segments
26
26
 
27
27
  def navigate(*segments)
28
28
  self.class[to_a + segments]
@@ -1,6 +1,12 @@
1
1
  module OpenapiContracts
2
2
  class Doc::Response
3
- def initialize(schema)
3
+ attr_reader :coverage, :schema, :status, :operation
4
+
5
+ delegate :pointer, to: :schema
6
+
7
+ def initialize(operation, status, schema)
8
+ @operation = operation
9
+ @status = status
4
10
  @schema = schema.follow_refs
5
11
  end
6
12
 
@@ -14,7 +14,7 @@ module OpenapiContracts
14
14
  new Parser.call(dir, filename)
15
15
  end
16
16
 
17
- attr_reader :schema
17
+ attr_reader :coverage, :schema
18
18
 
19
19
  def initialize(raw)
20
20
  @schema = Schema.new(raw)
@@ -22,6 +22,7 @@ module OpenapiContracts
22
22
  [path, Path.new(path, @schema.at_pointer(Doc::Pointer['paths', path]))]
23
23
  end
24
24
  @dynamic_paths = paths.select(&:dynamic?)
25
+ @coverage = Coverage.new(self)
25
26
  end
26
27
 
27
28
  # Returns an Enumerator over all paths
@@ -33,14 +34,21 @@ module OpenapiContracts
33
34
  OperationRouter.new(self).route(path, method.downcase)
34
35
  end
35
36
 
37
+ # Returns an Enumerator over all Operations
38
+ def operations(&block)
39
+ return enum_for(:operations) unless block_given?
40
+
41
+ paths.each do |path|
42
+ path.operations.each(&block)
43
+ end
44
+ end
45
+
36
46
  # Returns an Enumerator over all Responses
37
47
  def responses(&block)
38
48
  return enum_for(:responses) unless block_given?
39
49
 
40
- paths.each do |path|
41
- path.operations.each do |operation|
42
- operation.responses.each(&block)
43
- end
50
+ operations.each do |operation|
51
+ operation.responses.each(&block)
44
52
  end
45
53
  end
46
54
 
@@ -19,11 +19,20 @@ module OpenapiContracts
19
19
  return @errors.empty? if instance_variable_defined?(:@errors)
20
20
 
21
21
  @errors = matchers.call
22
+ @doc.coverage.increment!(operation.path.to_s, request_method, status, media_type) if collect_coverage?
22
23
  @errors.empty?
23
24
  end
24
25
 
25
26
  private
26
27
 
28
+ def collect_coverage?
29
+ OpenapiContracts.collect_coverage && @request.present? && @errors.empty? && !@options[:nocov]
30
+ end
31
+
32
+ def media_type
33
+ @response.headers['Content-Type']&.split(';')&.first || 'no_content'
34
+ end
35
+
27
36
  def matchers
28
37
  env = Env.new(
29
38
  options: @options,
@@ -39,10 +48,7 @@ module OpenapiContracts
39
48
  end
40
49
 
41
50
  def operation
42
- @doc.operation_for(
43
- @options.fetch(:path, @request.path),
44
- @request.request_method.downcase
45
- )
51
+ @operation ||= @doc.operation_for(path, request_method)
46
52
  end
47
53
 
48
54
  def request_compatible?
@@ -54,5 +60,17 @@ module OpenapiContracts
54
60
  ancestors = @response.class.ancestors.map(&:to_s)
55
61
  MIN_RESPONSE_ANCESTORS.all? { |s| ancestors.include?(s) }
56
62
  end
63
+
64
+ def request_method
65
+ @request.request_method.downcase
66
+ end
67
+
68
+ def path
69
+ @options.fetch(:path, @request.path)
70
+ end
71
+
72
+ def status
73
+ @response.status.to_s
74
+ end
57
75
  end
58
76
  end
@@ -1,13 +1,25 @@
1
1
  module OpenapiContracts::Parser::Transformers
2
2
  class Pointer < Base
3
3
  def call(object)
4
+ transform_discriminator(object)
5
+ transform_refs(object)
6
+ end
7
+
8
+ private
9
+
10
+ def transform_discriminator(object)
11
+ mappings = object.dig('discriminator', 'mapping')
12
+ return unless mappings.present?
13
+
14
+ mappings.transform_values! { |p| transform_pointer(p) }
15
+ end
16
+
17
+ def transform_refs(object)
4
18
  return unless object['$ref'].present?
5
19
 
6
20
  object['$ref'] = transform_pointer(object['$ref'])
7
21
  end
8
22
 
9
- private
10
-
11
23
  def transform_pointer(target)
12
24
  if %r{^#/(?<pointer>.*)} =~ target
13
25
  # A JSON Pointer
@@ -1,5 +1,4 @@
1
1
  module OpenapiContracts::Parser::Transformers
2
2
  autoload :Base, 'openapi_contracts/parser/transformers/base'
3
- autoload :Nullable, 'openapi_contracts/parser/transformers/nullable'
4
3
  autoload :Pointer, 'openapi_contracts/parser/transformers/pointer'
5
4
  end
@@ -2,7 +2,7 @@ module OpenapiContracts
2
2
  class Parser
3
3
  autoload :Transformers, 'openapi_contracts/parser/transformers'
4
4
 
5
- TRANSFORMERS = [Transformers::Nullable, Transformers::Pointer].freeze
5
+ TRANSFORMERS = [Transformers::Pointer].freeze
6
6
 
7
7
  def self.call(dir, filename)
8
8
  new(dir.join(filename)).parse
@@ -20,13 +20,17 @@ module OpenapiContracts
20
20
  @filenesting = build_file_list
21
21
  @filenesting.each_with_object({}) do |(path, pointer), schema|
22
22
  target = pointer.to_a.reduce(schema) { |d, k| d[k] ||= {} }
23
- target.delete('$ref') # ref file pointers must be replaced
23
+ target.delete('$ref') # ref file pointers should be in the file list so save to delete
24
24
  target.merge! file_to_data(path, pointer)
25
25
  end
26
26
  end
27
27
 
28
28
  private
29
29
 
30
+ # file list consists of
31
+ # - root file
32
+ # - all files in components/
33
+ # - all path files referenced by the root file
30
34
  def build_file_list
31
35
  list = {@rootfile.relative_path_from(@cwd) => Doc::Pointer[]}
32
36
  Dir[File.expand_path('components/**/*.yaml', @cwd)].each do |file|
@@ -19,9 +19,11 @@ RSpec::Matchers.define :match_openapi_doc do |doc, options = {}| # rubocop:disab
19
19
  desc
20
20
  end
21
21
 
22
+ # :nocov:
22
23
  failure_message do |_response|
23
24
  @errors.map { |e| "* #{e}" }.join("\n")
24
25
  end
26
+ # :nocov:
25
27
 
26
28
  def with_http_status(status)
27
29
  if status.is_a? Symbol
@@ -2,39 +2,36 @@ module OpenapiContracts::Validators
2
2
  module SchemaValidation
3
3
  module_function
4
4
 
5
- def build_validation_schema(schema)
6
- schema.raw.merge(
7
- '$ref' => schema.fragment,
8
- '$schema' => schema_draft_version(schema)
9
- )
5
+ def error_to_message(error)
6
+ msg = error['error']
7
+ msg.sub!(/^value/, error['data'].to_json) if error['data'].to_json.length < 50
8
+ msg
10
9
  end
11
10
 
12
- def error_to_message(error)
13
- pointer = " at #{error['data_pointer']}" if error['data_pointer'].present?
14
- if error.key?('details')
15
- error['details'].to_a.map { |(key, val)|
16
- "#{key.humanize}: #{val}#{pointer}"
17
- }.to_sentence
11
+ def schema_draft_version(schema)
12
+ if schema.openapi_version.blank? || schema.openapi_version < Gem::Version.new('3.1')
13
+ JSONSchemer.openapi30
18
14
  else
19
- "#{error['data'].inspect}#{pointer} does not match the schema"
15
+ JSONSchemer.openapi31
20
16
  end
21
17
  end
22
18
 
23
- def schema_draft_version(schema)
24
- if schema.openapi_version.blank? || schema.openapi_version < Gem::Version.new('3.1')
25
- # Closest compatible version is actually draft 5 but not supported by json-schemer
26
- 'http://json-schema.org/draft-04/schema#'
19
+ def validation_schemer(schema)
20
+ schemer = JSONSchemer.schema(schema.raw, meta_schema: schema_draft_version(schema))
21
+ if schema.pointer.any?
22
+ schemer.ref(schema.fragment)
27
23
  else
28
- # >= 3.1 is actually comptable with 2020-12 but not yet supported by json-schemer
29
- 'http://json-schema.org/draft-07/schema#'
24
+ schemer
30
25
  end
31
26
  end
32
27
 
33
28
  def validate_schema(schema, data)
34
- schemer = JSONSchemer.schema(build_validation_schema(schema))
35
- schemer.validate(data).map do |err|
29
+ validation_schemer(schema).validate(data).map do |err|
36
30
  error_to_message(err)
37
31
  end
32
+ rescue JSONSchemer::UnknownRef => e
33
+ # This usually happens when discriminators encounter unknown types
34
+ ["Could not resolve pointer #{e.message.inspect}"]
38
35
  end
39
36
  end
40
37
  end
@@ -11,6 +11,7 @@ require 'rack'
11
11
  require 'yaml'
12
12
 
13
13
  module OpenapiContracts
14
+ autoload :Coverage, 'openapi_contracts/coverage'
14
15
  autoload :Doc, 'openapi_contracts/doc'
15
16
  autoload :Helper, 'openapi_contracts/helper'
16
17
  autoload :Match, 'openapi_contracts/match'
@@ -19,13 +20,28 @@ module OpenapiContracts
19
20
  autoload :PayloadParser, 'openapi_contracts/payload_parser'
20
21
  autoload :Validators, 'openapi_contracts/validators'
21
22
 
23
+ include ActiveSupport::Configurable
24
+
22
25
  Env = Struct.new(:operation, :options, :request, :response, keyword_init: true)
23
26
 
27
+ config_accessor(:collect_coverage) { false }
28
+
24
29
  module_function
25
30
 
26
31
  def match(doc, response, options = {})
27
32
  Match.new(doc, response, options)
28
33
  end
34
+
35
+ def hash_bury(hash, keys, value)
36
+ other = keys.reverse.reduce(value) { |m, k| {k => m} }
37
+ hash.deep_merge other
38
+ end
39
+
40
+ def hash_bury!(hash, keys, value)
41
+ other = keys.reverse.reduce(value) { |m, k| {k => m} }
42
+ hash.deep_merge! other
43
+ other
44
+ end
29
45
  end
30
46
 
31
47
  require 'openapi_contracts/rspec' if defined?(RSpec)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi_contracts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mkon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-16 00:00:00.000000000 Z
11
+ date: 2023-11-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -36,14 +36,14 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 1.0.3
39
+ version: 2.0.0
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 1.0.3
46
+ version: 2.0.0
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rack
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -92,28 +92,28 @@ dependencies:
92
92
  requirements:
93
93
  - - '='
94
94
  - !ruby/object:Gem::Version
95
- version: 1.55.1
95
+ version: 1.57.2
96
96
  type: :development
97
97
  prerelease: false
98
98
  version_requirements: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - '='
101
101
  - !ruby/object:Gem::Version
102
- version: 1.55.1
102
+ version: 1.57.2
103
103
  - !ruby/object:Gem::Dependency
104
104
  name: rubocop-rspec
105
105
  requirement: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - '='
108
108
  - !ruby/object:Gem::Version
109
- version: 2.23.0
109
+ version: 2.25.0
110
110
  type: :development
111
111
  prerelease: false
112
112
  version_requirements: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - '='
115
115
  - !ruby/object:Gem::Version
116
- version: 2.23.0
116
+ version: 2.25.0
117
117
  - !ruby/object:Gem::Dependency
118
118
  name: simplecov
119
119
  requirement: !ruby/object:Gem::Requirement
@@ -137,6 +137,9 @@ extra_rdoc_files: []
137
137
  files:
138
138
  - README.md
139
139
  - lib/openapi_contracts.rb
140
+ - lib/openapi_contracts/coverage.rb
141
+ - lib/openapi_contracts/coverage/report.rb
142
+ - lib/openapi_contracts/coverage/store.rb
140
143
  - lib/openapi_contracts/doc.rb
141
144
  - lib/openapi_contracts/doc/header.rb
142
145
  - lib/openapi_contracts/doc/operation.rb
@@ -153,7 +156,6 @@ files:
153
156
  - lib/openapi_contracts/parser.rb
154
157
  - lib/openapi_contracts/parser/transformers.rb
155
158
  - lib/openapi_contracts/parser/transformers/base.rb
156
- - lib/openapi_contracts/parser/transformers/nullable.rb
157
159
  - lib/openapi_contracts/parser/transformers/pointer.rb
158
160
  - lib/openapi_contracts/payload_parser.rb
159
161
  - lib/openapi_contracts/rspec.rb
@@ -188,7 +190,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
188
190
  - !ruby/object:Gem::Version
189
191
  version: '0'
190
192
  requirements: []
191
- rubygems_version: 3.4.6
193
+ rubygems_version: 3.4.22
192
194
  signing_key:
193
195
  specification_version: 4
194
196
  summary: Openapi schemas as API contracts
@@ -1,10 +0,0 @@
1
- module OpenapiContracts::Parser::Transformers
2
- class Nullable < Base
3
- def call(object)
4
- return unless object['type'].present? && object['nullable'] == true
5
-
6
- object.delete('nullable')
7
- object['type'] = [object['type'], 'null']
8
- end
9
- end
10
- end