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
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openapi3_parser/error"
4
+
5
+ module Openapi3Parser
6
+ class Document
7
+ class ReferenceRegister
8
+ attr_reader :sources, :factories
9
+
10
+ def initialize
11
+ @sources = []
12
+ @factories = []
13
+ end
14
+
15
+ def register(factory)
16
+ error = "Can't register references when the register is frozen"
17
+ raise Error::ImmutableObject, error if frozen?
18
+ context = factory.context
19
+ add_source(context.source_location.source)
20
+ add_factory(factory)
21
+ end
22
+
23
+ def freeze
24
+ sources.freeze
25
+ factories.freeze
26
+ super
27
+ end
28
+
29
+ private
30
+
31
+ def add_source(source)
32
+ return if sources.include?(source)
33
+ sources << source
34
+ end
35
+
36
+ def add_factory(factory)
37
+ return if factory_registered?(factory)
38
+ factories << factory
39
+ end
40
+
41
+ def factory_registered?(factory)
42
+ source_location = factory.context.source_location
43
+ factories.any? do |f|
44
+ same_location = f.context.source_location == source_location
45
+ same_location && f.class == factory.class
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,5 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Openapi3Parser
4
- class Error < ::RuntimeError; end
4
+ # An abstract class for Exceptions produced by this Gem
5
+ class Error < ::RuntimeError
6
+ # Raised in cases where we have been provided a path or URL to a file and
7
+ # at runtime when we have tried to access that resource it is not available
8
+ # for whatever reason.
9
+ class InaccessibleInput < Error; end
10
+ # Raised in cases where we provided data that we expected to be parsable
11
+ # (such as a string of JSON data) but when we tried to parse it an error
12
+ # is raised
13
+ class UnparsableInput < Error; end
14
+ # Raised in cases where an object that is in an immutable state is modified
15
+ #
16
+ # Typically this would occur when a component that is frozen is modififed.
17
+ # Some components are mutable during the construction of a document and
18
+ # then frozen afterwards.
19
+ class ImmutableObject < Error; end
20
+ # Raised when a type that is not a whitelist of valid types is used
21
+ class InvalidType < Error; end
22
+ # Raised when we have to abort creating an object due to invalid data
23
+ class InvalidData < Error; end
24
+ # Used when there are fields that are missing from an object which prevents
25
+ # us from creating a node
26
+ class MissingFields < Error; end
27
+ # Used when there are extra fields that are not expected in the data for
28
+ # a node
29
+ class UnexpectedFields < Error; end
30
+ end
5
31
  end
@@ -12,14 +12,39 @@ module Openapi3Parser
12
12
  @node_context = context
13
13
  end
14
14
 
15
+ # Look up an attribute of the node by the name it has in the OpenAPI
16
+ # document.
17
+ #
18
+ # @example Look up by OpenAPI naming
19
+ # obj["externalDocs"]
20
+ #
21
+ # @example Look up by symbol
22
+ # obj[:servers]
23
+ #
24
+ # @example Look up an extension
25
+ # obj["x-myExtension"]
26
+ #
27
+ # @param [String, Symbol] value
28
+ #
29
+ # @return anything
15
30
  def [](value)
16
- node_data[value]
31
+ node_data[value.to_s]
17
32
  end
18
33
 
34
+ # Look up an extension provided for this object, doesn't need a prefix of
35
+ # "x-"
36
+ #
37
+ # @example Looking up an extension provided as "x-extra"
38
+ # obj.extension("extra")
39
+ #
40
+ # @param [String, Symbol] value
41
+ #
42
+ # @return [Hash, Array, Numeric, String, true, false, nil]
19
43
  def extension(value)
20
44
  node_data["x-#{value}"]
21
45
  end
22
46
 
47
+ # Iterate through the attributes of this object
23
48
  def each(&block)
24
49
  node_data.each(&block)
25
50
  end
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "openapi3_parser/context"
4
+ require "openapi3_parser/error"
3
5
  require "openapi3_parser/node_factory"
4
6
  require "openapi3_parser/nodes/array"
5
7
  require "openapi3_parser/validation/error"
8
+ require "openapi3_parser/validation/error_collection"
6
9
 
7
10
  module Openapi3Parser
8
11
  module NodeFactories
@@ -17,11 +20,11 @@ module Openapi3Parser
17
20
  value_factory: nil,
18
21
  validate: nil
19
22
  )
20
- super(context)
21
23
  @default = default
22
24
  @given_value_input_type = value_input_type
23
25
  @given_value_factory = value_factory
24
26
  @given_validate = validate
27
+ super(context)
25
28
  end
26
29
 
27
30
  private
@@ -32,7 +35,7 @@ module Openapi3Parser
32
35
  def process_input(input)
33
36
  input.each_with_index.map do |value, i|
34
37
  if value_factory?
35
- initialize_value_factory(context.next_namespace(i))
38
+ initialize_value_factory(Context.next_field(context, i))
36
39
  else
37
40
  value
38
41
  end
@@ -43,15 +46,13 @@ module Openapi3Parser
43
46
  given_validate&.call(input, context)
44
47
  end
45
48
 
46
- def validate_input(error_collection)
47
- super(error_collection)
48
- processed_input.each do |value|
49
- next unless value.respond_to?(:errors)
50
- error_collection.merge(value.errors)
49
+ def validate_input
50
+ value_errors = processed_input.flat_map do |value|
51
+ value.respond_to?(:errors) ? value.errors.to_a : []
51
52
  end
52
- error_collection.merge(
53
- validate_value_input_type(processed_input, context)
54
- )
53
+ errors = value_errors + validate_value_input_type(processed_input,
54
+ context)
55
+ Validation::ErrorCollection.combine(super, errors)
55
56
  end
56
57
 
57
58
  def build_node(input)
@@ -81,7 +82,7 @@ module Openapi3Parser
81
82
  error = error_for_value_input_type(value)
82
83
  next unless error
83
84
  memo << Validation::Error.new(
84
- context.next_namespace(i), error
85
+ error, Context.next_field(context, i), self.class
85
86
  )
86
87
  end
87
88
  end
@@ -90,9 +91,9 @@ module Openapi3Parser
90
91
  input.each_with_index do |value, i|
91
92
  error = error_for_value_input_type(value)
92
93
  next unless error
93
- next_context = context.next_namespace(i)
94
- raise Openapi3Parser::Error,
95
- "Invalid type for #{next_context.stringify_namespace}. "\
94
+ next_context = Context.next_field(context, i)
95
+ raise Openapi3Parser::Error::InvalidType,
96
+ "Invalid type for #{next_context.location_summary}. "\
96
97
  "#{error}"
97
98
  end
98
99
  end
@@ -14,7 +14,8 @@ module Openapi3Parser
14
14
  def process_input(input)
15
15
  input.each_with_object({}) do |(key, value), memo|
16
16
  memo[key] = value if extension?(key)
17
- memo[key] = NodeFactories::PathItem.new(context.next_namespace(key))
17
+ next_context = Context.next_field(context, key)
18
+ memo[key] = NodeFactories::PathItem.new(next_context)
18
19
  end
19
20
  end
20
21
 
@@ -12,7 +12,7 @@ module Openapi3Parser
12
12
 
13
13
  allow_extensions
14
14
 
15
- # @TODO The link object in OAS is pretty meaty and there's lot of scope
15
+ # @todo The link object in OAS is pretty meaty and there's lot of scope
16
16
  # for further work here to make use of it's funcationality
17
17
 
18
18
  field "operationRef", input_type: String
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "openapi3_parser/context"
4
+ require "openapi3_parser/error"
3
5
  require "openapi3_parser/node_factory/map"
4
6
  require "openapi3_parser/nodes/map"
5
7
  require "openapi3_parser/validation/error"
8
+ require "openapi3_parser/validation/error_collection"
6
9
 
7
10
  module Openapi3Parser
8
11
  module NodeFactories
@@ -16,11 +19,11 @@ module Openapi3Parser
16
19
  value_factory: nil,
17
20
  validate: nil
18
21
  )
19
- super(context)
20
22
  @given_key_input_type = key_input_type
21
23
  @given_value_input_type = value_input_type
22
24
  @given_value_factory = value_factory
23
25
  @given_validate = validate
26
+ super(context)
24
27
  end
25
28
 
26
29
  private
@@ -31,7 +34,8 @@ module Openapi3Parser
31
34
  def process_input(input)
32
35
  input.each_with_object({}) do |(key, value), memo|
33
36
  memo[key] = if value_factory?
34
- initialize_value_factory(context.next_namespace(key))
37
+ next_context = Context.next_field(context, key)
38
+ initialize_value_factory(next_context)
35
39
  else
36
40
  value
37
41
  end
@@ -53,11 +57,9 @@ module Openapi3Parser
53
57
  given_validate&.call(input, context)
54
58
  end
55
59
 
56
- def validate_input(error_collection)
57
- super(error_collection)
58
- error_collection.merge(
59
- validate_value_input_type(processed_input, context)
60
- )
60
+ def validate_input
61
+ errors = validate_value_input_type(processed_input, context)
62
+ Validation::ErrorCollection.combine(super, errors)
61
63
  end
62
64
 
63
65
  def build_node(input)
@@ -84,7 +86,7 @@ module Openapi3Parser
84
86
  error = error_for_value_input_type(value)
85
87
  next unless error
86
88
  memo << Validation::Error.new(
87
- context.next_namespace(key), error
89
+ error, Context.next_field(context, key), self.class
88
90
  )
89
91
  end
90
92
  end
@@ -93,9 +95,9 @@ module Openapi3Parser
93
95
  input.each do |key, value|
94
96
  error = error_for_value_input_type(value)
95
97
  next unless error
96
- next_context = context.next_namespace(key)
97
- raise Openapi3Parser::Error,
98
- "Invalid type for #{next_context.stringify_namespace}. "\
98
+ next_context = Context.next_field(context, key)
99
+ raise Openapi3Parser::Error::InvalidType,
100
+ "Invalid type for #{next_context.location_summary}. "\
99
101
  "#{error}"
100
102
  end
101
103
  end
@@ -17,6 +17,8 @@ module Openapi3Parser
17
17
  include NodeFactory::Object
18
18
 
19
19
  allow_extensions
20
+ disallow_default
21
+
20
22
  field "openapi", input_type: String, required: true
21
23
  field "info", factory: NodeFactories::Info, required: true
22
24
  field "servers", factory: :servers_factory
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "openapi3_parser/nodes/path_item"
4
+ require "openapi3_parser/node_factory/fields/reference"
4
5
  require "openapi3_parser/node_factory/object"
5
6
  require "openapi3_parser/node_factory/object/node_builder"
6
7
  require "openapi3_parser/node_factory/optional_reference"
@@ -15,7 +16,7 @@ module Openapi3Parser
15
16
  include NodeFactory::Object
16
17
 
17
18
  allow_extensions
18
- field "$ref", input_type: String
19
+ field "$ref", input_type: String, factory: :ref_factory
19
20
  field "summary", input_type: String
20
21
  field "description", input_type: String
21
22
  field "get", factory: :operation_factory
@@ -36,9 +37,17 @@ module Openapi3Parser
36
37
  private
37
38
 
38
39
  def build_object(data, context)
39
- merged_data = merge_reference(data, context)
40
- merged_data.delete("$ref")
41
- Nodes::PathItem.new(merged_data, context)
40
+ ref = data.delete("$ref")
41
+ return Nodes::PathItem.new(data, context) unless ref
42
+
43
+ merged_data = ref.node_data.merge(data) do |_, new, old|
44
+ new.nil? ? old : new
45
+ end
46
+ Nodes::PathItem.new(merged_data, ref.node_context)
47
+ end
48
+
49
+ def ref_factory(context)
50
+ Fields::Reference.new(context, self.class)
42
51
  end
43
52
 
44
53
  def operation_factory(context)
@@ -56,20 +65,6 @@ module Openapi3Parser
56
65
  factory = NodeFactory::OptionalReference.new(NodeFactories::Parameter)
57
66
  NodeFactories::Array.new(context, value_factory: factory)
58
67
  end
59
-
60
- def merge_reference(data, context)
61
- return data unless data["$ref"]
62
- factory = context.resolve_reference do |ref_context|
63
- self.class.new(ref_context)
64
- end
65
-
66
- # @TODO In this situation we're basically sacrificing the reference
67
- # context as we don't know how to merge them. Should develop a system
68
- # to have a dual context
69
- factory.node_data.merge(data) do |_, new, old|
70
- new.nil? ? old : new
71
- end
72
- end
73
68
  end
74
69
  end
75
70
  end
@@ -15,7 +15,8 @@ module Openapi3Parser
15
15
  def process_input(input)
16
16
  input.each_with_object({}) do |(key, value), memo|
17
17
  memo[key] = value if extension?(key)
18
- memo[key] = child_factory(context.next_namespace(key))
18
+ next_context = Context.next_field(context, key)
19
+ memo[key] = child_factory(next_context)
19
20
  end
20
21
  end
21
22
 
@@ -1,32 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "openapi3_parser/node_factory/object"
4
+ require "openapi3_parser/node_factory/fields/reference"
4
5
 
5
6
  module Openapi3Parser
6
7
  module NodeFactories
7
8
  class Reference
8
9
  include NodeFactory::Object
9
10
 
10
- field "$ref", input_type: String, required: true
11
+ field "$ref", input_type: String, required: true, factory: :ref_factory
11
12
 
12
13
  attr_reader :factory
13
14
 
14
15
  def initialize(context, factory)
15
- super(context)
16
16
  @factory = factory
17
+ super(context)
17
18
  end
18
19
 
19
20
  private
20
21
 
21
- def build_object(_, context)
22
- context.resolve_reference do |ref_context|
23
- resolve_factory(ref_context).node
24
- end
22
+ def build_node(input)
23
+ input["$ref"].node
25
24
  end
26
25
 
27
- def resolve_factory(ref_context)
28
- return factory.new(ref_context) if factory.is_a?(Class)
29
- factory.call(ref_context)
26
+ def ref_factory(context)
27
+ Fields::Reference.new(context, factory)
30
28
  end
31
29
  end
32
30
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "openapi3_parser/context"
4
+ require "openapi3_parser/node_factories/response"
3
5
  require "openapi3_parser/node_factory/map"
4
6
  require "openapi3_parser/node_factory/optional_reference"
5
7
  require "openapi3_parser/nodes/responses"
6
- require "openapi3_parser/node_factories/response"
7
8
 
8
9
  module Openapi3Parser
9
10
  module NodeFactories
@@ -15,7 +16,8 @@ module Openapi3Parser
15
16
  def process_input(input)
16
17
  input.each_with_object({}) do |(key, value), memo|
17
18
  memo[key] = value if extension?(key)
18
- memo[key] = child_factory(context.next_namespace(key))
19
+ next_context = Context.next_field(context, key)
20
+ memo[key] = child_factory(next_context)
19
21
  end
20
22
  end
21
23
 
@@ -13,7 +13,8 @@ module Openapi3Parser
13
13
 
14
14
  def process_input(input)
15
15
  input.keys.each_with_object({}) do |key, memo|
16
- memo[key] = NodeFactories::Array.new(context.next_namespace(key))
16
+ next_context = Context.next_field(context, key)
17
+ memo[key] = NodeFactories::Array.new(next_context)
17
18
  end
18
19
  end
19
20
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "openapi3_parser/context"
3
4
  require "openapi3_parser/error"
4
5
  require "openapi3_parser/validation/error"
5
6
  require "openapi3_parser/validation/error_collection"
@@ -19,18 +20,36 @@ module Openapi3Parser
19
20
  def expected_input_type
20
21
  @input_type
21
22
  end
23
+
24
+ def allow_default
25
+ @allow_default = true
26
+ end
27
+
28
+ def disallow_default
29
+ @allow_default = false
30
+ end
31
+
32
+ def allowed_default?
33
+ @allow_default.nil? || @allow_default
34
+ end
22
35
  end
23
36
 
24
37
  def self.included(base)
25
38
  base.extend(ClassMethods)
26
39
  end
27
40
 
41
+ def allowed_default?
42
+ self.class.allowed_default?
43
+ end
44
+
28
45
  EXTENSION_REGEX = /^x-(.*)/
29
46
 
30
47
  attr_reader :context
31
48
 
32
49
  def initialize(context)
33
50
  @context = context
51
+ input = nil_input? ? default : context.input
52
+ @processed_input = input.nil? ? nil : process_input(input)
34
53
  end
35
54
 
36
55
  def valid?
@@ -51,47 +70,44 @@ module Openapi3Parser
51
70
 
52
71
  private
53
72
 
73
+ attr_reader :processed_input
74
+
54
75
  def validate(_input, _context); end
55
76
 
56
- def validate_input(error_collection)
57
- add_validation_errors(
58
- validate(context.input, context),
59
- error_collection
60
- )
77
+ def validate_input
78
+ transform_errors(validate(context.input, context))
61
79
  end
62
80
 
63
- def add_validation_errors(errors, error_collection)
64
- errors = Array(errors)
65
- errors.each do |error|
66
- unless error.is_a?(Validation::Error)
67
- error = Validation::Error.new(context.namespace, error)
81
+ def transform_errors(errors)
82
+ error_objects = Array(errors).map do |error|
83
+ if !error.is_a?(Validation::Error)
84
+ error
85
+ else
86
+ Validation::Error.new(error, context, self.class)
68
87
  end
69
- error_collection.append(error)
70
88
  end
71
- error_collection
89
+ Validation::ErrorCollection.new(error_objects)
72
90
  end
73
91
 
74
92
  def build_errors
75
- error_collection = Validation::ErrorCollection.new
76
- return error_collection if nil_input?
93
+ return Validation::ErrorCollection.new if nil_input? && allowed_default?
77
94
  unless valid_type?
78
95
  error = Validation::Error.new(
79
- context.namespace, "Invalid type. #{validate_type}"
96
+ "Invalid type. #{validate_type}", context, self.class
80
97
  )
81
- return error_collection.tap { |ec| ec.append(error) }
98
+ return Validation::ErrorCollection.new([error])
82
99
  end
83
- validate_input(error_collection)
84
- error_collection
100
+ validate_input
85
101
  end
86
102
 
87
103
  def build_valid_node
88
- if nil_input?
104
+ if nil_input? && allowed_default?
89
105
  return default.nil? ? nil : build_node(processed_input)
90
106
  end
91
107
 
92
108
  unless valid_type?
93
- raise Openapi3Parser::Error,
94
- "Invalid type for #{context.stringify_namespace}. "\
109
+ raise Openapi3Parser::Error::InvalidType,
110
+ "Invalid type for #{context.location_summary}. "\
95
111
  "#{validate_type}"
96
112
  end
97
113
 
@@ -102,8 +118,8 @@ module Openapi3Parser
102
118
  def validate_before_build
103
119
  errors = Array(validate(context.input, context))
104
120
  return unless errors.any?
105
- raise Openapi3Parser::Error,
106
- "Invalid data for #{context.stringify_namespace}. "\
121
+ raise Openapi3Parser::Error::InvalidData,
122
+ "Invalid data for #{context.location_summary}. "\
107
123
  "#{errors.join(', ')}"
108
124
  end
109
125
 
@@ -116,13 +132,6 @@ module Openapi3Parser
116
132
  return "Expected #{self.class.expected_input_type}" unless valid_type
117
133
  end
118
134
 
119
- def processed_input
120
- @processed_input ||= begin
121
- input = nil_input? ? default : context.input
122
- process_input(input)
123
- end
124
- end
125
-
126
135
  def process_input(input)
127
136
  input
128
137
  end