openapi3_parser 0.1.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 (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