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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +10 -1
  3. data/CHANGELOG.md +5 -0
  4. data/README.md +13 -0
  5. data/TODO.md +11 -8
  6. data/lib/openapi3_parser.rb +16 -28
  7. data/lib/openapi3_parser/context.rb +92 -34
  8. data/lib/openapi3_parser/context/location.rb +37 -0
  9. data/lib/openapi3_parser/context/pointer.rb +34 -0
  10. data/lib/openapi3_parser/document.rb +115 -15
  11. data/lib/openapi3_parser/document/reference_register.rb +50 -0
  12. data/lib/openapi3_parser/error.rb +27 -1
  13. data/lib/openapi3_parser/node/object.rb +26 -1
  14. data/lib/openapi3_parser/node_factories/array.rb +15 -14
  15. data/lib/openapi3_parser/node_factories/callback.rb +2 -1
  16. data/lib/openapi3_parser/node_factories/link.rb +1 -1
  17. data/lib/openapi3_parser/node_factories/map.rb +13 -11
  18. data/lib/openapi3_parser/node_factories/openapi.rb +2 -0
  19. data/lib/openapi3_parser/node_factories/path_item.rb +13 -18
  20. data/lib/openapi3_parser/node_factories/paths.rb +2 -1
  21. data/lib/openapi3_parser/node_factories/reference.rb +7 -9
  22. data/lib/openapi3_parser/node_factories/responses.rb +4 -2
  23. data/lib/openapi3_parser/node_factories/security_requirement.rb +2 -1
  24. data/lib/openapi3_parser/node_factory.rb +39 -30
  25. data/lib/openapi3_parser/node_factory/fields/reference.rb +44 -0
  26. data/lib/openapi3_parser/node_factory/map.rb +5 -5
  27. data/lib/openapi3_parser/node_factory/object.rb +6 -5
  28. data/lib/openapi3_parser/node_factory/object/node_builder.rb +12 -11
  29. data/lib/openapi3_parser/node_factory/object/validator.rb +25 -14
  30. data/lib/openapi3_parser/nodes/components.rb +9 -11
  31. data/lib/openapi3_parser/nodes/discriminator.rb +1 -1
  32. data/lib/openapi3_parser/nodes/encoding.rb +1 -1
  33. data/lib/openapi3_parser/nodes/link.rb +1 -1
  34. data/lib/openapi3_parser/nodes/media_type.rb +2 -2
  35. data/lib/openapi3_parser/nodes/oauth_flow.rb +1 -1
  36. data/lib/openapi3_parser/nodes/openapi.rb +3 -4
  37. data/lib/openapi3_parser/nodes/operation.rb +5 -8
  38. data/lib/openapi3_parser/nodes/parameter/parameter_like.rb +2 -2
  39. data/lib/openapi3_parser/nodes/path_item.rb +2 -3
  40. data/lib/openapi3_parser/nodes/request_body.rb +1 -1
  41. data/lib/openapi3_parser/nodes/response.rb +3 -3
  42. data/lib/openapi3_parser/nodes/schema.rb +6 -7
  43. data/lib/openapi3_parser/nodes/server.rb +1 -2
  44. data/lib/openapi3_parser/nodes/server_variable.rb +1 -1
  45. data/lib/openapi3_parser/source.rb +136 -0
  46. data/lib/openapi3_parser/source/reference.rb +68 -0
  47. data/lib/openapi3_parser/source/reference_resolver.rb +81 -0
  48. data/lib/openapi3_parser/source_input.rb +71 -0
  49. data/lib/openapi3_parser/source_input/file.rb +102 -0
  50. data/lib/openapi3_parser/source_input/raw.rb +90 -0
  51. data/lib/openapi3_parser/source_input/resolve_next.rb +62 -0
  52. data/lib/openapi3_parser/source_input/string_parser.rb +43 -0
  53. data/lib/openapi3_parser/source_input/url.rb +123 -0
  54. data/lib/openapi3_parser/validation/error.rb +36 -3
  55. data/lib/openapi3_parser/validation/error_collection.rb +57 -15
  56. data/lib/openapi3_parser/validators/reference.rb +40 -0
  57. data/lib/openapi3_parser/version.rb +1 -1
  58. data/openapi3_parser.gemspec +10 -5
  59. metadata +34 -20
  60. data/lib/openapi3_parser/fields/map.rb +0 -83
  61. 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] a collection of {Server}[./Server.html] objects
61
+ # @return [Nodes::Array<Server>]
62
62
  def servers
63
63
  node_data["servers"]
64
64
  end
65
65
 
66
- # @return [Nodes::Array] a collection of {Parameter}[./Parameter.html]
67
- # objects
66
+ # @return [Nodes::Array<Parameter>]
68
67
  def parameters
69
68
  node_data["parameters"]
70
69
  end
@@ -13,7 +13,7 @@ module Openapi3Parser
13
13
  node_data["description"]
14
14
  end
15
15
 
16
- # @return [Map] a map of String: {MediaType}[./MediaType.html] objects
16
+ # @return [Map<String, MediaType>]
17
17
  def content
18
18
  node_data["content"]
19
19
  end
@@ -13,17 +13,17 @@ module Openapi3Parser
13
13
  node_data["description"]
14
14
  end
15
15
 
16
- # @return [Map] a map of String: {Header}[./Header.html] objects
16
+ # @return [Map<String, Header>]
17
17
  def headers
18
18
  node_data["headers"]
19
19
  end
20
20
 
21
- # @return [Map] a map of String: {MediaType}[./MediaType.html] objects
21
+ # @return [Map<String, MediaType>]
22
22
  def content
23
23
  node_data["content"]
24
24
  end
25
25
 
26
- # @return [Map] a map of String: {Link}[./Link.html] objects
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, nil] a collection of String objects or nil
82
+ # @return [Nodes::Array<String>, nil]
83
83
  def required
84
84
  node_data["required"]
85
85
  end
86
86
 
87
- # @return [Nodes::Array, nil] a collection of objects of no fixed type or
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, nil] a collection of Schema objects or nil
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, nil] a collection of Schema objects or nil
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, nil] a collection of Schema objects or nil
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] a collection of Schema objects
122
+ # @return [Map<String, Schema>]
124
123
  def properties
125
124
  node_data["properties"]
126
125
  end
@@ -18,8 +18,7 @@ module Openapi3Parser
18
18
  node_data["description"]
19
19
  end
20
20
 
21
- # @return [Map] a map of String: {ServerVariable}[./ServerVariable.html]
22
- # objects
21
+ # @return [Map<String, ServerVariable>]
23
22
  def variables
24
23
  node_data["variables"]
25
24
  end
@@ -8,7 +8,7 @@ module Openapi3Parser
8
8
  class ServerVariable
9
9
  include Node::Object
10
10
 
11
- # @return [Nodes::Array, nil] a collection of String objects or nil
11
+ # @return [Nodes::Array<String>, nil]
12
12
  def enum
13
13
  node_data["enum"]
14
14
  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