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
@@ -66,6 +66,14 @@ module Openapi3Parser
66
66
  self["servers"]
67
67
  end
68
68
 
69
+ # Whether this object uses it's own defined servers instead of falling
70
+ # back to the root ones.
71
+ #
72
+ # @return [Boolean]
73
+ def alternative_servers?
74
+ servers != node_context.document.root.servers
75
+ end
76
+
69
77
  # @return [Node::Array<Parameter>]
70
78
  def parameters
71
79
  self["parameters"]
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  module Openapi3Parser
4
6
  module Node
5
7
  class Placeholder
8
+ extend Forwardable
9
+
6
10
  def self.resolve(potential_placeholder)
7
11
  if potential_placeholder.is_a?(Placeholder)
8
12
  potential_placeholder.node
@@ -13,12 +17,12 @@ module Openapi3Parser
13
17
 
14
18
  # Used to iterate through hashes or arrays that may contain
15
19
  # Placeholder objects where these are resolved to being nodes
16
- # befor iteration
20
+ # before iteration
17
21
  def self.each(node_data, &block)
18
22
  resolved =
19
23
  if node_data.respond_to?(:keys)
20
- node_data.each_with_object({}) do |(key, value), memo|
21
- memo[key] = resolve(value)
24
+ node_data.transform_values do |value|
25
+ resolve(value)
22
26
  end
23
27
  else
24
28
  node_data.map { |item| resolve(item) }
@@ -27,6 +31,10 @@ module Openapi3Parser
27
31
  resolved.each(&block)
28
32
  end
29
33
 
34
+ attr_reader :node_factory, :field, :parent_context
35
+
36
+ def_delegators :node_factory, :nil_input?
37
+
30
38
  def initialize(node_factory, field, parent_context)
31
39
  @node_factory = node_factory
32
40
  @field = field
@@ -35,16 +43,12 @@ module Openapi3Parser
35
43
 
36
44
  def node
37
45
  @node ||= begin
38
- node_context = Context.next_field(parent_context,
39
- field,
40
- node_factory.context)
41
- node_factory.node(node_context)
42
- end
46
+ node_context = Context.next_field(parent_context,
47
+ field,
48
+ node_factory.context)
49
+ node_factory.node(node_context)
50
+ end
43
51
  end
44
-
45
- private
46
-
47
- attr_reader :node_factory, :field, :parent_context
48
52
  end
49
53
  end
50
54
  end
@@ -5,8 +5,39 @@ require "openapi3_parser/node/object"
5
5
  module Openapi3Parser
6
6
  module Node
7
7
  # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject
8
- # rubocop:disable ClassLength
8
+ # rubocop:disable Metrics/ClassLength
9
9
  class Schema < Node::Object
10
+ # This is used to provide a name for the schema based on it's position in
11
+ # an OpenAPI document.
12
+ #
13
+ # For example it's common to have an OpenAPI document structured like so:
14
+ # components:
15
+ # schemas:
16
+ # Product:
17
+ # properties:
18
+ # product_id:
19
+ # type: string
20
+ # description:
21
+ # type: string
22
+ #
23
+ # and there is then implied meaning in the field name of Product, ie
24
+ # that schema now represents a product. This data is not easily or
25
+ # consistently made available as it is part of the path to the data
26
+ # rather than the data itself. Instead the field that would be more
27
+ # appropriate would be "title" within a schema.
28
+ #
29
+ # As this is a common pattern in OpenAPI docs this provides a method
30
+ # to look up this contextual name of the schema so it can be referenced
31
+ # when working with the document, it only considers a field to be
32
+ # name if it is within a group called schemas (as is the case
33
+ # in #/components/schemas)
34
+ #
35
+ # @return [String, nil]
36
+ def name
37
+ segments = node_context.source_location.pointer.segments
38
+ segments[-1] if segments[-2] == "schemas"
39
+ end
40
+
10
41
  # @return [String, nil]
11
42
  def title
12
43
  self["title"]
@@ -82,6 +113,21 @@ module Openapi3Parser
82
113
  self["required"]
83
114
  end
84
115
 
116
+ # Returns whether a property is a required field or not. Can accept the
117
+ # property name or a schema
118
+ #
119
+ # @param [String, Schema] property
120
+ # @return [Boolean]
121
+ def requires?(property)
122
+ if property.is_a?(Schema)
123
+ properties.to_h
124
+ .select { |k, _| required.to_a.include?(k) }
125
+ .any? { |_, schema| schema == property }
126
+ else
127
+ required.to_a.include?(property)
128
+ end
129
+ end
130
+
85
131
  # @return [Node::Array<Object>, nil]
86
132
  def enum
87
133
  self["enum"]
@@ -131,6 +177,7 @@ module Openapi3Parser
131
177
  def additional_properties_schema
132
178
  properties = self["additionalProperties"]
133
179
  return if [true, false].include?(properties)
180
+
134
181
  properties
135
182
  end
136
183
 
@@ -160,7 +207,7 @@ module Openapi3Parser
160
207
  end
161
208
 
162
209
  # @return [Discriminator, nil]
163
- def disciminator
210
+ def discriminator
164
211
  self["discriminator"]
165
212
  end
166
213
 
@@ -194,6 +241,6 @@ module Openapi3Parser
194
241
  self["deprecated"]
195
242
  end
196
243
  end
197
- # rubocop:enable ClassLength
244
+ # rubocop:enable Metrics/ClassLength
198
245
  end
199
246
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Openapi3Parser
4
4
  module NodeFactory
5
- EXTENSION_REGEX = /^x-(.*)/
5
+ EXTENSION_REGEX = /^x-(.*)/.freeze
6
6
  end
7
7
  end
@@ -3,23 +3,27 @@
3
3
  module Openapi3Parser
4
4
  module NodeFactory
5
5
  class Array
6
- attr_reader :context, :data, :default, :value_input_type,
7
- :value_factory, :validation
6
+ attr_reader :context, :data, :default, :use_default_on_empty,
7
+ :value_input_type, :value_factory, :validation
8
8
 
9
+ # rubocop:disable Metrics/ParameterLists
9
10
  def initialize(
10
11
  context,
11
12
  default: [],
13
+ use_default_on_empty: false,
12
14
  value_input_type: nil,
13
15
  value_factory: nil,
14
16
  validate: nil
15
17
  )
16
18
  @context = context
17
19
  @default = default
20
+ @use_default_on_empty = use_default_on_empty
18
21
  @value_input_type = value_input_type
19
22
  @value_factory = value_factory
20
23
  @validation = validate
21
24
  @data = build_data(context.input)
22
25
  end
26
+ # rubocop:enable Metrics/ParameterLists
23
27
 
24
28
  def raw_input
25
29
  context.input
@@ -50,18 +54,25 @@ module Openapi3Parser
50
54
  %{#{self.class.name}(#{context.source_location.inspect})}
51
55
  end
52
56
 
57
+ def use_default?
58
+ return true if nil_input? || !raw_input.is_a?(::Array)
59
+ return false unless use_default_on_empty
60
+
61
+ raw_input.empty?
62
+ end
63
+
53
64
  private
54
65
 
55
66
  def build_data(raw_input)
56
- use_default = nil_input? || !raw_input.is_a?(::Array)
57
- return if use_default && default.nil?
58
- process_data(use_default ? default : raw_input)
67
+ return if use_default? && default.nil?
68
+
69
+ process_data(use_default? ? default : raw_input)
59
70
  end
60
71
 
61
72
  def process_data(data)
62
73
  data.each_with_index.map do |value, i|
63
74
  if value_factory
64
- initialize_value_factory(Context.next_field(context, i))
75
+ initialize_value_factory(Context.next_field(context, i, value))
65
76
  else
66
77
  value
67
78
  end
@@ -104,26 +115,24 @@ module Openapi3Parser
104
115
 
105
116
  def errors
106
117
  return validatable.collection if factory.nil_input?
118
+
107
119
  TypeChecker.validate_type(validatable, type: ::Array)
108
120
  return validatable.collection if validatable.errors.any?
121
+
109
122
  collate_errors
110
123
  validatable.collection
111
124
  end
112
125
 
113
126
  def data(parent_context)
114
- return default_value if factory.nil_input?
127
+ if factory.use_default?
128
+ return factory.default.nil? ? nil : build_node_data(parent_context)
129
+ end
115
130
 
116
131
  TypeChecker.raise_on_invalid_type(factory.context, type: ::Array)
117
132
  check_values(raise_on_invalid: true)
118
133
  validate(raise_on_invalid: true)
119
134
 
120
- factory.data.each_with_index.map do |value, i|
121
- if value.respond_to?(:node)
122
- Node::Placeholder.new(value, i, parent_context)
123
- else
124
- value
125
- end
126
- end
135
+ build_node_data(parent_context)
127
136
  end
128
137
 
129
138
  private_class_method :new
@@ -132,6 +141,14 @@ module Openapi3Parser
132
141
 
133
142
  attr_reader :factory, :validatable
134
143
 
144
+ def build_node_data(parent_context)
145
+ factory.data.each_with_index.map do |value, i|
146
+ next value unless value.respond_to?(:node)
147
+
148
+ Node::Placeholder.new(value, i, parent_context)
149
+ end
150
+ end
151
+
135
152
  def collate_errors
136
153
  check_values(raise_on_invalid: false)
137
154
  validate(raise_on_invalid: false)
@@ -141,21 +158,12 @@ module Openapi3Parser
141
158
  end
142
159
  end
143
160
 
144
- def default_value
145
- if factory.nil_input? && factory.default.nil?
146
- nil
147
- else
148
- factory.data
149
- end
150
- end
151
-
152
161
  def check_values(raise_on_invalid: false)
153
162
  return unless factory.value_input_type
154
163
 
155
- factory.context.input.each_index do |index|
156
- check_field_type(
157
- Context.next_field(factory.context, index), raise_on_invalid
158
- )
164
+ factory.context.input.each_with_index do |value, index|
165
+ check_field_type(Context.next_field(factory.context, index, value),
166
+ raise_on_invalid)
159
167
  end
160
168
  end
161
169
 
@@ -12,6 +12,8 @@ module Openapi3Parser
12
12
  # @attr_reader [Array<Source::Location>] refernce_locations
13
13
  #
14
14
  class Context
15
+ UNDEFINED = Class.new
16
+
15
17
  # Create a context for the root of a document
16
18
  #
17
19
  # @param [Any] input
@@ -29,10 +31,15 @@ module Openapi3Parser
29
31
  #
30
32
  # @param [Context] parent_context
31
33
  # @param [String] field
34
+ # @param [Any] input
32
35
  # @return [Context]
33
- def self.next_field(parent_context, field)
36
+ def self.next_field(parent_context, field, given_input = UNDEFINED)
34
37
  pc = parent_context
35
- input = pc.input.respond_to?(:[]) ? pc.input[field] : nil
38
+ input = if given_input == UNDEFINED
39
+ pc.input.respond_to?(:[]) ? pc.input[field] : nil
40
+ else
41
+ given_input
42
+ end
36
43
  source_location = Source::Location.next_field(pc.source_location, field)
37
44
  new(input,
38
45
  source_location: source_location,
@@ -19,9 +19,11 @@ module Openapi3Parser
19
19
  def validate_mapping(validatable)
20
20
  input = validatable.input
21
21
  return if input.empty?
22
+
22
23
  string_keys = input.keys.map(&:class).uniq == [String]
23
24
  string_values = input.values.map(&:class).uniq == [String]
24
25
  return if string_keys && string_values
26
+
25
27
  validatable.add_error("Expected string keys and string values")
26
28
  end
27
29
  end
@@ -70,8 +70,10 @@ module Openapi3Parser
70
70
 
71
71
  def errors
72
72
  return validatable.collection if factory.nil_input?
73
+
73
74
  TypeChecker.validate_type(validatable, type: factory.input_type)
74
75
  return validatable.collection if validatable.errors.any?
76
+
75
77
  validate(raise_on_invalid: false)
76
78
  validatable.collection
77
79
  end
@@ -72,6 +72,7 @@ module Openapi3Parser
72
72
 
73
73
  def create_resolved_reference
74
74
  return unless reference_validator.valid?
75
+
75
76
  context.resolve_reference(reference,
76
77
  factory,
77
78
  recursive: context.self_referencing?)
@@ -60,6 +60,7 @@ module Openapi3Parser
60
60
  def build_data(raw_input)
61
61
  use_default = nil_input? || !raw_input.is_a?(::Hash)
62
62
  return if use_default && default.nil?
63
+
63
64
  process_data(use_default ? default : raw_input)
64
65
  end
65
66
 
@@ -89,12 +90,12 @@ module Openapi3Parser
89
90
  def build_resolved_input
90
91
  return unless data
91
92
 
92
- data.each_with_object({}) do |(key, value), memo|
93
- memo[key] = if value.respond_to?(:resolved_input)
94
- value.resolved_input
95
- else
96
- value
97
- end
93
+ data.transform_values do |value|
94
+ if value.respond_to?(:resolved_input)
95
+ value.resolved_input
96
+ else
97
+ value
98
+ end
98
99
  end
99
100
  end
100
101
 
@@ -114,8 +115,10 @@ module Openapi3Parser
114
115
 
115
116
  def errors
116
117
  return validatable.collection if factory.nil_input?
118
+
117
119
  TypeChecker.validate_type(validatable, type: ::Hash)
118
120
  return validatable.collection if validatable.errors.any?
121
+
119
122
  collate_errors
120
123
  validatable.collection
121
124
  end
@@ -175,12 +178,11 @@ module Openapi3Parser
175
178
  def check_values(raise_on_invalid: false)
176
179
  return unless factory.value_input_type
177
180
 
178
- factory.context.input.keys.each do |key|
181
+ factory.context.input.each do |key, value|
179
182
  next if factory.allow_extensions && key.to_s =~ EXTENSION_REGEX
180
183
 
181
- check_field_type(
182
- Context.next_field(factory.context, key), raise_on_invalid
183
- )
184
+ check_field_type(Context.next_field(factory.context, key, value),
185
+ raise_on_invalid)
184
186
  end
185
187
  end
186
188
 
@@ -48,6 +48,7 @@ module Openapi3Parser
48
48
  def call(validatable)
49
49
  missing_keys = validatable.input.keys - properties
50
50
  return if missing_keys.empty?
51
+
51
52
  validatable.add_error(error_message(missing_keys))
52
53
  end
53
54
 
@@ -78,6 +78,7 @@ module Openapi3Parser
78
78
  def build_data(raw_input)
79
79
  use_default = nil_input? || !raw_input.is_a?(::Hash)
80
80
  return if use_default && default.nil?
81
+
81
82
  process_data(use_default ? default : raw_input)
82
83
  end
83
84
 
@@ -85,7 +86,8 @@ module Openapi3Parser
85
86
  field_configs.each_with_object(raw_data.dup) do |(field, config), memo|
86
87
  memo[field] = nil unless memo[field]
87
88
  next unless config.factory?
88
- next_context = Context.next_field(context, field)
89
+
90
+ next_context = Context.next_field(context, field, memo[field])
89
91
  memo[field] = config.initialize_factory(next_context, self)
90
92
  end
91
93
  end
@@ -95,6 +97,7 @@ module Openapi3Parser
95
97
 
96
98
  data.each_with_object({}) do |(key, value), memo|
97
99
  next if value.respond_to?(:nil_input?) && value.nil_input?
100
+
98
101
  memo[key] = if value.respond_to?(:resolved_input)
99
102
  value.resolved_input
100
103
  else