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
@@ -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