openapi_contracts 0.7.1 → 0.9.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 +4 -4
- data/README.md +43 -19
- data/lib/openapi_contracts/doc/operation.rb +27 -0
- data/lib/openapi_contracts/doc/parameter.rb +49 -0
- data/lib/openapi_contracts/doc/path.rb +32 -12
- data/lib/openapi_contracts/doc/pointer.rb +81 -0
- data/lib/openapi_contracts/doc/request.rb +17 -0
- data/lib/openapi_contracts/doc/response.rb +5 -5
- data/lib/openapi_contracts/doc/schema.rb +44 -10
- data/lib/openapi_contracts/doc/with_parameters.rb +9 -0
- data/lib/openapi_contracts/doc.rb +17 -14
- data/lib/openapi_contracts/match.rb +34 -10
- data/lib/openapi_contracts/operation_router.rb +33 -0
- data/lib/openapi_contracts/parser/transformers/base.rb +15 -0
- data/lib/openapi_contracts/parser/transformers/nullable.rb +10 -0
- data/lib/openapi_contracts/parser/transformers/pointer.rb +34 -0
- data/lib/openapi_contracts/parser/transformers.rb +5 -0
- data/lib/openapi_contracts/parser.rb +61 -0
- data/lib/openapi_contracts/payload_parser.rb +39 -0
- data/lib/openapi_contracts/rspec.rb +2 -2
- data/lib/openapi_contracts/validators/base.rb +5 -1
- data/lib/openapi_contracts/validators/documented.rb +12 -5
- data/lib/openapi_contracts/validators/headers.rb +4 -0
- data/lib/openapi_contracts/validators/http_status.rb +2 -6
- data/lib/openapi_contracts/validators/request_body.rb +26 -0
- data/lib/openapi_contracts/validators/response_body.rb +28 -0
- data/lib/openapi_contracts/validators/schema_validation.rb +40 -0
- data/lib/openapi_contracts/validators.rb +9 -6
- data/lib/openapi_contracts.rb +11 -5
- metadata +31 -20
- data/lib/openapi_contracts/doc/file_parser.rb +0 -85
- data/lib/openapi_contracts/doc/method.rb +0 -18
- data/lib/openapi_contracts/doc/parser.rb +0 -44
- data/lib/openapi_contracts/validators/body.rb +0 -38
@@ -0,0 +1,34 @@
|
|
1
|
+
module OpenapiContracts::Parser::Transformers
|
2
|
+
class Pointer < Base
|
3
|
+
def call(object)
|
4
|
+
return unless object['$ref'].present?
|
5
|
+
|
6
|
+
object['$ref'] = transform_pointer(object['$ref'])
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def transform_pointer(target)
|
12
|
+
if %r{^#/(?<pointer>.*)} =~ target
|
13
|
+
# A JSON Pointer
|
14
|
+
generate_absolute_pointer(pointer)
|
15
|
+
elsif %r{^(?<relpath>[^#]+)(?:#/(?<pointer>.*))?} =~ target
|
16
|
+
ptr = @parser.filenesting[@cwd.join(relpath)]
|
17
|
+
tgt = ptr.to_json_pointer
|
18
|
+
tgt += "/#{pointer}" if pointer
|
19
|
+
tgt
|
20
|
+
else
|
21
|
+
target
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# A JSON pointer to the currently parsed file as seen from the root openapi file
|
26
|
+
def generate_absolute_pointer(json_pointer)
|
27
|
+
if @pointer.empty?
|
28
|
+
"#/#{json_pointer}"
|
29
|
+
else
|
30
|
+
"#{@pointer.to_json_pointer}/#{json_pointer}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module OpenapiContracts
|
2
|
+
class Parser
|
3
|
+
autoload :Transformers, 'openapi_contracts/parser/transformers'
|
4
|
+
|
5
|
+
TRANSFORMERS = [Transformers::Nullable, Transformers::Pointer].freeze
|
6
|
+
|
7
|
+
def self.call(dir, filename)
|
8
|
+
new(dir.join(filename)).parse
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :filenesting, :rootfile
|
12
|
+
|
13
|
+
def initialize(rootfile)
|
14
|
+
@cwd = rootfile.parent
|
15
|
+
@rootfile = rootfile
|
16
|
+
@filenesting = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse
|
20
|
+
@filenesting = build_file_list
|
21
|
+
@filenesting.each_with_object({}) do |(path, pointer), schema|
|
22
|
+
target = pointer.to_a.reduce(schema) { |d, k| d[k] ||= {} }
|
23
|
+
target.delete('$ref') # ref file pointers must be replaced
|
24
|
+
target.merge! file_to_data(path, pointer)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def build_file_list
|
31
|
+
list = {@rootfile.relative_path_from(@cwd) => Doc::Pointer[]}
|
32
|
+
Dir[File.expand_path('components/**/*.yaml', @cwd)].each do |file|
|
33
|
+
pathname = Pathname(file).relative_path_from(@cwd)
|
34
|
+
pointer = Doc::Pointer.from_path pathname.sub_ext('')
|
35
|
+
list.merge! pathname => pointer
|
36
|
+
end
|
37
|
+
YAML.safe_load_file(@rootfile).fetch('paths') { {} }.each_pair do |k, v|
|
38
|
+
next unless v['$ref'] && !v['$ref'].start_with?('#')
|
39
|
+
|
40
|
+
list.merge! Pathname(v['$ref']) => Doc::Pointer['paths', k]
|
41
|
+
end
|
42
|
+
list
|
43
|
+
end
|
44
|
+
|
45
|
+
def file_to_data(pathname, pointer)
|
46
|
+
YAML.safe_load_file(@cwd.join(pathname)).tap do |data|
|
47
|
+
transform_objects!(data, pathname.parent, pointer)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def transform_objects!(object, cwd, pointer)
|
52
|
+
case object
|
53
|
+
when Hash
|
54
|
+
object.each_value { |v| transform_objects!(v, cwd, pointer) }
|
55
|
+
TRANSFORMERS.map { |t| t.new(self, cwd, pointer) }.each { |t| t.call(object) }
|
56
|
+
when Array
|
57
|
+
object.each { |o| transform_objects!(o, cwd, pointer) }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module OpenapiContracts
|
4
|
+
class PayloadParser
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
class << self
|
8
|
+
delegate :parse, :register, to: :instance
|
9
|
+
end
|
10
|
+
|
11
|
+
Entry = Struct.new(:matcher, :parser) do
|
12
|
+
def call(raw)
|
13
|
+
parser.call(raw)
|
14
|
+
end
|
15
|
+
|
16
|
+
def match?(media_type)
|
17
|
+
matcher == media_type || matcher.match?(media_type)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@parsers = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse(media_type, payload)
|
26
|
+
parser = @parsers.find { |e| e.match?(media_type) }
|
27
|
+
raise ArgumentError, "#{media_type.inspect} is not supported yet" unless parser
|
28
|
+
|
29
|
+
parser.call(payload)
|
30
|
+
end
|
31
|
+
|
32
|
+
def register(matcher, parser)
|
33
|
+
@parsers << Entry.new(matcher, parser)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
PayloadParser.register(%r{(/|\+)json$}, ->(raw) { JSON(raw) })
|
38
|
+
PayloadParser.register('application/x-www-form-urlencoded', ->(raw) { Rack::Utils.parse_nested_query(raw) })
|
39
|
+
end
|
@@ -14,12 +14,12 @@ RSpec::Matchers.define :match_openapi_doc do |doc, options = {}| # rubocop:disab
|
|
14
14
|
end
|
15
15
|
|
16
16
|
description do
|
17
|
-
desc = '
|
17
|
+
desc = 'match the openapi schema'
|
18
18
|
desc << " with #{http_status_desc(@status)}" if @status
|
19
19
|
desc
|
20
20
|
end
|
21
21
|
|
22
|
-
failure_message do |
|
22
|
+
failure_message do |_response|
|
23
23
|
@errors.map { |e| "* #{e}" }.join("\n")
|
24
24
|
end
|
25
25
|
|
@@ -22,7 +22,11 @@ module OpenapiContracts::Validators
|
|
22
22
|
|
23
23
|
private
|
24
24
|
|
25
|
-
delegate :
|
25
|
+
delegate :operation, :options, :request, :response, to: :@env
|
26
|
+
|
27
|
+
def response_desc
|
28
|
+
"#{request.request_method} #{request.path}"
|
29
|
+
end
|
26
30
|
|
27
31
|
# :nocov:
|
28
32
|
def validate
|
@@ -1,18 +1,25 @@
|
|
1
1
|
module OpenapiContracts::Validators
|
2
|
+
# Purpose of this validator
|
3
|
+
# * ensure the operation is documented (combination http-method + path)
|
4
|
+
# * ensure the response-status is documented on the operation
|
2
5
|
class Documented < Base
|
3
6
|
self.final = true
|
4
7
|
|
5
8
|
private
|
6
9
|
|
7
10
|
def validate
|
8
|
-
return
|
11
|
+
return operation_missing unless operation
|
9
12
|
|
10
|
-
|
11
|
-
|
13
|
+
response_missing unless operation.response_for_status(response.status)
|
14
|
+
end
|
15
|
+
|
16
|
+
def operation_missing
|
17
|
+
@errors << "Undocumented operation for #{response_desc.inspect}"
|
12
18
|
end
|
13
19
|
|
14
|
-
def
|
15
|
-
|
20
|
+
def response_missing
|
21
|
+
status_desc = http_status_desc(response.status)
|
22
|
+
@errors << "Undocumented response for #{response_desc.inspect} with #{status_desc}"
|
16
23
|
end
|
17
24
|
end
|
18
25
|
end
|
@@ -5,13 +5,9 @@ module OpenapiContracts::Validators
|
|
5
5
|
private
|
6
6
|
|
7
7
|
def validate
|
8
|
-
return if
|
8
|
+
return if options[:status] == response.status
|
9
9
|
|
10
|
-
@errors << "Response has #{http_status_desc}"
|
11
|
-
end
|
12
|
-
|
13
|
-
def http_status_desc
|
14
|
-
"http status #{Rack::Utils::HTTP_STATUS_CODES[response.status]} (#{response.status})"
|
10
|
+
@errors << "Response has #{http_status_desc(response.status)}"
|
15
11
|
end
|
16
12
|
end
|
17
13
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module OpenapiContracts::Validators
|
2
|
+
class RequestBody < Base
|
3
|
+
include SchemaValidation
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
delegate :media_type, to: :request
|
8
|
+
delegate :request_body, to: :operation
|
9
|
+
|
10
|
+
def data_for_validation
|
11
|
+
request.body.rewind
|
12
|
+
raw = request.body.read
|
13
|
+
OpenapiContracts::PayloadParser.parse(media_type, raw)
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate
|
17
|
+
if !request_body
|
18
|
+
@errors << "Undocumented request body for #{response_desc.inspect}"
|
19
|
+
elsif !request_body.supports_media_type?(media_type)
|
20
|
+
@errors << "Undocumented request with media-type #{media_type.inspect}"
|
21
|
+
else
|
22
|
+
@errors += validate_schema(request_body.schema_for(media_type), data_for_validation)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module OpenapiContracts::Validators
|
2
|
+
class ResponseBody < Base
|
3
|
+
include SchemaValidation
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
delegate :media_type, to: :response
|
8
|
+
|
9
|
+
def data_for_validation
|
10
|
+
# ActionDispatch::Response body is a plain string, while Rack::Response returns an array
|
11
|
+
OpenapiContracts::PayloadParser.parse(media_type, Array.wrap(response.body).join)
|
12
|
+
end
|
13
|
+
|
14
|
+
def spec
|
15
|
+
@spec ||= operation.response_for_status(response.status)
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate
|
19
|
+
if spec.no_content?
|
20
|
+
@errors << 'Expected empty response body' if Array.wrap(response.body).any?(&:present?)
|
21
|
+
elsif !spec.supports_media_type?(media_type)
|
22
|
+
@errors << "Undocumented response with content-type #{media_type.inspect}"
|
23
|
+
else
|
24
|
+
@errors += validate_schema(spec.schema_for(media_type), data_for_validation)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module OpenapiContracts::Validators
|
2
|
+
module SchemaValidation
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def build_validation_schema(schema)
|
6
|
+
schema.raw.merge(
|
7
|
+
'$ref' => schema.fragment,
|
8
|
+
'$schema' => schema_draft_version(schema)
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
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
|
18
|
+
else
|
19
|
+
"#{error['data'].inspect}#{pointer} does not match the schema"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
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#'
|
27
|
+
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#'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_schema(schema, data)
|
34
|
+
schemer = JSONSchemer.schema(build_validation_schema(schema))
|
35
|
+
schemer.validate(data).map do |err|
|
36
|
+
error_to_message(err)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,16 +1,19 @@
|
|
1
1
|
module OpenapiContracts
|
2
2
|
module Validators
|
3
|
-
autoload :Base,
|
4
|
-
autoload :
|
5
|
-
autoload :
|
6
|
-
autoload :
|
7
|
-
autoload :
|
3
|
+
autoload :Base, 'openapi_contracts/validators/base'
|
4
|
+
autoload :Documented, 'openapi_contracts/validators/documented'
|
5
|
+
autoload :Headers, 'openapi_contracts/validators/headers'
|
6
|
+
autoload :HttpStatus, 'openapi_contracts/validators/http_status'
|
7
|
+
autoload :RequestBody, 'openapi_contracts/validators/request_body'
|
8
|
+
autoload :ResponseBody, 'openapi_contracts/validators/response_body'
|
9
|
+
autoload :SchemaValidation, 'openapi_contracts/validators/schema_validation'
|
8
10
|
|
9
11
|
# Defines order of matching
|
10
12
|
ALL = [
|
11
13
|
Documented,
|
12
14
|
HttpStatus,
|
13
|
-
|
15
|
+
RequestBody,
|
16
|
+
ResponseBody,
|
14
17
|
Headers
|
15
18
|
].freeze
|
16
19
|
end
|
data/lib/openapi_contracts.rb
CHANGED
@@ -1,19 +1,25 @@
|
|
1
1
|
require 'active_support'
|
2
2
|
require 'active_support/core_ext/array'
|
3
|
+
require 'active_support/core_ext/hash'
|
3
4
|
require 'active_support/core_ext/class'
|
4
5
|
require 'active_support/core_ext/module'
|
5
6
|
require 'active_support/core_ext/string'
|
7
|
+
require 'rubygems/version'
|
6
8
|
|
7
9
|
require 'json_schemer'
|
10
|
+
require 'rack'
|
8
11
|
require 'yaml'
|
9
12
|
|
10
13
|
module OpenapiContracts
|
11
|
-
autoload :Doc,
|
12
|
-
autoload :Helper,
|
13
|
-
autoload :Match,
|
14
|
-
autoload :
|
14
|
+
autoload :Doc, 'openapi_contracts/doc'
|
15
|
+
autoload :Helper, 'openapi_contracts/helper'
|
16
|
+
autoload :Match, 'openapi_contracts/match'
|
17
|
+
autoload :OperationRouter, 'openapi_contracts/operation_router'
|
18
|
+
autoload :Parser, 'openapi_contracts/parser'
|
19
|
+
autoload :PayloadParser, 'openapi_contracts/payload_parser'
|
20
|
+
autoload :Validators, 'openapi_contracts/validators'
|
15
21
|
|
16
|
-
Env = Struct.new(:
|
22
|
+
Env = Struct.new(:operation, :options, :request, :response, keyword_init: true)
|
17
23
|
|
18
24
|
module_function
|
19
25
|
|
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.
|
4
|
+
version: 0.9.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-
|
11
|
+
date: 2023-08-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -36,42 +36,42 @@ dependencies:
|
|
36
36
|
requirements:
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version: 0.
|
39
|
+
version: 1.0.3
|
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: 0.
|
46
|
+
version: 1.0.3
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
48
|
+
name: rack
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
|
-
- - "
|
51
|
+
- - ">="
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version:
|
54
|
-
type: :
|
53
|
+
version: 2.0.0
|
54
|
+
type: :runtime
|
55
55
|
prerelease: false
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
|
-
- - "
|
58
|
+
- - ">="
|
59
59
|
- !ruby/object:Gem::Version
|
60
|
-
version:
|
60
|
+
version: 2.0.0
|
61
61
|
- !ruby/object:Gem::Dependency
|
62
|
-
name:
|
62
|
+
name: json_spec
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
64
64
|
requirements:
|
65
65
|
- - "~>"
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version:
|
67
|
+
version: 1.1.5
|
68
68
|
type: :development
|
69
69
|
prerelease: false
|
70
70
|
version_requirements: !ruby/object:Gem::Requirement
|
71
71
|
requirements:
|
72
72
|
- - "~>"
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version:
|
74
|
+
version: 1.1.5
|
75
75
|
- !ruby/object:Gem::Dependency
|
76
76
|
name: rspec
|
77
77
|
requirement: !ruby/object:Gem::Requirement
|
@@ -92,14 +92,14 @@ dependencies:
|
|
92
92
|
requirements:
|
93
93
|
- - '='
|
94
94
|
- !ruby/object:Gem::Version
|
95
|
-
version: 1.
|
95
|
+
version: 1.54.1
|
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.
|
102
|
+
version: 1.54.1
|
103
103
|
- !ruby/object:Gem::Dependency
|
104
104
|
name: rubocop-rspec
|
105
105
|
requirement: !ruby/object:Gem::Requirement
|
@@ -138,22 +138,33 @@ files:
|
|
138
138
|
- README.md
|
139
139
|
- lib/openapi_contracts.rb
|
140
140
|
- lib/openapi_contracts/doc.rb
|
141
|
-
- lib/openapi_contracts/doc/file_parser.rb
|
142
141
|
- lib/openapi_contracts/doc/header.rb
|
143
|
-
- lib/openapi_contracts/doc/
|
144
|
-
- lib/openapi_contracts/doc/
|
142
|
+
- lib/openapi_contracts/doc/operation.rb
|
143
|
+
- lib/openapi_contracts/doc/parameter.rb
|
145
144
|
- lib/openapi_contracts/doc/path.rb
|
145
|
+
- lib/openapi_contracts/doc/pointer.rb
|
146
|
+
- lib/openapi_contracts/doc/request.rb
|
146
147
|
- lib/openapi_contracts/doc/response.rb
|
147
148
|
- lib/openapi_contracts/doc/schema.rb
|
149
|
+
- lib/openapi_contracts/doc/with_parameters.rb
|
148
150
|
- lib/openapi_contracts/helper.rb
|
149
151
|
- lib/openapi_contracts/match.rb
|
152
|
+
- lib/openapi_contracts/operation_router.rb
|
153
|
+
- lib/openapi_contracts/parser.rb
|
154
|
+
- lib/openapi_contracts/parser/transformers.rb
|
155
|
+
- lib/openapi_contracts/parser/transformers/base.rb
|
156
|
+
- lib/openapi_contracts/parser/transformers/nullable.rb
|
157
|
+
- lib/openapi_contracts/parser/transformers/pointer.rb
|
158
|
+
- lib/openapi_contracts/payload_parser.rb
|
150
159
|
- lib/openapi_contracts/rspec.rb
|
151
160
|
- lib/openapi_contracts/validators.rb
|
152
161
|
- lib/openapi_contracts/validators/base.rb
|
153
|
-
- lib/openapi_contracts/validators/body.rb
|
154
162
|
- lib/openapi_contracts/validators/documented.rb
|
155
163
|
- lib/openapi_contracts/validators/headers.rb
|
156
164
|
- lib/openapi_contracts/validators/http_status.rb
|
165
|
+
- lib/openapi_contracts/validators/request_body.rb
|
166
|
+
- lib/openapi_contracts/validators/response_body.rb
|
167
|
+
- lib/openapi_contracts/validators/schema_validation.rb
|
157
168
|
homepage: https://github.com/mkon/openapi_contracts
|
158
169
|
licenses:
|
159
170
|
- MIT
|
@@ -167,7 +178,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
167
178
|
requirements:
|
168
179
|
- - ">="
|
169
180
|
- !ruby/object:Gem::Version
|
170
|
-
version: '
|
181
|
+
version: '3.0'
|
171
182
|
- - "<"
|
172
183
|
- !ruby/object:Gem::Version
|
173
184
|
version: '3.3'
|
@@ -1,85 +0,0 @@
|
|
1
|
-
module OpenapiContracts
|
2
|
-
class Doc::FileParser
|
3
|
-
Result = Struct.new(:data, :path) do
|
4
|
-
def to_mergable_hash
|
5
|
-
d = data
|
6
|
-
path.ascend do |p|
|
7
|
-
d = {p.basename.to_s => d}
|
8
|
-
end
|
9
|
-
d
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
def self.parse(rootfile, pathname)
|
14
|
-
new(rootfile, pathname).call
|
15
|
-
end
|
16
|
-
|
17
|
-
def initialize(rootfile, pathname)
|
18
|
-
@root = rootfile.parent
|
19
|
-
@rootfile = rootfile
|
20
|
-
@pathname = pathname.relative? ? @root.join(pathname) : pathname
|
21
|
-
end
|
22
|
-
|
23
|
-
def call
|
24
|
-
schema = YAML.safe_load(File.read(@pathname))
|
25
|
-
schema = transform_hash(schema)
|
26
|
-
Result.new(schema, @pathname.relative_path_from(@root).sub_ext(''))
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
def transform_hash(data)
|
32
|
-
data.each_with_object({}) do |(key, val), m|
|
33
|
-
if val.is_a?(Array)
|
34
|
-
m[key] = transform_array(val)
|
35
|
-
elsif val.is_a?(Hash)
|
36
|
-
m[key] = transform_hash(val)
|
37
|
-
elsif key == '$ref'
|
38
|
-
m.merge! transform_pointer(key, val)
|
39
|
-
else
|
40
|
-
m[key] = val
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def transform_array(data)
|
46
|
-
data.each_with_object([]) do |item, m|
|
47
|
-
case item
|
48
|
-
when Hash
|
49
|
-
m.push transform_hash(item)
|
50
|
-
when Array
|
51
|
-
m.push transform_array(item)
|
52
|
-
else
|
53
|
-
m.push item
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def transform_pointer(key, target)
|
59
|
-
if %r{^#/(?<pointer>.*)} =~ target
|
60
|
-
# A JSON Pointer
|
61
|
-
{key => generate_absolute_pointer(pointer)}
|
62
|
-
elsif %r{^(?<relpath>[^#]+)(?:#/(?<pointer>.*))?} =~ target
|
63
|
-
if relpath.start_with?('paths') # path description file pointer
|
64
|
-
# Inline the file contents
|
65
|
-
self.class.parse(@rootfile, Pathname(relpath)).data
|
66
|
-
else # A file pointer with potential JSON sub-pointer
|
67
|
-
tgt = @pathname.parent.relative_path_from(@root).join(relpath).sub_ext('')
|
68
|
-
tgt = tgt.join(pointer) if pointer
|
69
|
-
{key => "#/#{tgt}"}
|
70
|
-
end
|
71
|
-
else
|
72
|
-
{key => target}
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
# A JSON pointer to the currently parsed file as seen from the root openapi file
|
77
|
-
def generate_absolute_pointer(json_pointer)
|
78
|
-
if @rootfile == @pathname
|
79
|
-
"#/#{json_pointer}"
|
80
|
-
else
|
81
|
-
"#/#{@pathname.relative_path_from(@root).sub_ext('').join(json_pointer)}"
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
module OpenapiContracts
|
2
|
-
class Doc::Method
|
3
|
-
def initialize(schema)
|
4
|
-
@schema = schema
|
5
|
-
@responses = schema['responses'].to_h do |status, _|
|
6
|
-
[status, Doc::Response.new(schema.navigate('responses', status))]
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
|
-
def responses
|
11
|
-
@responses.each_value
|
12
|
-
end
|
13
|
-
|
14
|
-
def with_status(status)
|
15
|
-
@responses[status]
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
@@ -1,44 +0,0 @@
|
|
1
|
-
module OpenapiContracts
|
2
|
-
class Doc::Parser
|
3
|
-
def self.call(dir, filename)
|
4
|
-
new(dir.join(filename)).parse
|
5
|
-
end
|
6
|
-
|
7
|
-
def initialize(rootfile)
|
8
|
-
@rootfile = rootfile
|
9
|
-
end
|
10
|
-
|
11
|
-
def parse
|
12
|
-
file = Doc::FileParser.parse(@rootfile, @rootfile)
|
13
|
-
data = file.data
|
14
|
-
data.deep_merge! merge_components
|
15
|
-
nullable_to_type!(data)
|
16
|
-
# debugger
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
def merge_components
|
22
|
-
data = {}
|
23
|
-
Dir[File.expand_path('components/**/*.yaml', @rootfile.parent)].each do |file|
|
24
|
-
result = Doc::FileParser.parse(@rootfile, Pathname(file))
|
25
|
-
data.deep_merge!(result.to_mergable_hash)
|
26
|
-
end
|
27
|
-
data
|
28
|
-
end
|
29
|
-
|
30
|
-
def nullable_to_type!(object)
|
31
|
-
case object
|
32
|
-
when Hash
|
33
|
-
if object['type'] && object['nullable']
|
34
|
-
object['type'] = [object['type'], 'null']
|
35
|
-
object.delete 'nullable'
|
36
|
-
else
|
37
|
-
object.each_value { |o| nullable_to_type! o }
|
38
|
-
end
|
39
|
-
when Array
|
40
|
-
object.each { |o| nullable_to_type! o }
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|