openapi3_parser 0.6.1 → 0.9.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 (71) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +23 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +18 -3
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +41 -0
  7. data/README.md +7 -4
  8. data/TODO.md +1 -1
  9. data/lib/openapi3_parser.rb +2 -3
  10. data/lib/openapi3_parser/array_sentence.rb +2 -1
  11. data/lib/openapi3_parser/document.rb +3 -2
  12. data/lib/openapi3_parser/error.rb +11 -1
  13. data/lib/openapi3_parser/node/array.rb +9 -1
  14. data/lib/openapi3_parser/node/context.rb +34 -4
  15. data/lib/openapi3_parser/node/map.rb +17 -1
  16. data/lib/openapi3_parser/node/object.rb +17 -0
  17. data/lib/openapi3_parser/node/operation.rb +8 -0
  18. data/lib/openapi3_parser/node/path_item.rb +8 -0
  19. data/lib/openapi3_parser/node/placeholder.rb +16 -12
  20. data/lib/openapi3_parser/node/schema.rb +50 -3
  21. data/lib/openapi3_parser/node_factory.rb +1 -1
  22. data/lib/openapi3_parser/node_factory/array.rb +34 -26
  23. data/lib/openapi3_parser/node_factory/context.rb +9 -2
  24. data/lib/openapi3_parser/node_factory/discriminator.rb +2 -0
  25. data/lib/openapi3_parser/node_factory/field.rb +2 -0
  26. data/lib/openapi3_parser/node_factory/fields/reference.rb +1 -0
  27. data/lib/openapi3_parser/node_factory/map.rb +12 -10
  28. data/lib/openapi3_parser/node_factory/media_type.rb +1 -0
  29. data/lib/openapi3_parser/node_factory/object.rb +4 -1
  30. data/lib/openapi3_parser/node_factory/object_factory/dsl.rb +7 -5
  31. data/lib/openapi3_parser/node_factory/object_factory/field_config.rb +9 -7
  32. data/lib/openapi3_parser/node_factory/object_factory/node_builder.rb +12 -13
  33. data/lib/openapi3_parser/node_factory/object_factory/validator.rb +25 -21
  34. data/lib/openapi3_parser/node_factory/openapi.rb +2 -0
  35. data/lib/openapi3_parser/node_factory/operation.rb +9 -0
  36. data/lib/openapi3_parser/node_factory/parameter.rb +2 -0
  37. data/lib/openapi3_parser/node_factory/parameter_like.rb +2 -1
  38. data/lib/openapi3_parser/node_factory/path_item.rb +27 -5
  39. data/lib/openapi3_parser/node_factory/paths.rb +1 -1
  40. data/lib/openapi3_parser/node_factory/reference.rb +9 -1
  41. data/lib/openapi3_parser/node_factory/request_body.rb +2 -4
  42. data/lib/openapi3_parser/node_factory/response.rb +1 -0
  43. data/lib/openapi3_parser/node_factory/responses.rb +1 -1
  44. data/lib/openapi3_parser/node_factory/schema.rb +5 -2
  45. data/lib/openapi3_parser/node_factory/server_variable.rb +1 -0
  46. data/lib/openapi3_parser/node_factory/type_checker.rb +6 -0
  47. data/lib/openapi3_parser/source.rb +2 -0
  48. data/lib/openapi3_parser/source/location.rb +6 -0
  49. data/lib/openapi3_parser/source/pointer.rb +9 -4
  50. data/lib/openapi3_parser/source_input.rb +9 -9
  51. data/lib/openapi3_parser/source_input/file.rb +3 -1
  52. data/lib/openapi3_parser/source_input/raw.rb +4 -1
  53. data/lib/openapi3_parser/source_input/resolve_next.rb +1 -0
  54. data/lib/openapi3_parser/source_input/string_parser.rb +3 -2
  55. data/lib/openapi3_parser/source_input/url.rb +3 -1
  56. data/lib/openapi3_parser/validation/error.rb +2 -0
  57. data/lib/openapi3_parser/validation/error_collection.rb +1 -1
  58. data/lib/openapi3_parser/validation/input_validator.rb +1 -1
  59. data/lib/openapi3_parser/validators/component_keys.rb +1 -1
  60. data/lib/openapi3_parser/validators/duplicate_parameters.rb +1 -0
  61. data/lib/openapi3_parser/validators/email.rb +3 -3
  62. data/lib/openapi3_parser/validators/media_type.rb +1 -1
  63. data/lib/openapi3_parser/validators/mutually_exclusive_fields.rb +8 -8
  64. data/lib/openapi3_parser/validators/reference.rb +2 -0
  65. data/lib/openapi3_parser/validators/required_fields.rb +2 -2
  66. data/lib/openapi3_parser/validators/unexpected_fields.rb +3 -2
  67. data/lib/openapi3_parser/validators/url.rb +2 -7
  68. data/lib/openapi3_parser/version.rb +1 -1
  69. data/openapi3_parser.gemspec +12 -8
  70. metadata +81 -25
  71. data/.travis.yml +0 -11
@@ -39,7 +39,7 @@ module Openapi3Parser
39
39
  field "default"
40
40
 
41
41
  field "nullable", input_type: :boolean, default: false
42
- field "discriminator", factory: :disciminator_factory
42
+ field "discriminator", factory: :discriminator_factory
43
43
  field "readOnly", input_type: :boolean, default: false
44
44
  field "writeOnly", input_type: :boolean, default: false
45
45
  field "xml", factory: :xml_factory
@@ -61,6 +61,7 @@ module Openapi3Parser
61
61
  def read_only_or_write_only(validatable)
62
62
  input = validatable.input
63
63
  return if [input["readOnly"], input["writeOnly"]].uniq != [true]
64
+
64
65
  validatable.add_error("readOnly and writeOnly cannot both be true")
65
66
  end
66
67
 
@@ -80,7 +81,7 @@ module Openapi3Parser
80
81
  NodeFactory::Array.new(context, default: nil)
81
82
  end
82
83
 
83
- def disciminator_factory(context)
84
+ def discriminator_factory(context)
84
85
  NodeFactory::Discriminator.new(context)
85
86
  end
86
87
 
@@ -114,11 +115,13 @@ module Openapi3Parser
114
115
  def additional_properties_input_type(validatable)
115
116
  input = validatable.input
116
117
  return if [true, false].include?(input) || input.is_a?(Hash)
118
+
117
119
  validatable.add_error("Expected a Boolean or an Object")
118
120
  end
119
121
 
120
122
  def additional_properties_factory(context)
121
123
  return context.input if [true, false].include?(context.input)
124
+
122
125
  referenceable_schema(context)
123
126
  end
124
127
  end
@@ -19,6 +19,7 @@ module Openapi3Parser
19
19
  value_input_type: String,
20
20
  validate: lambda do |validatable|
21
21
  return if validatable.input.any?
22
+
22
23
  validatable.add_error("Expected atleast one value")
23
24
  end
24
25
  )
@@ -27,9 +27,11 @@ module Openapi3Parser
27
27
 
28
28
  def validate_type(validatable, context)
29
29
  return true unless type
30
+
30
31
  context ||= validatable.context
31
32
  valid_type?(context.input).tap do |valid|
32
33
  next if valid
34
+
33
35
  validatable.add_error("Invalid type. #{field_error_message}",
34
36
  context)
35
37
  end
@@ -37,9 +39,11 @@ module Openapi3Parser
37
39
 
38
40
  def validate_keys(validatable, context)
39
41
  return true unless type
42
+
40
43
  context ||= validatable.context
41
44
  valid_keys?(context.input).tap do |valid|
42
45
  next if valid
46
+
43
47
  validatable.add_error("Invalid keys. #{keys_error_message}",
44
48
  context)
45
49
  end
@@ -47,6 +51,7 @@ module Openapi3Parser
47
51
 
48
52
  def raise_on_invalid_type(context)
49
53
  return true if !type || valid_type?(context.input)
54
+
50
55
  raise Error::InvalidType,
51
56
  "Invalid type for #{context.location_summary}: "\
52
57
  "#{field_error_message}"
@@ -54,6 +59,7 @@ module Openapi3Parser
54
59
 
55
60
  def raise_on_invalid_keys(context)
56
61
  return true if !type || valid_keys?(context.input)
62
+
57
63
  raise Error::InvalidType,
58
64
  "Invalid keys for #{context.location_summary}: "\
59
65
  "#{keys_error_message}"
@@ -93,6 +93,7 @@ module Openapi3Parser
93
93
  # @return [Object]
94
94
  def data_at_pointer(json_pointer)
95
95
  return data if json_pointer.empty?
96
+
96
97
  data.dig(*json_pointer) if data.respond_to?(:dig)
97
98
  end
98
99
 
@@ -104,6 +105,7 @@ module Openapi3Parser
104
105
  # @return [String]
105
106
  def relative_to_root
106
107
  return "" if root?
108
+
107
109
  source_input.relative_to(document.root_source.source_input)
108
110
  end
109
111
 
@@ -1,15 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  module Openapi3Parser
4
6
  class Source
5
7
  # Class used to represent a location within an OpenAPI document.
6
8
  # It contains a source, which is the source file/data used for the contents
7
9
  # and the pointer which indicates where in the object like file the data is
8
10
  class Location
11
+ extend Forwardable
12
+
9
13
  def self.next_field(location, field)
10
14
  new(location.source, location.pointer.segments + [field])
11
15
  end
12
16
 
17
+ def_delegators :pointer, :root?
13
18
  attr_reader :source, :pointer
14
19
 
15
20
  # @param [Openapi3Parser::Source] source
@@ -21,6 +26,7 @@ module Openapi3Parser
21
26
 
22
27
  def ==(other)
23
28
  return false unless other.instance_of?(self.class)
29
+
24
30
  source == other.source && pointer == other.pointer
25
31
  end
26
32
 
@@ -12,10 +12,11 @@ module Openapi3Parser
12
12
  absolute = fragment[0] == "/"
13
13
  segments = fragment.split("/").map do |part|
14
14
  next if part == ""
15
+
15
16
  unescaped = CGI.unescape(part.gsub("%20", "+"))
16
17
  unescaped.match?(/\A\d+\z/) ? unescaped.to_i : unescaped
17
18
  end
18
- new(segments.compact, absolute)
19
+ new(segments.compact, absolute: absolute)
19
20
  end
20
21
 
21
22
  def self.merge_pointers(base_pointer, new_pointer)
@@ -26,7 +27,7 @@ module Openapi3Parser
26
27
 
27
28
  # @param [::Array] segments
28
29
  # @param [Boolean] absolute
29
- def initialize(segments, absolute = true)
30
+ def initialize(segments, absolute: true)
30
31
  @segments = segments.freeze
31
32
  @absolute = absolute
32
33
  end
@@ -38,7 +39,7 @@ module Openapi3Parser
38
39
  def fragment
39
40
  fragment = segments.map { |s| CGI.escape(s.to_s).gsub("+", "%20") }
40
41
  .join("/")
41
- "#" + (absolute ? fragment.prepend("/") : fragment)
42
+ "##{absolute ? fragment.prepend('/') : fragment}"
42
43
  end
43
44
 
44
45
  def to_s
@@ -49,6 +50,10 @@ module Openapi3Parser
49
50
  %{#{self.class.name}(segments: #{segments}, fragment: "#{fragment}")}
50
51
  end
51
52
 
53
+ def root?
54
+ segments.empty?
55
+ end
56
+
52
57
  class MergePointers
53
58
  private_class_method :new
54
59
 
@@ -75,7 +80,7 @@ module Openapi3Parser
75
80
  def create_pointer(pointer_like)
76
81
  case pointer_like
77
82
  when Pointer then pointer_like
78
- when ::Array then Pointer.new(pointer_like, false)
83
+ when ::Array then Pointer.new(pointer_like, absolute: false)
79
84
  when ::String then Pointer.from_fragment(pointer_like)
80
85
  when nil then nil
81
86
  else raise Openapi3Parser::Error, "Unexpected type for pointer"
@@ -20,6 +20,14 @@ module Openapi3Parser
20
20
  class SourceInput
21
21
  attr_reader :access_error, :parse_error
22
22
 
23
+ def initialize
24
+ return if access_error
25
+
26
+ @contents = parse_contents
27
+ rescue ::StandardError => e
28
+ @parse_error = Error::UnparsableInput.new(e.message)
29
+ end
30
+
23
31
  # Indicates that the data within this input is suitable (i.e. can parse
24
32
  # underlying JSON or YAML) for trying to use as part of a Document
25
33
  def available?
@@ -45,6 +53,7 @@ module Openapi3Parser
45
53
  def contents
46
54
  raise access_error if access_error
47
55
  raise parse_error if parse_error
56
+
48
57
  @contents
49
58
  end
50
59
 
@@ -56,14 +65,5 @@ module Openapi3Parser
56
65
  def relative_to(_source_input)
57
66
  ""
58
67
  end
59
-
60
- private
61
-
62
- def initialize_contents
63
- return if access_error
64
- @contents = parse_contents
65
- rescue ::StandardError => e
66
- @parse_error = Error::UnparsableInput.new(e.message)
67
- end
68
68
  end
69
69
  end
@@ -24,7 +24,7 @@ module Openapi3Parser
24
24
  @path = ::File.absolute_path(path)
25
25
  working_directory ||= resolve_working_directory
26
26
  @working_directory = ::File.absolute_path(working_directory)
27
- initialize_contents
27
+ super()
28
28
  end
29
29
 
30
30
  # @see SourceInput#resolve_next
@@ -39,6 +39,7 @@ module Openapi3Parser
39
39
  # @return [Boolean]
40
40
  def ==(other)
41
41
  return false unless other.instance_of?(self.class)
42
+
42
43
  path == other.path &&
43
44
  working_directory == other.working_directory
44
45
  end
@@ -66,6 +67,7 @@ module Openapi3Parser
66
67
  end
67
68
 
68
69
  return path unless other_path
70
+
69
71
  other_path ? relative_path(other_path, path) : path
70
72
  end
71
73
 
@@ -26,7 +26,7 @@ module Openapi3Parser
26
26
  @base_url = base_url
27
27
  working_directory ||= resolve_working_directory
28
28
  @working_directory = ::File.absolute_path(working_directory)
29
- initialize_contents
29
+ super()
30
30
  end
31
31
 
32
32
  # @see SourceInput#resolve_next
@@ -44,6 +44,7 @@ module Openapi3Parser
44
44
  # @return [Boolean]
45
45
  def ==(other)
46
46
  return false unless other.instance_of?(self.class)
47
+
47
48
  raw_input == other.raw_input &&
48
49
  base_url == other.base_url &&
49
50
  working_directory == other.working_directory
@@ -72,6 +73,7 @@ module Openapi3Parser
72
73
 
73
74
  def parse_contents
74
75
  return raw_input if raw_input.respond_to?(:keys)
76
+
75
77
  StringParser.call(
76
78
  input_to_string(raw_input),
77
79
  raw_input.respond_to?(:path) ? ::File.basename(raw_input.path) : nil
@@ -81,6 +83,7 @@ module Openapi3Parser
81
83
  def input_to_string(input)
82
84
  return input.read if input.respond_to?(:read)
83
85
  return input.to_s if input.respond_to?(:to_s)
86
+
84
87
  input
85
88
  end
86
89
  end
@@ -30,6 +30,7 @@ module Openapi3Parser
30
30
 
31
31
  def source_input
32
32
  return current_source_input if reference.only_fragment?
33
+
33
34
  if reference.absolute?
34
35
  SourceInput::Url.new(reference.resource_uri)
35
36
  else
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
3
+ require "psych"
4
4
  require "json"
5
5
 
6
6
  module Openapi3Parser
@@ -27,6 +27,7 @@ module Openapi3Parser
27
27
 
28
28
  def json?
29
29
  return false if filename && ::File.extname(filename) == ".yaml"
30
+
30
31
  json_filename = filename && ::File.extname(filename) == ".json"
31
32
  json_filename || input.strip[0] == "{"
32
33
  end
@@ -36,7 +37,7 @@ module Openapi3Parser
36
37
  end
37
38
 
38
39
  def parse_yaml
39
- YAML.safe_load(input, [Date, Time], [], true)
40
+ Psych.safe_load(input, permitted_classes: [Date, Time], aliases: true)
40
41
  end
41
42
  end
42
43
  end
@@ -17,7 +17,7 @@ module Openapi3Parser
17
17
  # @param [String, URI] request_url
18
18
  def initialize(request_url)
19
19
  @request_url = request_url.to_s
20
- initialize_contents
20
+ super()
21
21
  end
22
22
 
23
23
  # @see SourceInput#resolve_next
@@ -32,6 +32,7 @@ module Openapi3Parser
32
32
  # @return [Boolean]
33
33
  def ==(other)
34
34
  return false unless other.instance_of?(self.class)
35
+
35
36
  [request_url, resolved_url].include?(other.request_url) ||
36
37
  [request_url, resolved_url].include?(other.resolved_url)
37
38
  end
@@ -89,6 +90,7 @@ module Openapi3Parser
89
90
 
90
91
  def call
91
92
  return to_uri.to_s if different_hosts?
93
+
92
94
  relative_path
93
95
  end
94
96
 
@@ -35,6 +35,7 @@ module Openapi3Parser
35
35
  def for_type
36
36
  return unless factory_class
37
37
  return "(anonymous)" unless factory_class.name
38
+
38
39
  factory_class.name.split("::").last
39
40
  end
40
41
 
@@ -47,6 +48,7 @@ module Openapi3Parser
47
48
  # @return [Boolean]
48
49
  def ==(other)
49
50
  return false unless other.instance_of?(self.class)
51
+
50
52
  message == other.message &&
51
53
  context == other.context &&
52
54
  factory_class == other.factory_class
@@ -56,7 +56,7 @@ module Openapi3Parser
56
56
  grouped.each_with_object({}) do |(_, items), memo|
57
57
  items.each do |item|
58
58
  key = item.location_summary(with_type: items.count > 1)
59
- memo[key] = (memo[key] || []) + item.errors.map(&:to_s)
59
+ memo[key] = memo.fetch(key, []) + item.errors.map(&:to_s)
60
60
  end
61
61
  end
62
62
  end
@@ -10,7 +10,7 @@ module Openapi3Parser
10
10
  end
11
11
 
12
12
  def call(validatable)
13
- error = @callable.call(validatable.input)
13
+ error = callable.call(validatable.input)
14
14
  validatable.add_error(error) if error
15
15
  end
16
16
  end
@@ -6,7 +6,7 @@ module Openapi3Parser
6
6
  # defined for a Components node.
7
7
  # As defined: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#components-object
8
8
  class ComponentKeys
9
- REGEX = /\A[a-zA-Z0-9\.\-_]+\Z/
9
+ REGEX = /\A[a-zA-Z0-9.\-_]+\Z/.freeze
10
10
 
11
11
  def self.call(input)
12
12
  invalid = input.keys.reject { |key| REGEX.match(key) }
@@ -17,6 +17,7 @@ module Openapi3Parser
17
17
  def duplicate_names_by_in(resolved_input)
18
18
  potential_items = resolved_input.reject do |item|
19
19
  next true unless item.respond_to?(:keys)
20
+
20
21
  item["name"].nil? || item["in"].nil?
21
22
  end
22
23
 
@@ -7,12 +7,12 @@ module Openapi3Parser
7
7
  # https://html.spec.whatwg.org/#e-mail-state-(type=email)
8
8
  REGEX = %r{
9
9
  \A
10
- [a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+
10
+ [a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+
11
11
  @
12
12
  [a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?
13
- (?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*
13
+ (?:.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*
14
14
  \Z
15
- }x
15
+ }x.freeze
16
16
 
17
17
  def self.call(input)
18
18
  message = %("#{input}" is not a valid email address)
@@ -9,7 +9,7 @@ module Openapi3Parser
9
9
  / # separating slash
10
10
  ([-+.\w]+|\*) # word (with +, - & .) or asterisk
11
11
  \Z
12
- }x
12
+ }x.freeze
13
13
 
14
14
  def self.call(input)
15
15
  message = %("#{input}" is not a valid media type)
@@ -8,8 +8,8 @@ module Openapi3Parser
8
8
  using ArraySentence
9
9
  private_class_method :new
10
10
 
11
- def self.call(*args)
12
- new.call(*args)
11
+ def self.call(*args, **kwargs)
12
+ new.call(*args, **kwargs)
13
13
  end
14
14
 
15
15
  def call(validatable,
@@ -78,12 +78,12 @@ module Openapi3Parser
78
78
 
79
79
  def errors
80
80
  @errors ||= begin
81
- default = { required: [], exclusive: [] }
82
- mutually_exclusive_fields
83
- .each_with_object(default) do |exclusive, errors|
84
- add_error(errors, exclusive)
85
- end
86
- end
81
+ default = { required: [], exclusive: [] }
82
+ mutually_exclusive_fields
83
+ .each_with_object(default) do |exclusive, errors|
84
+ add_error(errors, exclusive)
85
+ end
86
+ end
87
87
  end
88
88
 
89
89
  private