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