openapi3_parser 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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