openapi3_parser 0.2.0 → 0.3.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.
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