openapi3_parser 0.4.0 → 0.5.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/CHANGELOG.md +8 -0
  4. data/README.md +1 -1
  5. data/TODO.md +4 -4
  6. data/lib/openapi3_parser/array_sentence.rb +12 -0
  7. data/lib/openapi3_parser/cautious_dig.rb +39 -0
  8. data/lib/openapi3_parser/context.rb +53 -1
  9. data/lib/openapi3_parser/context/location.rb +1 -0
  10. data/lib/openapi3_parser/context/pointer.rb +67 -5
  11. data/lib/openapi3_parser/document.rb +45 -4
  12. data/lib/openapi3_parser/error.rb +9 -0
  13. data/lib/openapi3_parser/node/array.rb +14 -4
  14. data/lib/openapi3_parser/node/map.rb +45 -3
  15. data/lib/openapi3_parser/node/object.rb +25 -5
  16. data/lib/openapi3_parser/node_factory.rb +0 -150
  17. data/lib/openapi3_parser/node_factory/array.rb +198 -0
  18. data/lib/openapi3_parser/node_factory/callback.rb +24 -0
  19. data/lib/openapi3_parser/{node_factories → node_factory}/components.rb +24 -25
  20. data/lib/openapi3_parser/{node_factories → node_factory}/contact.rb +5 -6
  21. data/lib/openapi3_parser/{node_factories → node_factory}/discriminator.rb +6 -7
  22. data/lib/openapi3_parser/{node_factories → node_factory}/encoding.rb +6 -8
  23. data/lib/openapi3_parser/{node_factories → node_factory}/example.rb +4 -5
  24. data/lib/openapi3_parser/{node_factories → node_factory}/external_documentation.rb +4 -5
  25. data/lib/openapi3_parser/node_factory/field.rb +129 -0
  26. data/lib/openapi3_parser/node_factory/fields/reference.rb +54 -18
  27. data/lib/openapi3_parser/{node_factories → node_factory}/header.rb +4 -5
  28. data/lib/openapi3_parser/{node_factories → node_factory}/info.rb +6 -7
  29. data/lib/openapi3_parser/{node_factories → node_factory}/license.rb +4 -5
  30. data/lib/openapi3_parser/{node_factories → node_factory}/link.rb +6 -8
  31. data/lib/openapi3_parser/node_factory/map.rb +206 -21
  32. data/lib/openapi3_parser/{node_factories → node_factory}/media_type.rb +17 -16
  33. data/lib/openapi3_parser/{node_factories → node_factory}/oauth_flow.rb +2 -4
  34. data/lib/openapi3_parser/{node_factories → node_factory}/oauth_flows.rb +4 -6
  35. data/lib/openapi3_parser/node_factory/object.rb +66 -63
  36. data/lib/openapi3_parser/node_factory/object_factory/dsl.rb +50 -0
  37. data/lib/openapi3_parser/node_factory/object_factory/field_config.rb +88 -0
  38. data/lib/openapi3_parser/node_factory/object_factory/node_builder.rb +96 -0
  39. data/lib/openapi3_parser/node_factory/object_factory/validator.rb +172 -0
  40. data/lib/openapi3_parser/node_factory/openapi.rb +65 -0
  41. data/lib/openapi3_parser/node_factory/operation.rb +87 -0
  42. data/lib/openapi3_parser/node_factory/optional_reference.rb +7 -3
  43. data/lib/openapi3_parser/{node_factories → node_factory}/parameter.rb +16 -22
  44. data/lib/openapi3_parser/node_factory/parameter_like.rb +42 -0
  45. data/lib/openapi3_parser/{node_factories → node_factory}/path_item.rb +27 -29
  46. data/lib/openapi3_parser/{node_factories → node_factory}/paths.rb +21 -27
  47. data/lib/openapi3_parser/node_factory/recursive_pointer.rb +17 -0
  48. data/lib/openapi3_parser/node_factory/reference.rb +48 -0
  49. data/lib/openapi3_parser/{node_factories → node_factory}/request_body.rb +15 -19
  50. data/lib/openapi3_parser/{node_factories → node_factory}/response.rb +18 -21
  51. data/lib/openapi3_parser/node_factory/responses.rb +51 -0
  52. data/lib/openapi3_parser/{node_factories → node_factory}/schema.rb +33 -33
  53. data/lib/openapi3_parser/node_factory/security_requirement.rb +21 -0
  54. data/lib/openapi3_parser/{node_factories → node_factory}/security_scheme.rb +4 -6
  55. data/lib/openapi3_parser/{node_factories → node_factory}/server.rb +6 -8
  56. data/lib/openapi3_parser/{node_factories → node_factory}/server_variable.rb +10 -12
  57. data/lib/openapi3_parser/{node_factories → node_factory}/tag.rb +4 -6
  58. data/lib/openapi3_parser/node_factory/type_checker.rb +103 -0
  59. data/lib/openapi3_parser/{node_factories → node_factory}/xml.rb +4 -5
  60. data/lib/openapi3_parser/source/reference_resolver.rb +3 -3
  61. data/lib/openapi3_parser/validation/error.rb +9 -0
  62. data/lib/openapi3_parser/validation/input_validator.rb +18 -0
  63. data/lib/openapi3_parser/validation/validatable.rb +44 -0
  64. data/lib/openapi3_parser/validators/mutually_exclusive_fields.rb +121 -0
  65. data/lib/openapi3_parser/validators/required_fields.rb +37 -0
  66. data/lib/openapi3_parser/validators/unexpected_fields.rb +52 -0
  67. data/lib/openapi3_parser/version.rb +1 -1
  68. metadata +48 -38
  69. data/lib/openapi3_parser/node_factories/array.rb +0 -114
  70. data/lib/openapi3_parser/node_factories/callback.rb +0 -27
  71. data/lib/openapi3_parser/node_factories/map.rb +0 -120
  72. data/lib/openapi3_parser/node_factories/openapi.rb +0 -62
  73. data/lib/openapi3_parser/node_factories/operation.rb +0 -84
  74. data/lib/openapi3_parser/node_factories/parameter/parameter_like.rb +0 -41
  75. data/lib/openapi3_parser/node_factories/reference.rb +0 -35
  76. data/lib/openapi3_parser/node_factories/responses.rb +0 -60
  77. data/lib/openapi3_parser/node_factories/security_requirement.rb +0 -26
  78. data/lib/openapi3_parser/node_factory/field_config.rb +0 -88
  79. data/lib/openapi3_parser/node_factory/object/node_builder.rb +0 -97
  80. data/lib/openapi3_parser/node_factory/object/validator.rb +0 -176
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 39b97559bdb240544c9f0f65b848c71edc28af1c
4
- data.tar.gz: b8b1339e8d116a202a16ada58ffb75cf403f608f
3
+ metadata.gz: 0e48a69a367595cb50a68fbfef79e0d9fc925c61
4
+ data.tar.gz: a53afad6f342b79d622201e9b87cd014be1eeac8
5
5
  SHA512:
6
- metadata.gz: 5a9772d8bfd2c67b3736f32aa25d937c08c1fc76f8142bc92685f3cc2f0202a7c74f313d6e836d1ddcf92b15b99084b94e056d53ed299b437eefd15f56387eff
7
- data.tar.gz: c47362a370307d40b1497e8b1c67e58ce267efaa611e0f0b49765ab3f2769635aba04f67f611250a1125116cadca09ac4d6d6372290253069192336c641169ba
6
+ metadata.gz: 2d772db41130444cbb1bfae66508ec1bde2ae21ba583da15b9cf3776d68dee5864f061e5e333a7cd2676361f9ac3bd6362efc4afaccaa0967b47c1fcbc00c781
7
+ data.tar.gz: 79df18c70a0e16b3d509c669a37be4b421b8b2504e36ff97b4144cbb982123c6d146c77a190eda794abb7ecd8282d41397006ba3284452ba65220e2952f6b6a4
data/.rubocop.yml CHANGED
@@ -2,6 +2,8 @@ Style/StringLiterals:
2
2
  EnforcedStyle: double_quotes
3
3
  Metrics/MethodLength:
4
4
  Max: 30
5
+ Metrics/AbcSize:
6
+ Max: 30
5
7
  Documentation:
6
8
  Enabled: false
7
9
  Metrics/BlockLength:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # Unreleased
2
+
3
+ - Support for recursive references - fixes: https://github.com/kevindew/openapi3_parser/issues/4
4
+ - `node_at` method on nodes and document to allow looking up nodes by string
5
+ paths
6
+ - Refactor of the node factory classes to use simpler inheritance rather than
7
+ the mixins in mixins approach.
8
+
1
9
  # 0.4.0
2
10
 
3
11
  - Determine the OpenAPI specification version and store it in document.
data/README.md CHANGED
@@ -31,7 +31,7 @@ You can install this gem into your bundler application by adding this line to
31
31
  your Gemfile:
32
32
 
33
33
  ```
34
- gem "openapi3_parser", "~> 0.3.0"
34
+ gem "openapi3_parser", "~> 0.5.0"
35
35
  ```
36
36
 
37
37
  and then running `$ bundle install`
data/TODO.md CHANGED
@@ -3,7 +3,7 @@
3
3
  These are the steps defined to reach 1.0. Assistance is very welcome.
4
4
 
5
5
  - [x] Handle mutually exclusive fields
6
- - [ ] Refactor the various NodeFactory modules to be a less confusing
6
+ - [x] Refactor the various NodeFactory modules to be a less confusing
7
7
  hierachical structure. Consider having factories subclass instead of use
8
8
  mixin
9
9
  - [x] Decouple Document class for the source file. Consider a source file class
@@ -18,16 +18,16 @@ These are the steps defined to reach 1.0. Assistance is very welcome.
18
18
  - [ ] Reach parity with OpenAPI specification for validation
19
19
  - [ ] Consider a lenient mode for a document to only have to comply with type
20
20
  based validation
21
- - [ ] Improve test coverage
21
+ - [x] Improve test coverage
22
22
  - [ ] Publish documentation of the interface through the structure
23
23
  - [x] Consider a resolved context class for representing context with a node
24
24
  that can handle scenarios where a node is represented by both a reference
25
25
  and resolved context
26
- - [ ] Create error classes for various scenarios
26
+ - [x] Create error classes for various scenarios
27
27
  - [ ] Associate/resolve operation id / operation references
28
28
  - [ ] Do something to model expressions
29
29
  - [x] Improve the modelling of namespace
30
- - [ ] Set up nicer string representations of key classes to help them be
30
+ - [x] Set up nicer string representations of key classes to help them be
31
31
  debugged
32
32
  - [x] Ensure Array and Map nodes return empty ones by default rather than nil
33
33
  - [ ] Make JSON pointer public access to be consistent accepting string, array
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openapi3Parser
4
+ module ArraySentence
5
+ refine ::Array do
6
+ def sentence_join
7
+ return join if count < 2
8
+ self[0..-2].join(", ") + " and " + self[-1]
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openapi3Parser
4
+ class CautiousDig
5
+ private_class_method :new
6
+
7
+ def self.call(*args)
8
+ new.call(*args)
9
+ end
10
+
11
+ def call(collection, *segments)
12
+ segments.inject(collection) do |next_depth, segment|
13
+ break unless next_depth
14
+
15
+ if next_depth.respond_to?(:keys)
16
+ hash_like(next_depth, segment)
17
+ elsif next_depth.respond_to?(:[])
18
+ array_like(next_depth, segment)
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def hash_like(item, segment)
26
+ key = item.keys.find { |k| segment == k || segment.to_s == k.to_s }
27
+ item[key]
28
+ end
29
+
30
+ def array_like(item, segment)
31
+ index = if segment.is_a?(String) && segment =~ /\A\d+\z/
32
+ segment.to_i
33
+ else
34
+ segment
35
+ end
36
+ index.is_a?(Integer) ? item[index] : nil
37
+ end
38
+ end
39
+ end
@@ -38,6 +38,18 @@ module Openapi3Parser
38
38
  referenced_by: pc.referenced_by)
39
39
  end
40
40
 
41
+ # Convert a context into one that knows that is a reference
42
+ #
43
+ # @param [Context] current_context
44
+ # @return [Context]
45
+ def self.as_reference(current_context)
46
+ new(current_context.input,
47
+ document_location: current_context.document_location,
48
+ source_location: current_context.source_location,
49
+ referenced_by: current_context.referenced_by,
50
+ is_reference: true)
51
+ end
52
+
41
53
  # Creates the context for a field that is referenced by a context.
42
54
  # In this scenario the context of the document is the same but we are in
43
55
  # a different part of the source file, or even a different source file
@@ -63,14 +75,25 @@ module Openapi3Parser
63
75
  # @param [Context::Location] document_location
64
76
  # @param [Context::Location, nil] source_location
65
77
  # @param [Context, nil] referenced_by
78
+ # @param [Boolean] is_reference
66
79
  def initialize(input,
67
80
  document_location:,
68
81
  source_location: nil,
69
- referenced_by: nil)
82
+ referenced_by: nil,
83
+ is_reference: false)
70
84
  @input = input
71
85
  @document_location = document_location
72
86
  @source_location = source_location || document_location
73
87
  @referenced_by = referenced_by
88
+ @is_reference = is_reference
89
+ end
90
+
91
+ # @return [Boolean]
92
+ def ==(other)
93
+ input == other.input &&
94
+ document_location == other.document_location &&
95
+ source_location == other.source_location &&
96
+ referenced_by == other.referenced_by
74
97
  end
75
98
 
76
99
  # @return [Document]
@@ -93,6 +116,11 @@ module Openapi3Parser
93
116
  document_location.pointer.segments
94
117
  end
95
118
 
119
+ # @return [Boolean]
120
+ def reference?
121
+ @is_reference
122
+ end
123
+
96
124
  def inspect
97
125
  %{#{self.class.name}(document_location: #{document_location}, } +
98
126
  %{source_location: #{source_location}), referenced_by: } +
@@ -108,5 +136,29 @@ module Openapi3Parser
108
136
  def to_s
109
137
  location_summary
110
138
  end
139
+
140
+ def resolved_input
141
+ # The resolved input for a reference is at the segment before the
142
+ # reference
143
+ pointer = if reference?
144
+ document_location.pointer.segments[0...-1]
145
+ else
146
+ document_location.pointer
147
+ end
148
+
149
+ document.resolved_input_at(pointer)
150
+ end
151
+
152
+ def node
153
+ # The created node for a reference is at the segment before the
154
+ # reference
155
+ pointer = if reference?
156
+ document_location.pointer.segments[0...-1]
157
+ else
158
+ document_location.pointer
159
+ end
160
+
161
+ document.node_at(pointer)
162
+ end
111
163
  end
112
164
  end
@@ -22,6 +22,7 @@ module Openapi3Parser
22
22
  end
23
23
 
24
24
  def ==(other)
25
+ return false unless other.instance_of?(self.class)
25
26
  source == other.source && pointer == other.pointer
26
27
  end
27
28
 
@@ -7,11 +7,28 @@ module Openapi3Parser
7
7
  # A class to decorate the array of fields that make up a pointer and
8
8
  # provide common means to convert it into different representations.
9
9
  class Pointer
10
- attr_reader :segments
10
+ def self.from_fragment(fragment)
11
+ fragment = fragment[1..-1] if fragment.start_with?("#")
12
+ absolute = fragment[0] == "/"
13
+ segments = fragment.split("/").map do |part|
14
+ next if part == ""
15
+ unescaped = CGI.unescape(part.gsub("%20", "+"))
16
+ unescaped =~ /\A\d+\z/ ? unescaped.to_i : unescaped
17
+ end
18
+ new(segments.compact, absolute)
19
+ end
20
+
21
+ def self.merge_pointers(base_pointer, new_pointer)
22
+ MergePointers.call(base_pointer, new_pointer)
23
+ end
24
+
25
+ attr_reader :segments, :absolute
11
26
 
12
27
  # @param [::Array] segments
13
- def initialize(segments)
28
+ # @param [Boolean] absolute
29
+ def initialize(segments, absolute = true)
14
30
  @segments = segments.freeze
31
+ @absolute = absolute
15
32
  end
16
33
 
17
34
  def ==(other)
@@ -19,9 +36,9 @@ module Openapi3Parser
19
36
  end
20
37
 
21
38
  def fragment
22
- segments.map { |s| CGI.escape(s.to_s).gsub("+", "%20") }
23
- .join("/")
24
- .prepend("#/")
39
+ fragment = segments.map { |s| CGI.escape(s.to_s).gsub("+", "%20") }
40
+ .join("/")
41
+ "#" + (absolute ? fragment.prepend("/") : fragment)
25
42
  end
26
43
 
27
44
  def to_s
@@ -31,6 +48,51 @@ module Openapi3Parser
31
48
  def inspect
32
49
  %{#{self.class.name}(segments: #{segments}, fragment: "#{fragment}")}
33
50
  end
51
+
52
+ class MergePointers
53
+ private_class_method :new
54
+
55
+ def self.call(*args)
56
+ new(*args).call
57
+ end
58
+
59
+ def initialize(base_pointer, new_pointer)
60
+ @base_pointer = create_pointer(base_pointer)
61
+ @new_pointer = create_pointer(new_pointer)
62
+ end
63
+
64
+ def call
65
+ return base_pointer if new_pointer.nil?
66
+ return new_pointer if base_pointer.nil? || new_pointer.absolute
67
+
68
+ merge_pointers(base_pointer, new_pointer)
69
+ end
70
+
71
+ private
72
+
73
+ attr_reader :base_pointer, :new_pointer
74
+
75
+ def create_pointer(pointer_like)
76
+ case pointer_like
77
+ when Pointer then pointer_like
78
+ when ::Array then Pointer.new(pointer_like, false)
79
+ when ::String then Pointer.from_fragment(pointer_like)
80
+ when nil then nil
81
+ else raise Openapi3Parser::Error, "Unexpected type for pointer"
82
+ end
83
+ end
84
+
85
+ def merge_pointers(pointer_a, pointer_b)
86
+ fragment_a = pointer_a.fragment.gsub(%r{\A#?/?}, "")
87
+ fragment_b = pointer_b.fragment.gsub(%r{\A#?/?}, "")
88
+
89
+ joined = File.expand_path("/#{fragment_a}/#{fragment_b}", "/")
90
+
91
+ joined = joined[1..-1] unless pointer_a.absolute
92
+
93
+ Pointer.from_fragment("##{joined}")
94
+ end
95
+ end
34
96
  end
35
97
  end
36
98
  end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "openapi3_parser/cautious_dig"
3
4
  require "openapi3_parser/context"
5
+ require "openapi3_parser/context/pointer"
4
6
  require "openapi3_parser/document/reference_register"
5
7
  require "openapi3_parser/error"
6
- require "openapi3_parser/node_factories/openapi"
8
+ require "openapi3_parser/node_factory/openapi"
7
9
  require "openapi3_parser/source"
8
10
  require "openapi3_parser/validation/error_collection"
9
11
 
@@ -79,9 +81,11 @@ module Openapi3Parser
79
81
  # @return Object
80
82
  # @!method each
81
83
  # Iterate through the attributes of the root object
82
- # @see Node::Object#each
84
+ # @!method keys
85
+ # Access keys of the root object
83
86
  def_delegators :root, :openapi, :info, :servers, :paths, :components,
84
- :security, :tags, :external_docs, :extension, :[], :each
87
+ :security, :tags, :external_docs, :extension, :[], :each,
88
+ :keys
85
89
 
86
90
  # @param [SourceInput] source_input
87
91
  def initialize(source_input)
@@ -132,10 +136,47 @@ module Openapi3Parser
132
136
  sources.find { |source| source.source_input == source_input }
133
137
  end
134
138
 
139
+ # Look up the resolved input for an address in the OpenAPI document,
140
+ # resolved_input refers to the input with references resolevd and all
141
+ # optional fields existing
142
+ #
143
+ # @param [Context::Pointer, String, Array] pointer
144
+ # @param [Context::Pointer, String, Array, nil] relative_to
145
+ # @return anything
146
+ def resolved_input_at(pointer, relative_to = nil)
147
+ look_up_pointer(pointer, relative_to, factory.resolved_input)
148
+ end
149
+
150
+ # Look up a node at a particular location in the OpenAPI docuemnt
151
+ #
152
+ # Examples:
153
+ #
154
+ # document.node_at("#/components/schemas")
155
+ # document.node_at(%w[components schemas])
156
+ #
157
+ # @param [Context::Pointer, String, Array] pointer
158
+ # @param [Context::Pointer, String, Array, nil] relative_to
159
+ # @return anything
160
+ def node_at(pointer, relative_to = nil)
161
+ look_up_pointer(pointer, relative_to, root)
162
+ end
163
+
164
+ # @return [String]
165
+ def inspect
166
+ %{#{self.class.name}(openapi_version: #{openapi_version}, } +
167
+ %{root_source: #{root_source.inspect})}
168
+ end
169
+
135
170
  private
136
171
 
137
172
  attr_reader :reference_register, :built, :build_in_progress
138
173
 
174
+ def look_up_pointer(pointer, relative_pointer, subject)
175
+ merged_pointer = Context::Pointer.merge_pointers(relative_pointer,
176
+ pointer)
177
+ CautiousDig.call(subject, *merged_pointer.segments)
178
+ end
179
+
139
180
  def add_warning(text)
140
181
  @warnings << text
141
182
  end
@@ -144,7 +185,7 @@ module Openapi3Parser
144
185
  return if build_in_progress || built
145
186
  @build_in_progress = true
146
187
  context = Context.root(root_source.data, root_source)
147
- @factory = NodeFactories::Openapi.new(context)
188
+ @factory = NodeFactory::Openapi.new(context)
148
189
  reference_register.freeze
149
190
  @warnings.freeze
150
191
  @build_in_progress = false
@@ -27,5 +27,14 @@ module Openapi3Parser
27
27
  # Used when there are extra fields that are not expected in the data for
28
28
  # a node
29
29
  class UnexpectedFields < Error; end
30
+ # Used when a method we expect to be able to call (through symbol or proc)
31
+ # is not callable
32
+ class NotCallable < Error; end
33
+ # Raised when we in a recursive data structure and can't perform an
34
+ # operation
35
+ class InRecursiveStructure < Error; end
36
+ # Used when we're trying to validate that a type is something that is not
37
+ # validatable, most likely a sign that we're in a bug
38
+ class UnvalidatableType < Error; end
30
39
  end
31
40
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  require "openapi3_parser/node/map"
4
6
 
5
7
  module Openapi3Parser
@@ -11,8 +13,10 @@ module Openapi3Parser
11
13
  # The contents of the data will be dependent on where this document is in
12
14
  # the document hierachy.
13
15
  class Array
16
+ extend Forwardable
14
17
  include Enumerable
15
18
 
19
+ def_delegators :node_data, :each, :[], :empty?
16
20
  attr_reader :node_data, :node_context
17
21
 
18
22
  # @param [::Array] data data used to populate this node
@@ -22,12 +26,18 @@ module Openapi3Parser
22
26
  @node_context = context
23
27
  end
24
28
 
25
- def [](value)
26
- node_data[value]
29
+ # Used to access a node relative to this node
30
+ # @param [Context::Pointer, ::Array, ::String] pointer_like
31
+ # @return anything
32
+ def node_at(pointer_like)
33
+ current_pointer = node_context.document_location.pointer
34
+ node_context.document.node_at(pointer_like, current_pointer)
27
35
  end
28
36
 
29
- def each(&block)
30
- node_data.each(&block)
37
+ # @return [String]
38
+ def inspect
39
+ fragment = node_context.document_location.pointer.fragment
40
+ %{#{self.class.name}(#{fragment})}
31
41
  end
32
42
  end
33
43
  end