openapi3_parser 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +10 -1
- data/CHANGELOG.md +5 -0
- data/README.md +13 -0
- data/TODO.md +11 -8
- data/lib/openapi3_parser.rb +16 -28
- data/lib/openapi3_parser/context.rb +92 -34
- data/lib/openapi3_parser/context/location.rb +37 -0
- data/lib/openapi3_parser/context/pointer.rb +34 -0
- data/lib/openapi3_parser/document.rb +115 -15
- data/lib/openapi3_parser/document/reference_register.rb +50 -0
- data/lib/openapi3_parser/error.rb +27 -1
- data/lib/openapi3_parser/node/object.rb +26 -1
- data/lib/openapi3_parser/node_factories/array.rb +15 -14
- data/lib/openapi3_parser/node_factories/callback.rb +2 -1
- data/lib/openapi3_parser/node_factories/link.rb +1 -1
- data/lib/openapi3_parser/node_factories/map.rb +13 -11
- data/lib/openapi3_parser/node_factories/openapi.rb +2 -0
- data/lib/openapi3_parser/node_factories/path_item.rb +13 -18
- data/lib/openapi3_parser/node_factories/paths.rb +2 -1
- data/lib/openapi3_parser/node_factories/reference.rb +7 -9
- data/lib/openapi3_parser/node_factories/responses.rb +4 -2
- data/lib/openapi3_parser/node_factories/security_requirement.rb +2 -1
- data/lib/openapi3_parser/node_factory.rb +39 -30
- data/lib/openapi3_parser/node_factory/fields/reference.rb +44 -0
- data/lib/openapi3_parser/node_factory/map.rb +5 -5
- data/lib/openapi3_parser/node_factory/object.rb +6 -5
- data/lib/openapi3_parser/node_factory/object/node_builder.rb +12 -11
- data/lib/openapi3_parser/node_factory/object/validator.rb +25 -14
- data/lib/openapi3_parser/nodes/components.rb +9 -11
- data/lib/openapi3_parser/nodes/discriminator.rb +1 -1
- data/lib/openapi3_parser/nodes/encoding.rb +1 -1
- data/lib/openapi3_parser/nodes/link.rb +1 -1
- data/lib/openapi3_parser/nodes/media_type.rb +2 -2
- data/lib/openapi3_parser/nodes/oauth_flow.rb +1 -1
- data/lib/openapi3_parser/nodes/openapi.rb +3 -4
- data/lib/openapi3_parser/nodes/operation.rb +5 -8
- data/lib/openapi3_parser/nodes/parameter/parameter_like.rb +2 -2
- data/lib/openapi3_parser/nodes/path_item.rb +2 -3
- data/lib/openapi3_parser/nodes/request_body.rb +1 -1
- data/lib/openapi3_parser/nodes/response.rb +3 -3
- data/lib/openapi3_parser/nodes/schema.rb +6 -7
- data/lib/openapi3_parser/nodes/server.rb +1 -2
- data/lib/openapi3_parser/nodes/server_variable.rb +1 -1
- data/lib/openapi3_parser/source.rb +136 -0
- data/lib/openapi3_parser/source/reference.rb +68 -0
- data/lib/openapi3_parser/source/reference_resolver.rb +81 -0
- data/lib/openapi3_parser/source_input.rb +71 -0
- data/lib/openapi3_parser/source_input/file.rb +102 -0
- data/lib/openapi3_parser/source_input/raw.rb +90 -0
- data/lib/openapi3_parser/source_input/resolve_next.rb +62 -0
- data/lib/openapi3_parser/source_input/string_parser.rb +43 -0
- data/lib/openapi3_parser/source_input/url.rb +123 -0
- data/lib/openapi3_parser/validation/error.rb +36 -3
- data/lib/openapi3_parser/validation/error_collection.rb +57 -15
- data/lib/openapi3_parser/validators/reference.rb +40 -0
- data/lib/openapi3_parser/version.rb +1 -1
- data/openapi3_parser.gemspec +10 -5
- metadata +34 -20
- data/lib/openapi3_parser/fields/map.rb +0 -83
- data/lib/openapi3_parser/node.rb +0 -115
@@ -58,13 +58,12 @@ module Openapi3Parser
|
|
58
58
|
node_data["trace"]
|
59
59
|
end
|
60
60
|
|
61
|
-
# @return [Nodes::Array
|
61
|
+
# @return [Nodes::Array<Server>]
|
62
62
|
def servers
|
63
63
|
node_data["servers"]
|
64
64
|
end
|
65
65
|
|
66
|
-
# @return [Nodes::Array
|
67
|
-
# objects
|
66
|
+
# @return [Nodes::Array<Parameter>]
|
68
67
|
def parameters
|
69
68
|
node_data["parameters"]
|
70
69
|
end
|
@@ -13,17 +13,17 @@ module Openapi3Parser
|
|
13
13
|
node_data["description"]
|
14
14
|
end
|
15
15
|
|
16
|
-
# @return [Map
|
16
|
+
# @return [Map<String, Header>]
|
17
17
|
def headers
|
18
18
|
node_data["headers"]
|
19
19
|
end
|
20
20
|
|
21
|
-
# @return [Map
|
21
|
+
# @return [Map<String, MediaType>]
|
22
22
|
def content
|
23
23
|
node_data["content"]
|
24
24
|
end
|
25
25
|
|
26
|
-
# @return [Map
|
26
|
+
# @return [Map<String, Link>]
|
27
27
|
def links
|
28
28
|
node_data["links"]
|
29
29
|
end
|
@@ -79,13 +79,12 @@ module Openapi3Parser
|
|
79
79
|
node_data["minProperties"]
|
80
80
|
end
|
81
81
|
|
82
|
-
# @return [Nodes::Array
|
82
|
+
# @return [Nodes::Array<String>, nil]
|
83
83
|
def required
|
84
84
|
node_data["required"]
|
85
85
|
end
|
86
86
|
|
87
|
-
# @return [Nodes::Array
|
88
|
-
# nil
|
87
|
+
# @return [Nodes::Array<Object>, nil]
|
89
88
|
def enum
|
90
89
|
node_data["enum"]
|
91
90
|
end
|
@@ -95,17 +94,17 @@ module Openapi3Parser
|
|
95
94
|
node_data["type"]
|
96
95
|
end
|
97
96
|
|
98
|
-
# @return [Nodes::Array
|
97
|
+
# @return [Nodes::Array<Schema>, nil]
|
99
98
|
def all_of
|
100
99
|
node_data["allOf"]
|
101
100
|
end
|
102
101
|
|
103
|
-
# @return [Nodes::Array
|
102
|
+
# @return [Nodes::Array<Schema>, nil]
|
104
103
|
def one_of
|
105
104
|
node_data["oneOf"]
|
106
105
|
end
|
107
106
|
|
108
|
-
# @return [Nodes::Array
|
107
|
+
# @return [Nodes::Array<Schema>, nil]
|
109
108
|
def any_of
|
110
109
|
node_data["anyOf"]
|
111
110
|
end
|
@@ -120,7 +119,7 @@ module Openapi3Parser
|
|
120
119
|
node_data["items"]
|
121
120
|
end
|
122
121
|
|
123
|
-
# @return [Map
|
122
|
+
# @return [Map<String, Schema>]
|
124
123
|
def properties
|
125
124
|
node_data["properties"]
|
126
125
|
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openapi3_parser/error"
|
4
|
+
require "openapi3_parser/source/reference"
|
5
|
+
require "openapi3_parser/source/reference_resolver"
|
6
|
+
|
7
|
+
module Openapi3Parser
|
8
|
+
# Represents a source of data used to produce the OpenAPI document. Documents
|
9
|
+
# which do not have any references to external files will only have a single
|
10
|
+
# source
|
11
|
+
#
|
12
|
+
# @attr_reader [SourceInput] source_input
|
13
|
+
# The source input which provides the data
|
14
|
+
# @attr_reader [Document] document
|
15
|
+
# The document that this source is associated with
|
16
|
+
# @attr_reader [Document::ReferenceRegister] reference_register
|
17
|
+
# An object to track references for a document
|
18
|
+
# @attr_reader [Source, nil] parent
|
19
|
+
# Set to a Source if this source was created due to a reference within
|
20
|
+
# a different Source
|
21
|
+
class Source
|
22
|
+
attr_reader :source_input, :document, :reference_register, :parent
|
23
|
+
|
24
|
+
# @param [SourceInput] source_input
|
25
|
+
# @param [Document] document
|
26
|
+
# @param [Document::ReferenceRegister] reference_register
|
27
|
+
# @param [Source, nil] parent
|
28
|
+
def initialize(source_input, document, reference_register, parent = nil)
|
29
|
+
@source_input = source_input
|
30
|
+
@document = document
|
31
|
+
@reference_register = reference_register
|
32
|
+
@parent = parent
|
33
|
+
end
|
34
|
+
|
35
|
+
# The data from the source
|
36
|
+
def data
|
37
|
+
@data ||= normalize_data(source_input.contents)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @see SourceInput#available?
|
41
|
+
def available?
|
42
|
+
source_input.available?
|
43
|
+
end
|
44
|
+
|
45
|
+
# Whether this is the root source of a document
|
46
|
+
def root?
|
47
|
+
document.root_source == self
|
48
|
+
end
|
49
|
+
|
50
|
+
# Used to register a reference with the underlying document and return a
|
51
|
+
# reference resolver to access the object referenced
|
52
|
+
#
|
53
|
+
# @param [String] given_reference The reference as text
|
54
|
+
# @param [NodeFactory] factory Factory class for the expected
|
55
|
+
# eventual resource
|
56
|
+
# @param [Context] context The context of the object
|
57
|
+
# calling this reference
|
58
|
+
# @return [ReferenceResolver]
|
59
|
+
def register_reference(given_reference, factory, context)
|
60
|
+
reference = Reference.new(given_reference)
|
61
|
+
ReferenceResolver.new(
|
62
|
+
reference, factory, context
|
63
|
+
).tap do |resolver|
|
64
|
+
next if resolver.in_root_source?
|
65
|
+
reference_register.register(resolver.reference_factory)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Access/create the source object for a reference
|
70
|
+
#
|
71
|
+
# @param [Reference] reference
|
72
|
+
# @return [Source]
|
73
|
+
def resolve_source(reference)
|
74
|
+
if reference.only_fragment?
|
75
|
+
# I found the spec wasn't fully clear on expected behaviour if a source
|
76
|
+
# references a fragment that doesn't exist in it's current document
|
77
|
+
# and just the root source. I'm assuming to be consistent with URI a
|
78
|
+
# fragment only reference only references current JSON document. This
|
79
|
+
# could be incorrect though.
|
80
|
+
self
|
81
|
+
else
|
82
|
+
next_source_input = source_input.resolve_next(reference)
|
83
|
+
source = document.source_for_source_input(next_source_input)
|
84
|
+
source || self.class.new(next_source_input,
|
85
|
+
document,
|
86
|
+
reference_register,
|
87
|
+
self)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Access the data in a source at a particular pointer
|
92
|
+
#
|
93
|
+
# @param [Array] json_pointer An array of segments of a JSON pointer
|
94
|
+
# @return [Object]
|
95
|
+
def data_at_pointer(json_pointer)
|
96
|
+
return data if json_pointer.empty?
|
97
|
+
data.dig(*json_pointer) if data.respond_to?(:dig)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Whether the source has data at the particular pointer
|
101
|
+
def has_pointer?(json_pointer) # rubocop:disable Naming/PredicateName
|
102
|
+
!data_at_pointer(json_pointer).nil?
|
103
|
+
end
|
104
|
+
|
105
|
+
# @return [String]
|
106
|
+
def relative_to_root
|
107
|
+
return "" if root?
|
108
|
+
source_input.relative_to(document.root_source.source_input)
|
109
|
+
end
|
110
|
+
|
111
|
+
def ==(other)
|
112
|
+
source_input == other.source_input && document == other.document
|
113
|
+
end
|
114
|
+
|
115
|
+
# return [String]
|
116
|
+
def inspect
|
117
|
+
%{#{self.class.name}(input: #{source_input})}
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def normalize_data(input)
|
123
|
+
normalized = if input.respond_to?(:keys)
|
124
|
+
input.each_with_object({}) do |(key, value), memo|
|
125
|
+
memo[key.to_s.freeze] = normalize_data(value)
|
126
|
+
end
|
127
|
+
elsif input.respond_to?(:map)
|
128
|
+
input.map { |v| normalize_data(v) }
|
129
|
+
else
|
130
|
+
input
|
131
|
+
end
|
132
|
+
|
133
|
+
normalized.freeze
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openapi3_parser/source"
|
4
|
+
require "openapi3_parser/validators/reference"
|
5
|
+
|
6
|
+
module Openapi3Parser
|
7
|
+
class Source
|
8
|
+
# An object which represents a reference that can be indicated in a OpenAPI
|
9
|
+
# file. Given a string reference it can be used to answer key questions
|
10
|
+
# that aid in resolving the reference
|
11
|
+
#
|
12
|
+
# e.g.
|
13
|
+
# r = Openapi3Parser::Source::Reference.new("test.yaml#/path/to/item")
|
14
|
+
#
|
15
|
+
# r.only_fragment?
|
16
|
+
# => false
|
17
|
+
#
|
18
|
+
# r.rsource_uri
|
19
|
+
# => "test.yaml"
|
20
|
+
class Reference
|
21
|
+
# @param [String] reference reference from an OpenAPI file
|
22
|
+
def initialize(reference)
|
23
|
+
@given_reference = reference
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
given_reference.to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
def only_fragment?
|
31
|
+
resource_uri.to_s == ""
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [String, nil]
|
35
|
+
def fragment
|
36
|
+
uri.fragment
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [URI]
|
40
|
+
def resource_uri
|
41
|
+
uri_without_fragment
|
42
|
+
end
|
43
|
+
|
44
|
+
def absolute?
|
45
|
+
uri.absolute?
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [::Array] an array of strings of the components in the fragment
|
49
|
+
def json_pointer
|
50
|
+
@json_pointer ||= (fragment || "").split("/").drop(1).map do |field|
|
51
|
+
CGI.unescape(field.gsub("+", "%20"))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
attr_reader :given_reference
|
58
|
+
|
59
|
+
def uri
|
60
|
+
@uri = URI.parse(given_reference)
|
61
|
+
end
|
62
|
+
|
63
|
+
def uri_without_fragment
|
64
|
+
@uri_without_fragment ||= uri.dup.tap { |u| u.fragment = nil }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openapi3_parser/context"
|
4
|
+
require "openapi3_parser/source"
|
5
|
+
|
6
|
+
module Openapi3Parser
|
7
|
+
class Source
|
8
|
+
class ReferenceResolver
|
9
|
+
def initialize(reference, factory, context)
|
10
|
+
@reference = reference
|
11
|
+
@factory = factory
|
12
|
+
@context = context
|
13
|
+
end
|
14
|
+
|
15
|
+
def reference_factory
|
16
|
+
@reference_factory ||= begin
|
17
|
+
next_context = Context.reference_field(
|
18
|
+
context,
|
19
|
+
input: source.data_at_pointer(json_pointer),
|
20
|
+
source: source,
|
21
|
+
pointer_segments: json_pointer
|
22
|
+
)
|
23
|
+
build_factory(next_context)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def valid?
|
28
|
+
errors.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
def errors
|
32
|
+
@errors ||= Array(build_errors)
|
33
|
+
end
|
34
|
+
|
35
|
+
def node
|
36
|
+
reference_factory.node
|
37
|
+
end
|
38
|
+
|
39
|
+
def in_root_source?
|
40
|
+
source == context.document.root_source
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
attr_reader :reference, :factory, :context
|
46
|
+
|
47
|
+
def source
|
48
|
+
@source ||= context.source.resolve_source(reference)
|
49
|
+
end
|
50
|
+
|
51
|
+
def build_errors
|
52
|
+
return source_unavailabe_error unless source.available?
|
53
|
+
return pointer_missing_error unless source.has_pointer?(json_pointer)
|
54
|
+
resolution_error unless reference_factory.valid?
|
55
|
+
end
|
56
|
+
|
57
|
+
def source_unavailabe_error
|
58
|
+
# @todo include a location
|
59
|
+
"Source is unavailable"
|
60
|
+
end
|
61
|
+
|
62
|
+
def pointer_missing_error
|
63
|
+
# @todo include a location and a pointer
|
64
|
+
"Source does not have pointer"
|
65
|
+
end
|
66
|
+
|
67
|
+
def resolution_error
|
68
|
+
# @todo include exepected object
|
69
|
+
"Reference does not resolve to a valid object"
|
70
|
+
end
|
71
|
+
|
72
|
+
def json_pointer
|
73
|
+
reference.json_pointer
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_factory(context)
|
77
|
+
factory.is_a?(Class) ? factory.new(context) : factory.call(context)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openapi3_parser/error"
|
4
|
+
|
5
|
+
module Openapi3Parser
|
6
|
+
# An abstract class which is used to provide a foundation for classes that
|
7
|
+
# represent the different means of input an OpenAPI document can have. It is
|
8
|
+
# used to represent the underlying source of the data which is used as a
|
9
|
+
# source within an OpenAPI document.
|
10
|
+
#
|
11
|
+
# @see SourceInput::Raw SourceInput::Raw for an input that is done through
|
12
|
+
# data, such as a Hash.
|
13
|
+
#
|
14
|
+
# @see SourceInput::File SourceInput::File for an input that is done through
|
15
|
+
# the path to a file within the local file system.
|
16
|
+
#
|
17
|
+
# @see SourceInput::Url SourceInput::Url for an input that is done through]
|
18
|
+
# a URL to an OpenAPI Document
|
19
|
+
#
|
20
|
+
# @attr_reader [Error::InaccessibleInput, nil] access_error
|
21
|
+
# @attr_reader [Error::UnparsableInput, nil] parse_error
|
22
|
+
class SourceInput
|
23
|
+
attr_reader :access_error, :parse_error
|
24
|
+
|
25
|
+
# Indicates that the data within this input is suitable (i.e. can parse
|
26
|
+
# underlying JSON or YAML) for trying to use as part of a Document
|
27
|
+
def available?
|
28
|
+
access_error.nil? && parse_error.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
# For a given reference use the context of the current SourceInput to
|
32
|
+
# determine which file is required for the reference. This allows
|
33
|
+
# references to use relative file paths because we can combine them witt
|
34
|
+
# the current SourceInput location to determine the next one
|
35
|
+
def resolve_next(_reference); end
|
36
|
+
|
37
|
+
# Used to determine whether a different instance of SourceInput is
|
38
|
+
# the same file/data
|
39
|
+
def ==(_other); end
|
40
|
+
|
41
|
+
# The parsed data from the input
|
42
|
+
#
|
43
|
+
# @raise [Error::InaccessibleInput] In cases where the file does not exist
|
44
|
+
# @raise [Error::UnparsableInput] In cases where the data is not parsable
|
45
|
+
#
|
46
|
+
# @return Object
|
47
|
+
def contents
|
48
|
+
raise access_error if access_error
|
49
|
+
raise parse_error if parse_error
|
50
|
+
@contents
|
51
|
+
end
|
52
|
+
|
53
|
+
# The relative path, if possible, for this source_input compared to a
|
54
|
+
# different one. Defaults to empty string and should be specialised in
|
55
|
+
# subclasses
|
56
|
+
#
|
57
|
+
# @return [String]
|
58
|
+
def relative_to(_source_input)
|
59
|
+
""
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def initialize_contents
|
65
|
+
return if access_error
|
66
|
+
@contents = parse_contents
|
67
|
+
rescue ::StandardError => e
|
68
|
+
@parse_error = Error::UnparsableInput.new(e.message)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|