openapi3_parser 0.6.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 +5 -5
- data/.github/workflows/ci.yml +23 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +18 -3
- data/.ruby-version +1 -1
- data/CHANGELOG.md +41 -0
- data/README.md +7 -4
- data/TODO.md +1 -1
- data/lib/openapi3_parser.rb +2 -3
- data/lib/openapi3_parser/array_sentence.rb +2 -1
- data/lib/openapi3_parser/document.rb +3 -2
- data/lib/openapi3_parser/error.rb +11 -1
- data/lib/openapi3_parser/node/array.rb +9 -1
- data/lib/openapi3_parser/node/context.rb +34 -4
- data/lib/openapi3_parser/node/map.rb +17 -1
- data/lib/openapi3_parser/node/object.rb +17 -0
- data/lib/openapi3_parser/node/operation.rb +8 -0
- data/lib/openapi3_parser/node/path_item.rb +8 -0
- data/lib/openapi3_parser/node/placeholder.rb +16 -12
- data/lib/openapi3_parser/node/schema.rb +50 -3
- data/lib/openapi3_parser/node_factory.rb +1 -1
- data/lib/openapi3_parser/node_factory/array.rb +34 -26
- data/lib/openapi3_parser/node_factory/context.rb +9 -2
- data/lib/openapi3_parser/node_factory/discriminator.rb +2 -0
- data/lib/openapi3_parser/node_factory/field.rb +2 -0
- data/lib/openapi3_parser/node_factory/fields/reference.rb +1 -0
- data/lib/openapi3_parser/node_factory/map.rb +12 -10
- data/lib/openapi3_parser/node_factory/media_type.rb +1 -0
- data/lib/openapi3_parser/node_factory/object.rb +4 -1
- data/lib/openapi3_parser/node_factory/object_factory/dsl.rb +7 -5
- data/lib/openapi3_parser/node_factory/object_factory/field_config.rb +9 -7
- data/lib/openapi3_parser/node_factory/object_factory/node_builder.rb +12 -13
- data/lib/openapi3_parser/node_factory/object_factory/validator.rb +25 -21
- data/lib/openapi3_parser/node_factory/openapi.rb +2 -0
- data/lib/openapi3_parser/node_factory/operation.rb +9 -0
- data/lib/openapi3_parser/node_factory/parameter.rb +2 -0
- data/lib/openapi3_parser/node_factory/parameter_like.rb +2 -1
- data/lib/openapi3_parser/node_factory/path_item.rb +27 -5
- data/lib/openapi3_parser/node_factory/paths.rb +1 -1
- data/lib/openapi3_parser/node_factory/reference.rb +9 -1
- data/lib/openapi3_parser/node_factory/request_body.rb +2 -4
- data/lib/openapi3_parser/node_factory/response.rb +1 -0
- data/lib/openapi3_parser/node_factory/responses.rb +1 -1
- data/lib/openapi3_parser/node_factory/schema.rb +5 -2
- data/lib/openapi3_parser/node_factory/server_variable.rb +1 -0
- data/lib/openapi3_parser/node_factory/type_checker.rb +6 -0
- data/lib/openapi3_parser/source.rb +2 -0
- data/lib/openapi3_parser/source/location.rb +6 -0
- data/lib/openapi3_parser/source/pointer.rb +9 -4
- data/lib/openapi3_parser/source_input.rb +9 -9
- data/lib/openapi3_parser/source_input/file.rb +3 -1
- data/lib/openapi3_parser/source_input/raw.rb +4 -1
- data/lib/openapi3_parser/source_input/resolve_next.rb +1 -0
- data/lib/openapi3_parser/source_input/string_parser.rb +3 -2
- data/lib/openapi3_parser/source_input/url.rb +3 -1
- data/lib/openapi3_parser/validation/error.rb +2 -0
- data/lib/openapi3_parser/validation/error_collection.rb +1 -1
- data/lib/openapi3_parser/validation/input_validator.rb +1 -1
- data/lib/openapi3_parser/validators/component_keys.rb +1 -1
- data/lib/openapi3_parser/validators/duplicate_parameters.rb +1 -0
- data/lib/openapi3_parser/validators/email.rb +3 -3
- data/lib/openapi3_parser/validators/media_type.rb +1 -1
- data/lib/openapi3_parser/validators/mutually_exclusive_fields.rb +8 -8
- data/lib/openapi3_parser/validators/reference.rb +2 -0
- data/lib/openapi3_parser/validators/required_fields.rb +2 -2
- data/lib/openapi3_parser/validators/unexpected_fields.rb +3 -2
- data/lib/openapi3_parser/validators/url.rb +2 -7
- data/lib/openapi3_parser/version.rb +1 -1
- data/openapi3_parser.gemspec +12 -8
- metadata +81 -25
- data/.travis.yml +0 -11
|
@@ -39,7 +39,7 @@ module Openapi3Parser
|
|
|
39
39
|
field "default"
|
|
40
40
|
|
|
41
41
|
field "nullable", input_type: :boolean, default: false
|
|
42
|
-
field "discriminator", factory: :
|
|
42
|
+
field "discriminator", factory: :discriminator_factory
|
|
43
43
|
field "readOnly", input_type: :boolean, default: false
|
|
44
44
|
field "writeOnly", input_type: :boolean, default: false
|
|
45
45
|
field "xml", factory: :xml_factory
|
|
@@ -61,6 +61,7 @@ module Openapi3Parser
|
|
|
61
61
|
def read_only_or_write_only(validatable)
|
|
62
62
|
input = validatable.input
|
|
63
63
|
return if [input["readOnly"], input["writeOnly"]].uniq != [true]
|
|
64
|
+
|
|
64
65
|
validatable.add_error("readOnly and writeOnly cannot both be true")
|
|
65
66
|
end
|
|
66
67
|
|
|
@@ -80,7 +81,7 @@ module Openapi3Parser
|
|
|
80
81
|
NodeFactory::Array.new(context, default: nil)
|
|
81
82
|
end
|
|
82
83
|
|
|
83
|
-
def
|
|
84
|
+
def discriminator_factory(context)
|
|
84
85
|
NodeFactory::Discriminator.new(context)
|
|
85
86
|
end
|
|
86
87
|
|
|
@@ -114,11 +115,13 @@ module Openapi3Parser
|
|
|
114
115
|
def additional_properties_input_type(validatable)
|
|
115
116
|
input = validatable.input
|
|
116
117
|
return if [true, false].include?(input) || input.is_a?(Hash)
|
|
118
|
+
|
|
117
119
|
validatable.add_error("Expected a Boolean or an Object")
|
|
118
120
|
end
|
|
119
121
|
|
|
120
122
|
def additional_properties_factory(context)
|
|
121
123
|
return context.input if [true, false].include?(context.input)
|
|
124
|
+
|
|
122
125
|
referenceable_schema(context)
|
|
123
126
|
end
|
|
124
127
|
end
|
|
@@ -27,9 +27,11 @@ module Openapi3Parser
|
|
|
27
27
|
|
|
28
28
|
def validate_type(validatable, context)
|
|
29
29
|
return true unless type
|
|
30
|
+
|
|
30
31
|
context ||= validatable.context
|
|
31
32
|
valid_type?(context.input).tap do |valid|
|
|
32
33
|
next if valid
|
|
34
|
+
|
|
33
35
|
validatable.add_error("Invalid type. #{field_error_message}",
|
|
34
36
|
context)
|
|
35
37
|
end
|
|
@@ -37,9 +39,11 @@ module Openapi3Parser
|
|
|
37
39
|
|
|
38
40
|
def validate_keys(validatable, context)
|
|
39
41
|
return true unless type
|
|
42
|
+
|
|
40
43
|
context ||= validatable.context
|
|
41
44
|
valid_keys?(context.input).tap do |valid|
|
|
42
45
|
next if valid
|
|
46
|
+
|
|
43
47
|
validatable.add_error("Invalid keys. #{keys_error_message}",
|
|
44
48
|
context)
|
|
45
49
|
end
|
|
@@ -47,6 +51,7 @@ module Openapi3Parser
|
|
|
47
51
|
|
|
48
52
|
def raise_on_invalid_type(context)
|
|
49
53
|
return true if !type || valid_type?(context.input)
|
|
54
|
+
|
|
50
55
|
raise Error::InvalidType,
|
|
51
56
|
"Invalid type for #{context.location_summary}: "\
|
|
52
57
|
"#{field_error_message}"
|
|
@@ -54,6 +59,7 @@ module Openapi3Parser
|
|
|
54
59
|
|
|
55
60
|
def raise_on_invalid_keys(context)
|
|
56
61
|
return true if !type || valid_keys?(context.input)
|
|
62
|
+
|
|
57
63
|
raise Error::InvalidType,
|
|
58
64
|
"Invalid keys for #{context.location_summary}: "\
|
|
59
65
|
"#{keys_error_message}"
|
|
@@ -93,6 +93,7 @@ module Openapi3Parser
|
|
|
93
93
|
# @return [Object]
|
|
94
94
|
def data_at_pointer(json_pointer)
|
|
95
95
|
return data if json_pointer.empty?
|
|
96
|
+
|
|
96
97
|
data.dig(*json_pointer) if data.respond_to?(:dig)
|
|
97
98
|
end
|
|
98
99
|
|
|
@@ -104,6 +105,7 @@ module Openapi3Parser
|
|
|
104
105
|
# @return [String]
|
|
105
106
|
def relative_to_root
|
|
106
107
|
return "" if root?
|
|
108
|
+
|
|
107
109
|
source_input.relative_to(document.root_source.source_input)
|
|
108
110
|
end
|
|
109
111
|
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
3
5
|
module Openapi3Parser
|
|
4
6
|
class Source
|
|
5
7
|
# Class used to represent a location within an OpenAPI document.
|
|
6
8
|
# It contains a source, which is the source file/data used for the contents
|
|
7
9
|
# and the pointer which indicates where in the object like file the data is
|
|
8
10
|
class Location
|
|
11
|
+
extend Forwardable
|
|
12
|
+
|
|
9
13
|
def self.next_field(location, field)
|
|
10
14
|
new(location.source, location.pointer.segments + [field])
|
|
11
15
|
end
|
|
12
16
|
|
|
17
|
+
def_delegators :pointer, :root?
|
|
13
18
|
attr_reader :source, :pointer
|
|
14
19
|
|
|
15
20
|
# @param [Openapi3Parser::Source] source
|
|
@@ -21,6 +26,7 @@ module Openapi3Parser
|
|
|
21
26
|
|
|
22
27
|
def ==(other)
|
|
23
28
|
return false unless other.instance_of?(self.class)
|
|
29
|
+
|
|
24
30
|
source == other.source && pointer == other.pointer
|
|
25
31
|
end
|
|
26
32
|
|
|
@@ -12,10 +12,11 @@ module Openapi3Parser
|
|
|
12
12
|
absolute = fragment[0] == "/"
|
|
13
13
|
segments = fragment.split("/").map do |part|
|
|
14
14
|
next if part == ""
|
|
15
|
+
|
|
15
16
|
unescaped = CGI.unescape(part.gsub("%20", "+"))
|
|
16
17
|
unescaped.match?(/\A\d+\z/) ? unescaped.to_i : unescaped
|
|
17
18
|
end
|
|
18
|
-
new(segments.compact, absolute)
|
|
19
|
+
new(segments.compact, absolute: absolute)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def self.merge_pointers(base_pointer, new_pointer)
|
|
@@ -26,7 +27,7 @@ module Openapi3Parser
|
|
|
26
27
|
|
|
27
28
|
# @param [::Array] segments
|
|
28
29
|
# @param [Boolean] absolute
|
|
29
|
-
def initialize(segments, absolute
|
|
30
|
+
def initialize(segments, absolute: true)
|
|
30
31
|
@segments = segments.freeze
|
|
31
32
|
@absolute = absolute
|
|
32
33
|
end
|
|
@@ -38,7 +39,7 @@ module Openapi3Parser
|
|
|
38
39
|
def fragment
|
|
39
40
|
fragment = segments.map { |s| CGI.escape(s.to_s).gsub("+", "%20") }
|
|
40
41
|
.join("/")
|
|
41
|
-
"
|
|
42
|
+
"##{absolute ? fragment.prepend('/') : fragment}"
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
def to_s
|
|
@@ -49,6 +50,10 @@ module Openapi3Parser
|
|
|
49
50
|
%{#{self.class.name}(segments: #{segments}, fragment: "#{fragment}")}
|
|
50
51
|
end
|
|
51
52
|
|
|
53
|
+
def root?
|
|
54
|
+
segments.empty?
|
|
55
|
+
end
|
|
56
|
+
|
|
52
57
|
class MergePointers
|
|
53
58
|
private_class_method :new
|
|
54
59
|
|
|
@@ -75,7 +80,7 @@ module Openapi3Parser
|
|
|
75
80
|
def create_pointer(pointer_like)
|
|
76
81
|
case pointer_like
|
|
77
82
|
when Pointer then pointer_like
|
|
78
|
-
when ::Array then Pointer.new(pointer_like, false)
|
|
83
|
+
when ::Array then Pointer.new(pointer_like, absolute: false)
|
|
79
84
|
when ::String then Pointer.from_fragment(pointer_like)
|
|
80
85
|
when nil then nil
|
|
81
86
|
else raise Openapi3Parser::Error, "Unexpected type for pointer"
|
|
@@ -20,6 +20,14 @@ module Openapi3Parser
|
|
|
20
20
|
class SourceInput
|
|
21
21
|
attr_reader :access_error, :parse_error
|
|
22
22
|
|
|
23
|
+
def initialize
|
|
24
|
+
return if access_error
|
|
25
|
+
|
|
26
|
+
@contents = parse_contents
|
|
27
|
+
rescue ::StandardError => e
|
|
28
|
+
@parse_error = Error::UnparsableInput.new(e.message)
|
|
29
|
+
end
|
|
30
|
+
|
|
23
31
|
# Indicates that the data within this input is suitable (i.e. can parse
|
|
24
32
|
# underlying JSON or YAML) for trying to use as part of a Document
|
|
25
33
|
def available?
|
|
@@ -45,6 +53,7 @@ module Openapi3Parser
|
|
|
45
53
|
def contents
|
|
46
54
|
raise access_error if access_error
|
|
47
55
|
raise parse_error if parse_error
|
|
56
|
+
|
|
48
57
|
@contents
|
|
49
58
|
end
|
|
50
59
|
|
|
@@ -56,14 +65,5 @@ module Openapi3Parser
|
|
|
56
65
|
def relative_to(_source_input)
|
|
57
66
|
""
|
|
58
67
|
end
|
|
59
|
-
|
|
60
|
-
private
|
|
61
|
-
|
|
62
|
-
def initialize_contents
|
|
63
|
-
return if access_error
|
|
64
|
-
@contents = parse_contents
|
|
65
|
-
rescue ::StandardError => e
|
|
66
|
-
@parse_error = Error::UnparsableInput.new(e.message)
|
|
67
|
-
end
|
|
68
68
|
end
|
|
69
69
|
end
|
|
@@ -24,7 +24,7 @@ module Openapi3Parser
|
|
|
24
24
|
@path = ::File.absolute_path(path)
|
|
25
25
|
working_directory ||= resolve_working_directory
|
|
26
26
|
@working_directory = ::File.absolute_path(working_directory)
|
|
27
|
-
|
|
27
|
+
super()
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
# @see SourceInput#resolve_next
|
|
@@ -39,6 +39,7 @@ module Openapi3Parser
|
|
|
39
39
|
# @return [Boolean]
|
|
40
40
|
def ==(other)
|
|
41
41
|
return false unless other.instance_of?(self.class)
|
|
42
|
+
|
|
42
43
|
path == other.path &&
|
|
43
44
|
working_directory == other.working_directory
|
|
44
45
|
end
|
|
@@ -66,6 +67,7 @@ module Openapi3Parser
|
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
return path unless other_path
|
|
70
|
+
|
|
69
71
|
other_path ? relative_path(other_path, path) : path
|
|
70
72
|
end
|
|
71
73
|
|
|
@@ -26,7 +26,7 @@ module Openapi3Parser
|
|
|
26
26
|
@base_url = base_url
|
|
27
27
|
working_directory ||= resolve_working_directory
|
|
28
28
|
@working_directory = ::File.absolute_path(working_directory)
|
|
29
|
-
|
|
29
|
+
super()
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# @see SourceInput#resolve_next
|
|
@@ -44,6 +44,7 @@ module Openapi3Parser
|
|
|
44
44
|
# @return [Boolean]
|
|
45
45
|
def ==(other)
|
|
46
46
|
return false unless other.instance_of?(self.class)
|
|
47
|
+
|
|
47
48
|
raw_input == other.raw_input &&
|
|
48
49
|
base_url == other.base_url &&
|
|
49
50
|
working_directory == other.working_directory
|
|
@@ -72,6 +73,7 @@ module Openapi3Parser
|
|
|
72
73
|
|
|
73
74
|
def parse_contents
|
|
74
75
|
return raw_input if raw_input.respond_to?(:keys)
|
|
76
|
+
|
|
75
77
|
StringParser.call(
|
|
76
78
|
input_to_string(raw_input),
|
|
77
79
|
raw_input.respond_to?(:path) ? ::File.basename(raw_input.path) : nil
|
|
@@ -81,6 +83,7 @@ module Openapi3Parser
|
|
|
81
83
|
def input_to_string(input)
|
|
82
84
|
return input.read if input.respond_to?(:read)
|
|
83
85
|
return input.to_s if input.respond_to?(:to_s)
|
|
86
|
+
|
|
84
87
|
input
|
|
85
88
|
end
|
|
86
89
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "psych"
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
6
|
module Openapi3Parser
|
|
@@ -27,6 +27,7 @@ module Openapi3Parser
|
|
|
27
27
|
|
|
28
28
|
def json?
|
|
29
29
|
return false if filename && ::File.extname(filename) == ".yaml"
|
|
30
|
+
|
|
30
31
|
json_filename = filename && ::File.extname(filename) == ".json"
|
|
31
32
|
json_filename || input.strip[0] == "{"
|
|
32
33
|
end
|
|
@@ -36,7 +37,7 @@ module Openapi3Parser
|
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def parse_yaml
|
|
39
|
-
|
|
40
|
+
Psych.safe_load(input, permitted_classes: [Date, Time], aliases: true)
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
end
|
|
@@ -17,7 +17,7 @@ module Openapi3Parser
|
|
|
17
17
|
# @param [String, URI] request_url
|
|
18
18
|
def initialize(request_url)
|
|
19
19
|
@request_url = request_url.to_s
|
|
20
|
-
|
|
20
|
+
super()
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# @see SourceInput#resolve_next
|
|
@@ -32,6 +32,7 @@ module Openapi3Parser
|
|
|
32
32
|
# @return [Boolean]
|
|
33
33
|
def ==(other)
|
|
34
34
|
return false unless other.instance_of?(self.class)
|
|
35
|
+
|
|
35
36
|
[request_url, resolved_url].include?(other.request_url) ||
|
|
36
37
|
[request_url, resolved_url].include?(other.resolved_url)
|
|
37
38
|
end
|
|
@@ -89,6 +90,7 @@ module Openapi3Parser
|
|
|
89
90
|
|
|
90
91
|
def call
|
|
91
92
|
return to_uri.to_s if different_hosts?
|
|
93
|
+
|
|
92
94
|
relative_path
|
|
93
95
|
end
|
|
94
96
|
|
|
@@ -35,6 +35,7 @@ module Openapi3Parser
|
|
|
35
35
|
def for_type
|
|
36
36
|
return unless factory_class
|
|
37
37
|
return "(anonymous)" unless factory_class.name
|
|
38
|
+
|
|
38
39
|
factory_class.name.split("::").last
|
|
39
40
|
end
|
|
40
41
|
|
|
@@ -47,6 +48,7 @@ module Openapi3Parser
|
|
|
47
48
|
# @return [Boolean]
|
|
48
49
|
def ==(other)
|
|
49
50
|
return false unless other.instance_of?(self.class)
|
|
51
|
+
|
|
50
52
|
message == other.message &&
|
|
51
53
|
context == other.context &&
|
|
52
54
|
factory_class == other.factory_class
|
|
@@ -56,7 +56,7 @@ module Openapi3Parser
|
|
|
56
56
|
grouped.each_with_object({}) do |(_, items), memo|
|
|
57
57
|
items.each do |item|
|
|
58
58
|
key = item.location_summary(with_type: items.count > 1)
|
|
59
|
-
memo[key] = (
|
|
59
|
+
memo[key] = memo.fetch(key, []) + item.errors.map(&:to_s)
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
end
|
|
@@ -6,7 +6,7 @@ module Openapi3Parser
|
|
|
6
6
|
# defined for a Components node.
|
|
7
7
|
# As defined: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#components-object
|
|
8
8
|
class ComponentKeys
|
|
9
|
-
REGEX = /\A[a-zA-Z0-9
|
|
9
|
+
REGEX = /\A[a-zA-Z0-9.\-_]+\Z/.freeze
|
|
10
10
|
|
|
11
11
|
def self.call(input)
|
|
12
12
|
invalid = input.keys.reject { |key| REGEX.match(key) }
|
|
@@ -7,12 +7,12 @@ module Openapi3Parser
|
|
|
7
7
|
# https://html.spec.whatwg.org/#e-mail-state-(type=email)
|
|
8
8
|
REGEX = %r{
|
|
9
9
|
\A
|
|
10
|
-
[a-zA-Z0-9.!#$%&'
|
|
10
|
+
[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+
|
|
11
11
|
@
|
|
12
12
|
[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?
|
|
13
|
-
(
|
|
13
|
+
(?:.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*
|
|
14
14
|
\Z
|
|
15
|
-
}x
|
|
15
|
+
}x.freeze
|
|
16
16
|
|
|
17
17
|
def self.call(input)
|
|
18
18
|
message = %("#{input}" is not a valid email address)
|
|
@@ -8,8 +8,8 @@ module Openapi3Parser
|
|
|
8
8
|
using ArraySentence
|
|
9
9
|
private_class_method :new
|
|
10
10
|
|
|
11
|
-
def self.call(*args)
|
|
12
|
-
new.call(*args)
|
|
11
|
+
def self.call(*args, **kwargs)
|
|
12
|
+
new.call(*args, **kwargs)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def call(validatable,
|
|
@@ -78,12 +78,12 @@ module Openapi3Parser
|
|
|
78
78
|
|
|
79
79
|
def errors
|
|
80
80
|
@errors ||= begin
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
default = { required: [], exclusive: [] }
|
|
82
|
+
mutually_exclusive_fields
|
|
83
|
+
.each_with_object(default) do |exclusive, errors|
|
|
84
|
+
add_error(errors, exclusive)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
private
|