openapi3_parser 0.2.0 → 0.3.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 (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
@@ -2,35 +2,77 @@
2
2
 
3
3
  module Openapi3Parser
4
4
  module Validation
5
+ # An immutable collection of Validation::Error objects
6
+ # @attr_reader [Array<Validation::Error>] errors
5
7
  class ErrorCollection
6
- def initialize(*errors)
7
- reset
8
- append(*errors)
9
- end
8
+ include Enumerable
10
9
 
11
- def append(*errors)
12
- errors.each { |e| @errors << e }
10
+ # Combines ErrorCollection objects or arrays of Validation::Error objects
11
+ # @param [ErrorCollection, Array<Validation::Error>] errors
12
+ # @param [ErrorCollection, Array<Validation::Error>] other_errors
13
+ # @return [ErrorCollection]
14
+ def self.combine(errors, other_errors)
15
+ new(errors.to_a + other_errors.to_a)
13
16
  end
14
17
 
15
- def reset
16
- @errors = []
17
- end
18
+ attr_reader :errors
19
+ alias to_a errors
18
20
 
19
- def merge(error_collection)
20
- append(*error_collection.to_a)
21
+ # @param [Array<Validation::Error>] errors
22
+ def initialize(errors = [])
23
+ @errors = errors.freeze
21
24
  end
22
25
 
23
26
  def empty?
24
27
  errors.empty?
25
28
  end
26
29
 
27
- def to_a
28
- errors.dup
30
+ def each(&block)
31
+ errors.each(&block)
29
32
  end
30
33
 
31
- private
34
+ # Group errors by those in the same location for the same node
35
+ #
36
+ # @return [Array<LocationTypeGroup]
37
+ def group_errors
38
+ grouped = group_by do |e|
39
+ [e.source_location.to_s, e.for_type]
40
+ end
32
41
 
33
- attr_reader :errors
42
+ grouped.map do |_, errors|
43
+ LocationTypeGroup.new(errors[0].source_location,
44
+ errors[0].for_type,
45
+ errors)
46
+ end
47
+ end
48
+
49
+ # Return a hash structure where the location is key and the errors are
50
+ # values
51
+ #
52
+ # @return [Hash]
53
+ def to_h
54
+ grouped = group_errors.group_by { |g| g.source_location.to_s }
55
+
56
+ grouped.each_with_object({}) do |(_, items), memo|
57
+ items.each do |item|
58
+ key = item.location_summary(with_type: items.count > 1)
59
+ memo[key] = (memo[key] || []) + item.errors.map(&:to_s)
60
+ end
61
+ end
62
+ end
63
+
64
+ # @return [String]
65
+ def inspect
66
+ "#{self.class.name}(errors: #{to_h})"
67
+ end
68
+
69
+ LocationTypeGroup = Struct.new(:source_location, :for_type, :errors) do
70
+ def location_summary(with_type: false)
71
+ string = source_location.to_s
72
+ string << " (as #{for_type})" if with_type && !for_type&.empty?
73
+ string
74
+ end
75
+ end
34
76
  end
35
77
  end
36
78
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openapi3Parser
4
+ module Validators
5
+ class Reference
6
+ def initialize(given_reference)
7
+ @given_reference = given_reference
8
+ end
9
+
10
+ def valid?
11
+ errors.empty?
12
+ end
13
+
14
+ def errors
15
+ @errors ||= Array(build_errors)
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :given_reference
21
+
22
+ def build_errors
23
+ return "expected a string" unless given_reference.is_a?(String)
24
+ begin
25
+ uri = URI.parse(given_reference)
26
+ rescue URI::Error
27
+ return "could not parse as a URI"
28
+ end
29
+ check_fragment(uri) || []
30
+ end
31
+
32
+ def check_fragment(uri)
33
+ return if uri.fragment.nil? || uri.fragment.empty?
34
+ first_char = uri.fragment[0]
35
+
36
+ "invalid JSON pointer, expected a root slash" if first_char != "/"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Openapi3Parser
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -7,22 +7,27 @@ require "openapi3_parser/version"
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = "openapi3_parser"
9
9
  spec.version = Openapi3Parser::VERSION
10
- spec.authors = ["Kevin Dew"]
11
- spec.email = ["kevindew@me.com"]
10
+ spec.author = "Kevin Dew"
11
+ spec.email = "kevindew@me.com"
12
12
 
13
13
  spec.summary = "An OpenAPI V3 parser for Ruby"
14
- spec.description = "An OpenAPI V3 parser for Ruby"
14
+ spec.description = <<-DESCRIPTION
15
+ A tool to parse and validate OpenAPI V3 files.
16
+ Aims to provide complete compatibility with the OpenAPI specification and
17
+ to provide a natural, idiomatic way to interact with a openapi.yaml file.
18
+ DESCRIPTION
15
19
  spec.homepage = "https://github.com/kevindew/openapi_parser"
16
20
  spec.license = "MIT"
17
21
 
18
22
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
23
  f.match(%r{^spec/})
20
24
  end
21
- spec.require_paths = ["lib"]
25
+
26
+ spec.required_ruby_version = ">= 2.3.0"
22
27
 
23
28
  spec.add_development_dependency "bundler", "~> 1.15"
24
- spec.add_development_dependency "byebug", "~> 9.1"
25
29
  spec.add_development_dependency "rake", "~> 10.0"
26
30
  spec.add_development_dependency "rspec", "~> 3.6"
27
31
  spec.add_development_dependency "rubocop", "~> 0.51"
32
+ spec.add_development_dependency "webmock", "~> 3.1"
28
33
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi3_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Dew
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-12-09 00:00:00.000000000 Z
11
+ date: 2018-01-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -25,64 +25,66 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.15'
27
27
  - !ruby/object:Gem::Dependency
28
- name: byebug
28
+ name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '9.1'
33
+ version: '10.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '9.1'
40
+ version: '10.0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rake
42
+ name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '10.0'
47
+ version: '3.6'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '10.0'
54
+ version: '3.6'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rspec
56
+ name: rubocop
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '3.6'
61
+ version: '0.51'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '3.6'
68
+ version: '0.51'
69
69
  - !ruby/object:Gem::Dependency
70
- name: rubocop
70
+ name: webmock
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0.51'
75
+ version: '3.1'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0.51'
83
- description: An OpenAPI V3 parser for Ruby
84
- email:
85
- - kevindew@me.com
82
+ version: '3.1'
83
+ description: |2
84
+ A tool to parse and validate OpenAPI V3 files.
85
+ Aims to provide complete compatibility with the OpenAPI specification and
86
+ to provide a natural, idiomatic way to interact with a openapi.yaml file.
87
+ email: kevindew@me.com
86
88
  executables: []
87
89
  extensions: []
88
90
  extra_rdoc_files: []
@@ -100,10 +102,11 @@ files:
100
102
  - TODO.md
101
103
  - lib/openapi3_parser.rb
102
104
  - lib/openapi3_parser/context.rb
105
+ - lib/openapi3_parser/context/location.rb
106
+ - lib/openapi3_parser/context/pointer.rb
103
107
  - lib/openapi3_parser/document.rb
108
+ - lib/openapi3_parser/document/reference_register.rb
104
109
  - lib/openapi3_parser/error.rb
105
- - lib/openapi3_parser/fields/map.rb
106
- - lib/openapi3_parser/node.rb
107
110
  - lib/openapi3_parser/node/map.rb
108
111
  - lib/openapi3_parser/node/object.rb
109
112
  - lib/openapi3_parser/node_factories/array.rb
@@ -141,6 +144,7 @@ files:
141
144
  - lib/openapi3_parser/node_factories/xml.rb
142
145
  - lib/openapi3_parser/node_factory.rb
143
146
  - lib/openapi3_parser/node_factory/field_config.rb
147
+ - lib/openapi3_parser/node_factory/fields/reference.rb
144
148
  - lib/openapi3_parser/node_factory/map.rb
145
149
  - lib/openapi3_parser/node_factory/object.rb
146
150
  - lib/openapi3_parser/node_factory/object/node_builder.rb
@@ -178,8 +182,18 @@ files:
178
182
  - lib/openapi3_parser/nodes/server_variable.rb
179
183
  - lib/openapi3_parser/nodes/tag.rb
180
184
  - lib/openapi3_parser/nodes/xml.rb
185
+ - lib/openapi3_parser/source.rb
186
+ - lib/openapi3_parser/source/reference.rb
187
+ - lib/openapi3_parser/source/reference_resolver.rb
188
+ - lib/openapi3_parser/source_input.rb
189
+ - lib/openapi3_parser/source_input/file.rb
190
+ - lib/openapi3_parser/source_input/raw.rb
191
+ - lib/openapi3_parser/source_input/resolve_next.rb
192
+ - lib/openapi3_parser/source_input/string_parser.rb
193
+ - lib/openapi3_parser/source_input/url.rb
181
194
  - lib/openapi3_parser/validation/error.rb
182
195
  - lib/openapi3_parser/validation/error_collection.rb
196
+ - lib/openapi3_parser/validators/reference.rb
183
197
  - lib/openapi3_parser/version.rb
184
198
  - openapi3_parser.gemspec
185
199
  homepage: https://github.com/kevindew/openapi_parser
@@ -194,7 +208,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
194
208
  requirements:
195
209
  - - ">="
196
210
  - !ruby/object:Gem::Version
197
- version: '0'
211
+ version: 2.3.0
198
212
  required_rubygems_version: !ruby/object:Gem::Requirement
199
213
  requirements:
200
214
  - - ">="
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "openapi3_parser/error"
4
-
5
- module Openapi3Parser
6
- module Fields
7
- class Map
8
- private_class_method :new
9
-
10
- def initialize(input, context, value_type, key_format)
11
- @input = input
12
- @context = context
13
- @value_type = value_type
14
- @key_format = key_format
15
- end
16
-
17
- def self.call(
18
- input,
19
- context,
20
- value_type: Hash,
21
- key_format: nil,
22
- &block
23
- )
24
- new(input, context, value_type, key_format).call(&block)
25
- end
26
-
27
- def self.reference_input(
28
- input,
29
- context,
30
- value_type: Hash,
31
- key_format: nil,
32
- &block
33
- )
34
- call(
35
- input, context, value_type: value_type, key_format: key_format
36
- ) do |field_input, field_context|
37
- field_context.possible_reference(field_input, &block)
38
- end
39
- end
40
-
41
- def call(&block)
42
- validate_keys
43
- validate_values
44
-
45
- input.each_with_object({}) do |(key, value), memo|
46
- memo[key] = if block
47
- yield(value, context.next_namespace(key), key)
48
- else
49
- value
50
- end
51
- end
52
- end
53
-
54
- private
55
-
56
- attr_reader :input, :context, :value_type, :key_format
57
-
58
- def validate_keys
59
- return unless key_format
60
- invalid_keys = input.keys.reject { |key| key =~ key_format }
61
- return if invalid_keys.empty?
62
-
63
- raise Openapi3Parser::Error, "Invalid field names for "\
64
- "#{context.stringify_namespace}: #{invalid_keys.join(', ')}"
65
- end
66
-
67
- def validate_values
68
- return unless value_type
69
- invalid = input.reject do |key, value|
70
- if value_type.is_a?(Proc)
71
- value.call(value, key)
72
- else
73
- value.is_a?(value_type)
74
- end
75
- end
76
- return if invalid.empty?
77
-
78
- raise Openapi3Parser::Error, "Unexpected type for "\
79
- "#{context.stringify_namespace}: #{invalid.keys.join(', ')}"
80
- end
81
- end
82
- end
83
- end
@@ -1,115 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "openapi3_parser/node/field_config"
4
-
5
- module Openapi3Parser
6
- module Node
7
- module ClassMethods
8
- def field(name, **options)
9
- @field_configs ||= {}
10
- @field_configs[name] = FieldConfig.new(options)
11
- end
12
-
13
- def field_configs
14
- @field_configs || {}
15
- end
16
-
17
- def allow_extensions
18
- @allow_extensions = true
19
- end
20
-
21
- def disallow_extensions
22
- @allow_extensions = false
23
- end
24
-
25
- def allowed_extensions?
26
- @allow_extensions == true
27
- end
28
- end
29
-
30
- def self.included(base)
31
- base.extend(ClassMethods)
32
- end
33
-
34
- EXTENSION_REGEX = /^x-(.*)/
35
-
36
- attr_reader :input, :context, :fields
37
-
38
- def initialize(input, context)
39
- @input = input
40
- @context = context
41
- @fields = build_fields(input)
42
- end
43
-
44
- def [](value)
45
- fields[value]
46
- end
47
-
48
- def extension(value)
49
- fields["x-#{value}"]
50
- end
51
-
52
- private
53
-
54
- def build_fields(input)
55
- check_for_unexpected_fields(input)
56
- create_fields(input)
57
- end
58
-
59
- def check_for_unexpected_fields(input)
60
- allowed_fields = field_configs.keys
61
- remaining_fields = input.keys - allowed_fields
62
- return if remaining_fields.empty?
63
-
64
- if allowed_extensions?
65
- remaining_fields.reject! { |key| key =~ EXTENSION_REGEX }
66
- end
67
-
68
- return if remaining_fields.empty?
69
- raise Error,
70
- "Unexpected attributes for #{context.stringify_namespace}: "\
71
- "#{remaining_fields.join(', ')}"
72
- end
73
-
74
- def create_fields(input)
75
- check_required(input)
76
- check_types(input)
77
- fields = field_configs.each_with_object({}) do |(field, config), memo|
78
- next_context = context.next_namespace(field)
79
- memo[field] = config.build(input[field], self, next_context)
80
- end
81
- extensions = input.select { |(k, _)| k =~ EXTENSION_REGEX }
82
- fields.merge(extensions)
83
- end
84
-
85
- def check_required(input)
86
- missing = field_configs.reject do |field, config|
87
- config.valid_presence?(input[field])
88
- end
89
-
90
- return if missing.empty?
91
- raise Error,
92
- "Missing required fields for #{context.stringify_namespace}: "\
93
- "#{missing.keys}"
94
- end
95
-
96
- def check_types(input)
97
- invalid = field_configs.reject do |field, config|
98
- config.valid_input_type?(input[field], self)
99
- end
100
-
101
- return if invalid.empty?
102
- raise Error,
103
- "Invalid fields for #{context.stringify_namespace}: "\
104
- "#{invalid.keys}"
105
- end
106
-
107
- def allowed_extensions?
108
- self.class.allowed_extensions?
109
- end
110
-
111
- def field_configs
112
- self.class.field_configs || {}
113
- end
114
- end
115
- end