openapi_contracts 0.8.0 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +41 -20
- data/lib/openapi_contracts/doc/operation.rb +27 -0
- data/lib/openapi_contracts/doc/parameter.rb +22 -59
- data/lib/openapi_contracts/doc/path.rb +23 -38
- 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 +49 -10
- data/lib/openapi_contracts/doc/with_parameters.rb +9 -0
- data/lib/openapi_contracts/doc.rb +17 -20
- 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 +10 -5
- metadata +32 -22
- 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
@@ -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
|
@@ -1,38 +0,0 @@
|
|
1
|
-
module OpenapiContracts::Validators
|
2
|
-
class Body < Base
|
3
|
-
private
|
4
|
-
|
5
|
-
def validate
|
6
|
-
if spec.no_content?
|
7
|
-
@errors << 'Expected empty response body' if response.body.present?
|
8
|
-
elsif !spec.supports_content_type?(response_content_type)
|
9
|
-
@errors << "Undocumented response with content-type #{response_content_type.inspect}"
|
10
|
-
else
|
11
|
-
validate_schema
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def validate_schema
|
16
|
-
schema = spec.schema_for(response_content_type)
|
17
|
-
# Trick JSONSchemer into validating only against the response schema
|
18
|
-
schemer = JSONSchemer.schema(schema.raw.merge('$ref' => schema.fragment))
|
19
|
-
schemer.validate(JSON(response.body)).each do |err|
|
20
|
-
@errors << error_to_message(err)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def error_to_message(error)
|
25
|
-
if error.key?('details')
|
26
|
-
error['details'].to_a.map { |(key, val)|
|
27
|
-
"#{key.humanize}: #{val} at #{error['data_pointer']}"
|
28
|
-
}.to_sentence
|
29
|
-
else
|
30
|
-
"#{error['data'].inspect} at #{error['data_pointer']} does not match the schema"
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def response_content_type
|
35
|
-
response.headers['Content-Type'].split(';').first
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|