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
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openapi3_parser/source_input"
4
+ require "openapi3_parser/source_input/string_parser"
5
+ require "openapi3_parser/source_input/resolve_next"
6
+ require "openapi3_parser/error"
7
+
8
+ module Openapi3Parser
9
+ class SourceInput
10
+ # An input of a file on the file system
11
+ #
12
+ # @attr_reader [String] path The absolute path to this file
13
+ # @attr_reader [String] working_directory The abolsute path of the
14
+ # working directory to use when
15
+ # opening relative references to
16
+ # this file
17
+ class File < SourceInput
18
+ attr_reader :path, :working_directory
19
+
20
+ # @param [String] path The path to the file to open
21
+ # as this source
22
+ # @param [String, nil] working_directory The path to the
23
+ # working directory to use, will
24
+ # be calculated from path if not
25
+ # provided
26
+ def initialize(path, working_directory: nil)
27
+ @path = ::File.absolute_path(path)
28
+ working_directory ||= resolve_working_directory
29
+ @working_directory = ::File.absolute_path(working_directory)
30
+ initialize_contents
31
+ end
32
+
33
+ # @see SourceInput#resolve_next
34
+ # @param [Source::Reference] reference
35
+ # @return [SourceInput]
36
+ def resolve_next(reference)
37
+ ResolveNext.call(reference, self, working_directory: working_directory)
38
+ end
39
+
40
+ # @see SourceInput#other
41
+ # @param [SourceInput] other
42
+ # @return [Boolean]
43
+ def ==(other)
44
+ return false unless other.instance_of?(self.class)
45
+ path == other.path &&
46
+ working_directory == other.working_directory
47
+ end
48
+
49
+ # return [String]
50
+ def inspect
51
+ %{#{self.class.name}(path: #{path}, working_directory: } +
52
+ %{#{working_directory})}
53
+ end
54
+
55
+ # @return [String]
56
+ def to_s
57
+ path
58
+ end
59
+
60
+ # Attempt to return a shorter relative path to the other source input
61
+ # so we can produce succinct output
62
+ #
63
+ # @return [String]
64
+ def relative_to(source_input)
65
+ other_path = if source_input.respond_to?(:path)
66
+ ::File.dirname(source_input.path)
67
+ elsif source_input.respond_to?(:working_directory)
68
+ source_input.working_directory
69
+ end
70
+
71
+ return path unless other_path
72
+ other_path ? relative_path(other_path, path) : path
73
+ end
74
+
75
+ private
76
+
77
+ def resolve_working_directory
78
+ ::File.dirname(path)
79
+ end
80
+
81
+ def parse_contents
82
+ begin
83
+ contents = ::File.read(path)
84
+ rescue ::StandardError => e
85
+ @access_error = Error::InaccessibleInput.new(e.message)
86
+ return
87
+ end
88
+
89
+ filename = ::File.basename(path)
90
+ StringParser.call(contents, filename)
91
+ end
92
+
93
+ def relative_path(from, to)
94
+ from_path = Pathname.new(from)
95
+ to_path = Pathname.new(to)
96
+ relative = to_path.relative_path_from(from_path).to_s
97
+
98
+ relative.size > to.size ? to : relative
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openapi3_parser/source_input"
4
+ require "openapi3_parser/source_input/string_parser"
5
+ require "openapi3_parser/source_input/resolve_next"
6
+
7
+ module Openapi3Parser
8
+ class SourceInput
9
+ # An input of data (typically a Hash) to for initialising an OpenAPI
10
+ # document. Most likely used in development scenarios when you want to
11
+ # test things without creating/tweaking an OpenAPI source file
12
+ #
13
+ # @attr_reader [Object] raw_input The data for the document
14
+ # @attr_reader [String, nil] base_url A url to be used for
15
+ # resolving relative
16
+ # references
17
+ # @attr_reader [String, nil] working_directory A path to be used for
18
+ # resolving relative
19
+ # references
20
+ class Raw < SourceInput
21
+ attr_reader :raw_input, :base_url, :working_directory
22
+
23
+ # @param [Object] raw_input
24
+ # @param [String, nil] base_url
25
+ # @param [String, nil] working_directory
26
+ def initialize(raw_input, base_url: nil, working_directory: nil)
27
+ @raw_input = raw_input
28
+ @base_url = base_url
29
+ working_directory ||= resolve_working_directory
30
+ @working_directory = ::File.absolute_path(working_directory)
31
+ initialize_contents
32
+ end
33
+
34
+ # @see SourceInput#resolve_next
35
+ # @param [Source::Reference] reference
36
+ # @return [SourceInput]
37
+ def resolve_next(reference)
38
+ ResolveNext.call(reference,
39
+ self,
40
+ base_url: base_url,
41
+ working_directory: working_directory)
42
+ end
43
+
44
+ # @see SourceInput#other
45
+ # @param [SourceInput] other
46
+ # @return [Boolean]
47
+ def ==(other)
48
+ return false unless other.instance_of?(self.class)
49
+ raw_input == other.raw_input &&
50
+ base_url == other.base_url &&
51
+ working_directory == other.working_directory
52
+ end
53
+
54
+ # return [String]
55
+ def inspect
56
+ %{#{self.class.name}(input: #{raw_input.inspect}, base_url: } +
57
+ %{#{base_url}, working_directory: #{working_directory})}
58
+ end
59
+
60
+ # @return [String]
61
+ def to_s
62
+ raw_input.to_s
63
+ end
64
+
65
+ private
66
+
67
+ def resolve_working_directory
68
+ if raw_input.respond_to?(:path)
69
+ ::File.dirname(raw_input)
70
+ else
71
+ Dir.pwd
72
+ end
73
+ end
74
+
75
+ def parse_contents
76
+ return raw_input if raw_input.respond_to?(:keys)
77
+ StringParser.call(
78
+ input_to_string(raw_input),
79
+ raw_input.respond_to?(:path) ? ::File.basename(raw_input.path) : nil
80
+ )
81
+ end
82
+
83
+ def input_to_string(input)
84
+ return input.read if input.respond_to?(:read)
85
+ return input.to_s if input.respond_to?(:to_s)
86
+ input
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openapi3_parser/source_input/file"
4
+ require "openapi3_parser/source_input/url"
5
+
6
+ module Openapi3Parser
7
+ class SourceInput
8
+ class ResolveNext
9
+ # @param reference [Source::Reference]
10
+ # @param current_source_input [SourceInput]
11
+ # @param base_url [String, nil]
12
+ # @param working_directory [String, nil]
13
+ # @return [SourceInput]
14
+ def self.call(reference,
15
+ current_source_input,
16
+ base_url: nil,
17
+ working_directory: nil)
18
+ new(reference, current_source_input, base_url, working_directory)
19
+ .source_input
20
+ end
21
+
22
+ def initialize(reference,
23
+ current_source_input,
24
+ base_url,
25
+ working_directory)
26
+ @reference = reference
27
+ @current_source_input = current_source_input
28
+ @base_url = base_url
29
+ @working_directory = working_directory
30
+ end
31
+
32
+ private_class_method :new
33
+
34
+ def source_input
35
+ return current_source_input if reference.only_fragment?
36
+ if reference.absolute?
37
+ SourceInput::Url.new(reference.resource_uri)
38
+ else
39
+ base_url ? url_source_input : file_source_input
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :reference, :current_source_input, :base_url,
46
+ :working_directory
47
+
48
+ def url_source_input
49
+ url = URI.join(base_url, reference.resource_uri)
50
+ SourceInput::Url.new(url)
51
+ end
52
+
53
+ def file_source_input
54
+ path = reference.resource_uri.path
55
+ return SourceInput::File.new(path) if path[0] == "/"
56
+
57
+ expanded_path = ::File.expand_path(path, working_directory)
58
+ SourceInput::File.new(expanded_path)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "json"
5
+
6
+ module Openapi3Parser
7
+ class SourceInput
8
+ class StringParser
9
+ def self.call(input, filename = nil)
10
+ new(input, filename).call
11
+ end
12
+
13
+ def initialize(input, filename)
14
+ @input = input
15
+ @filename = filename
16
+ end
17
+
18
+ def call
19
+ json? ? parse_json : parse_yaml
20
+ end
21
+
22
+ private_class_method :new
23
+
24
+ private
25
+
26
+ attr_reader :input, :filename
27
+
28
+ def json?
29
+ return false if filename && ::File.extname(filename) == ".yaml"
30
+ json_filename = filename && ::File.extname(filename) == ".json"
31
+ json_filename || input.strip[0] == "{"
32
+ end
33
+
34
+ def parse_json
35
+ JSON.parse(input)
36
+ end
37
+
38
+ def parse_yaml
39
+ YAML.safe_load(input, [], [], true)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open-uri"
4
+ require "openapi3_parser/source_input"
5
+ require "openapi3_parser/source_input/string_parser"
6
+ require "openapi3_parser/source_input/resolve_next"
7
+ require "openapi3_parser/error"
8
+
9
+ module Openapi3Parser
10
+ class SourceInput
11
+ # An input of a URL to an OpenAPI file
12
+ #
13
+ # @attr_reader [String] request_url The URL that will be requested
14
+ # @attr_reader [String] resolved_url The eventual URL of the file that was
15
+ # accessed, this may differ to the
16
+ # request_url in the case of redirects
17
+ class Url < SourceInput
18
+ attr_reader :request_url, :resolved_url
19
+
20
+ # @param [String, URI] request_url
21
+ def initialize(request_url)
22
+ @request_url = request_url.to_s
23
+ initialize_contents
24
+ end
25
+
26
+ # @see SourceInput#resolve_next
27
+ # @param [Source::Reference] reference
28
+ # @return [SourceInput]
29
+ def resolve_next(reference)
30
+ ResolveNext.call(reference, self, base_url: resolved_url)
31
+ end
32
+
33
+ # @see SourceInput#other
34
+ # @param [SourceInput] other
35
+ # @return [Boolean]
36
+ def ==(other)
37
+ return false unless other.instance_of?(self.class)
38
+ [request_url, resolved_url].include?(other.request_url) ||
39
+ [request_url, resolved_url].include?(other.resolved_url)
40
+ end
41
+
42
+ # return [String]
43
+ def url
44
+ resolved_url || request_url
45
+ end
46
+
47
+ # return [String]
48
+ def inspect
49
+ %{#{self.class.name}(url: #{url})}
50
+ end
51
+
52
+ # @return [String]
53
+ def to_s
54
+ request_url
55
+ end
56
+
57
+ # @return [String]
58
+ def relative_to(source_input)
59
+ other_url = if source_input.respond_to?(:url)
60
+ source_input.url
61
+ elsif source_input.respond_to?(:base_url)
62
+ source_input.base_url
63
+ end
64
+
65
+ return url unless other_url
66
+
67
+ other_url ? RelativeUrlDifference.call(other_url, url) : url
68
+ end
69
+
70
+ private
71
+
72
+ def parse_contents
73
+ begin
74
+ file = URI.parse(request_url).open
75
+ rescue ::StandardError => e
76
+ @access_error = Error::InaccessibleInput.new(e.message)
77
+ return
78
+ end
79
+ @resolved_url = file.base_uri.to_s
80
+ StringParser.call(file.read, resolved_url)
81
+ end
82
+
83
+ class RelativeUrlDifference
84
+ def initialize(from_url, to_url)
85
+ @from_uri = URI.parse(from_url)
86
+ @to_uri = URI.parse(to_url)
87
+ end
88
+
89
+ def self.call(from_url, to_url)
90
+ new(from_url, to_url).call
91
+ end
92
+
93
+ def call
94
+ return to_uri.to_s if different_hosts?
95
+ relative_path
96
+ end
97
+
98
+ private_class_method :new
99
+
100
+ private
101
+
102
+ attr_reader :from_uri, :to_uri
103
+
104
+ def different_hosts?
105
+ URI.join(from_uri, "/") != URI.join(to_uri, "/")
106
+ end
107
+
108
+ def relative_path
109
+ relative = to_uri.route_from(from_uri)
110
+ return relative.to_s unless relative.path.empty?
111
+
112
+ # if we have same path it's nice to show just the filename
113
+ file_and_query(to_uri)
114
+ end
115
+
116
+ def file_and_query(uri)
117
+ Pathname.new(uri.path).basename.to_s +
118
+ (uri.query ? "?#{uri.query}" : "")
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -1,13 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  module Openapi3Parser
4
6
  module Validation
7
+ # Represents a validation error for an OpenAPI document
8
+ # @attr_reader [String] message The error message
9
+ # @attr_reader [Context] context The context where this was
10
+ # validated
11
+ # @attr_reader [Class, nil] factory_class The NodeFactory that was being
12
+ # created when this error was found
5
13
  class Error
6
- attr_reader :namespace, :message
14
+ extend Forwardable
15
+
16
+ attr_reader :message, :context, :factory_class
17
+
18
+ # @!method source_location
19
+ # The source file and pointer for where this error occurred
20
+ # @return [Context::Location]
21
+ def_delegator :context, :source_location
22
+
23
+ alias to_s message
7
24
 
8
- def initialize(namespace, message)
9
- @namespace = namespace
25
+ # @param [String] message
26
+ # @param [Context] context
27
+ # @param [Class, nil] factory_class
28
+ def initialize(message, context, factory_class = nil)
10
29
  @message = message
30
+ @context = context
31
+ @factory_class = factory_class
32
+ end
33
+
34
+ # @return [String, nil]
35
+ def for_type
36
+ return unless factory_class
37
+ factory_class.name.split("::").last
38
+ end
39
+
40
+ # @return [String]
41
+ def inspect
42
+ "#{self.class.name}(message: #{message}, context: #{context}, " \
43
+ "for_type: #{for_type})"
11
44
  end
12
45
  end
13
46
  end