openapi3_parser 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +9 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +4 -0
  7. data/Gemfile +5 -0
  8. data/LICENCE +19 -0
  9. data/README.md +32 -0
  10. data/Rakefile +10 -0
  11. data/TODO.md +30 -0
  12. data/lib/openapi3_parser.rb +39 -0
  13. data/lib/openapi3_parser/context.rb +54 -0
  14. data/lib/openapi3_parser/document.rb +48 -0
  15. data/lib/openapi3_parser/error.rb +5 -0
  16. data/lib/openapi3_parser/fields/map.rb +83 -0
  17. data/lib/openapi3_parser/node.rb +115 -0
  18. data/lib/openapi3_parser/node/map.rb +24 -0
  19. data/lib/openapi3_parser/node/object.rb +28 -0
  20. data/lib/openapi3_parser/node_factories/array.rb +105 -0
  21. data/lib/openapi3_parser/node_factories/callback.rb +26 -0
  22. data/lib/openapi3_parser/node_factories/components.rb +83 -0
  23. data/lib/openapi3_parser/node_factories/contact.rb +24 -0
  24. data/lib/openapi3_parser/node_factories/discriminator.rb +31 -0
  25. data/lib/openapi3_parser/node_factories/encoding.rb +34 -0
  26. data/lib/openapi3_parser/node_factories/example.rb +25 -0
  27. data/lib/openapi3_parser/node_factories/external_documentation.rb +23 -0
  28. data/lib/openapi3_parser/node_factories/header.rb +36 -0
  29. data/lib/openapi3_parser/node_factories/info.rb +28 -0
  30. data/lib/openapi3_parser/node_factories/license.rb +22 -0
  31. data/lib/openapi3_parser/node_factories/link.rb +40 -0
  32. data/lib/openapi3_parser/node_factories/map.rb +110 -0
  33. data/lib/openapi3_parser/node_factories/media_type.rb +44 -0
  34. data/lib/openapi3_parser/node_factories/oauth_flow.rb +24 -0
  35. data/lib/openapi3_parser/node_factories/oauth_flows.rb +31 -0
  36. data/lib/openapi3_parser/node_factories/openapi.rb +50 -0
  37. data/lib/openapi3_parser/node_factories/operation.rb +76 -0
  38. data/lib/openapi3_parser/node_factories/parameter.rb +43 -0
  39. data/lib/openapi3_parser/node_factories/parameter/parameter_like.rb +37 -0
  40. data/lib/openapi3_parser/node_factories/path_item.rb +75 -0
  41. data/lib/openapi3_parser/node_factories/paths.rb +32 -0
  42. data/lib/openapi3_parser/node_factories/reference.rb +33 -0
  43. data/lib/openapi3_parser/node_factories/request_body.rb +31 -0
  44. data/lib/openapi3_parser/node_factories/response.rb +45 -0
  45. data/lib/openapi3_parser/node_factories/responses.rb +32 -0
  46. data/lib/openapi3_parser/node_factories/schema.rb +106 -0
  47. data/lib/openapi3_parser/node_factories/security_requirement.rb +25 -0
  48. data/lib/openapi3_parser/node_factories/security_scheme.rb +35 -0
  49. data/lib/openapi3_parser/node_factories/server.rb +32 -0
  50. data/lib/openapi3_parser/node_factories/server_variable.rb +28 -0
  51. data/lib/openapi3_parser/node_factories/tag.rb +24 -0
  52. data/lib/openapi3_parser/node_factories/xml.rb +25 -0
  53. data/lib/openapi3_parser/node_factory.rb +126 -0
  54. data/lib/openapi3_parser/node_factory/field_config.rb +88 -0
  55. data/lib/openapi3_parser/node_factory/map.rb +39 -0
  56. data/lib/openapi3_parser/node_factory/object.rb +80 -0
  57. data/lib/openapi3_parser/node_factory/object/node_builder.rb +85 -0
  58. data/lib/openapi3_parser/node_factory/object/validator.rb +102 -0
  59. data/lib/openapi3_parser/node_factory/optional_reference.rb +23 -0
  60. data/lib/openapi3_parser/nodes/array.rb +26 -0
  61. data/lib/openapi3_parser/nodes/callback.rb +11 -0
  62. data/lib/openapi3_parser/nodes/components.rb +47 -0
  63. data/lib/openapi3_parser/nodes/contact.rb +23 -0
  64. data/lib/openapi3_parser/nodes/discriminator.rb +19 -0
  65. data/lib/openapi3_parser/nodes/encoding.rb +31 -0
  66. data/lib/openapi3_parser/nodes/example.rb +27 -0
  67. data/lib/openapi3_parser/nodes/external_documentation.rb +19 -0
  68. data/lib/openapi3_parser/nodes/header.rb +13 -0
  69. data/lib/openapi3_parser/nodes/info.rb +35 -0
  70. data/lib/openapi3_parser/nodes/license.rb +19 -0
  71. data/lib/openapi3_parser/nodes/link.rb +35 -0
  72. data/lib/openapi3_parser/nodes/map.rb +11 -0
  73. data/lib/openapi3_parser/nodes/media_type.rb +27 -0
  74. data/lib/openapi3_parser/nodes/oauth_flow.rb +27 -0
  75. data/lib/openapi3_parser/nodes/oauth_flows.rb +27 -0
  76. data/lib/openapi3_parser/nodes/openapi.rb +44 -0
  77. data/lib/openapi3_parser/nodes/operation.rb +59 -0
  78. data/lib/openapi3_parser/nodes/parameter.rb +21 -0
  79. data/lib/openapi3_parser/nodes/parameter/parameter_like.rb +53 -0
  80. data/lib/openapi3_parser/nodes/path_item.rb +59 -0
  81. data/lib/openapi3_parser/nodes/paths.rb +11 -0
  82. data/lib/openapi3_parser/nodes/request_body.rb +23 -0
  83. data/lib/openapi3_parser/nodes/response.rb +27 -0
  84. data/lib/openapi3_parser/nodes/responses.rb +15 -0
  85. data/lib/openapi3_parser/nodes/schema.rb +159 -0
  86. data/lib/openapi3_parser/nodes/security_requirement.rb +11 -0
  87. data/lib/openapi3_parser/nodes/security_scheme.rb +44 -0
  88. data/lib/openapi3_parser/nodes/server.rb +25 -0
  89. data/lib/openapi3_parser/nodes/server_variable.rb +23 -0
  90. data/lib/openapi3_parser/nodes/tag.rb +23 -0
  91. data/lib/openapi3_parser/nodes/xml.rb +31 -0
  92. data/lib/openapi3_parser/validation/error.rb +14 -0
  93. data/lib/openapi3_parser/validation/error_collection.rb +36 -0
  94. data/lib/openapi3_parser/version.rb +5 -0
  95. data/openapi3_parser.gemspec +28 -0
  96. metadata +208 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3c7e2585cb3bbdbda459519eb7e264dfd89f5745
4
+ data.tar.gz: 4fdbb4b547e626e1b0ba5f502b93107638af3655
5
+ SHA512:
6
+ metadata.gz: 498d2043a338970c66ef9d14590adb34ec2f7b65e751985f8d12a8c9506d63e09a9840e7521b00a953000af1e286917147fb4507be29f68a5c047aa9f124f7d8
7
+ data.tar.gz: 7fb4d6b168b514fa01056fa5be0026551ad02e0e47506e99516b14b290e450c0940031e82fc38bdc0f4c5d137d98d3a621f28d42bd6e5923f335a7c31255796e
@@ -0,0 +1 @@
1
+ /Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
@@ -0,0 +1,9 @@
1
+ Style/StringLiterals:
2
+ EnforcedStyle: double_quotes
3
+ Metrics/MethodLength:
4
+ Max: 30
5
+ Documentation:
6
+ Enabled: false
7
+ Metrics/BlockLength:
8
+ Exclude:
9
+ - 'spec/**/*.rb'
@@ -0,0 +1 @@
1
+ 2.3.1
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ sudo: false
3
+ rvm:
4
+ - 2.3.1
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
data/LICENCE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2017 Kevin Dew
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,32 @@
1
+ # OpenAPI 3 Parser
2
+
3
+ [![Build Status](https://travis-ci.org/kevindew/openapi_parser.svg?branch=master)](https://travis-ci.org/kevindew/openapi_parser)
4
+
5
+
6
+ This is a parser/validator for [Open API 3][openapi-3] built in Ruby.
7
+
8
+ Example usage:
9
+
10
+ ```
11
+ require "openapi3_parser"
12
+
13
+ document = Openapi3Parser.load_file("path/to/example.yaml")
14
+
15
+ # check whether document is valid
16
+ document.valid?
17
+
18
+ # traverse document
19
+ document.paths["/"]
20
+ ```
21
+
22
+ [openapi-3]: https://github.com/OAI/OpenAPI-Specification
23
+
24
+ ## Status
25
+
26
+ This is currently a work in progress and will remain so until it reaches 1.0.
27
+
28
+ See [TODO](TODO.md) for details of the roadmap there.
29
+
30
+ ## Licence
31
+
32
+ [MIT License](LICENCE)
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "rubocop/rake_task"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %I[rubocop spec]
data/TODO.md ADDED
@@ -0,0 +1,30 @@
1
+ # Todo
2
+
3
+ These are the steps defined to reach 1.0. Assistance is very welcome.
4
+
5
+ - [ ] Handle mutually exclusive fields
6
+ - [ ] Refactor the various NodeFactory modules to be a less confusing
7
+ hierachical structure. Consider having factories subclass instead of use
8
+ mixin
9
+ - [ ] Decouple Document class for the source file. Consider a source file class
10
+ instead
11
+ - [ ] Validate that a reference creates the type of node that is expected in
12
+ a context
13
+ - [ ] Allow opening of references from additional files
14
+ - [ ] Allow opening of openapi documents by URL
15
+ - [ ] Support references by URL, consider option to limit behaviour
16
+ - [ ] Support converting CommonMark to HTML
17
+ - [ ] Reach parity with OpenAPI specification for validation
18
+ - [ ] Consider a lenient mode for a document to only have to comply with type
19
+ based validation
20
+ - [ ] Improve test coverage
21
+ - [ ] Publish documentation of the interface through the structure
22
+ - [ ] Consider a resolved context class for representing context with a node
23
+ that can handle scenarios where a node is represented by both a reference
24
+ and resolved context
25
+ - [ ] Create error classes for various scenarios
26
+ - [ ] Associate/resolve operation id / operation references
27
+ - [ ] Do something to model expressions
28
+ - [ ] Improve the modelling of namespace
29
+ - [ ] Set up nicer string representations of key classes to help them be
30
+ debugged
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openapi3_parser/error"
4
+ require "openapi3_parser/document"
5
+
6
+ require "yaml"
7
+ require "json"
8
+
9
+ module Openapi3Parser
10
+ def self.load(input)
11
+ # working_directory ||= if input.respond_to?(:read)
12
+ # File.dirname(input)
13
+ # else
14
+ # Dir.pwd
15
+ # end
16
+
17
+ Document.new(parse_input(input))
18
+ end
19
+
20
+ def self.load_file(path)
21
+ file = File.open(path)
22
+ load(file)
23
+ end
24
+
25
+ def self.parse_input(input)
26
+ return input if input.respond_to?(:keys)
27
+
28
+ extension = input.respond_to?(:extname) ? input.extname : nil
29
+ contents = input.respond_to?(:read) ? input.read : input
30
+
31
+ if extension == ".json" || contents.strip[0] == "{"
32
+ JSON.parse(contents)
33
+ else
34
+ YAML.safe_load(contents, [], [], true)
35
+ end
36
+ end
37
+
38
+ private_class_method :parse_input
39
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openapi3Parser
4
+ class Context
5
+ attr_reader :input, :namespace, :document, :parent
6
+
7
+ def initialize(input:, namespace: [], document:, parent: nil)
8
+ @input = input
9
+ @namespace = namespace.freeze
10
+ @document = document
11
+ @parent = parent
12
+ end
13
+
14
+ def self.root(input, document)
15
+ new(input: input, document: document)
16
+ end
17
+
18
+ def stringify_namespace
19
+ return "root" if namespace.empty?
20
+ namespace
21
+ .map { |i| i.include?("/") ? %("#{i}") : i }
22
+ .join("/")
23
+ end
24
+
25
+ def next_namespace(segment, next_input = nil)
26
+ next_input ||= input[segment]
27
+ self.class.new(
28
+ input: next_input,
29
+ namespace: namespace + [segment],
30
+ document: document,
31
+ parent: self
32
+ )
33
+ end
34
+
35
+ def resolve_reference
36
+ document.resolve_reference(input["$ref"]) do |resolved_input, namespace|
37
+ # @TODO track reference for cyclic depenendies
38
+ next_context = resolved_reference(resolved_input, namespace)
39
+ yield(next_context)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def resolved_reference(input, namespace)
46
+ self.class.new(
47
+ input: input,
48
+ namespace: namespace,
49
+ document: document,
50
+ parent: parent
51
+ )
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openapi3_parser/context"
4
+ require "openapi3_parser/error"
5
+ require "openapi3_parser/node_factories/openapi"
6
+
7
+ require "forwardable"
8
+
9
+ module Openapi3Parser
10
+ class Document
11
+ extend Forwardable
12
+
13
+ attr_reader :input
14
+
15
+ def_delegators :factory, :valid?, :errors
16
+ def_delegators :root, :openapi, :info, :servers, :paths, :components,
17
+ :security, :tags, :external_docs, :extension, :[], :each
18
+
19
+ def initialize(input)
20
+ @input = input
21
+ end
22
+
23
+ def root
24
+ factory.node
25
+ end
26
+
27
+ def resolve_reference(reference)
28
+ if reference[0..1] != "#/"
29
+ raise Error, "Only anchor references are currently supported"
30
+ end
31
+
32
+ parts = reference.split("/").drop(1).map do |field|
33
+ CGI.unescape(field.gsub("+", "%20"))
34
+ end
35
+
36
+ result = input.dig(*parts)
37
+ raise Error, "Could not resolve reference #{reference}" unless result
38
+
39
+ yield(result, parts)
40
+ end
41
+
42
+ private
43
+
44
+ def factory
45
+ @factory ||= NodeFactories::Openapi.new(Context.root(input, self))
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openapi3Parser
4
+ class Error < ::RuntimeError; end
5
+ end
@@ -0,0 +1,83 @@
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
@@ -0,0 +1,115 @@
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