open_api-loader 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +15 -0
  3. data/.gitignore +13 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +28 -0
  6. data/.travis.yml +24 -0
  7. data/CHANGELOG.md +10 -0
  8. data/Gemfile +8 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +99 -0
  11. data/Rakefile +10 -0
  12. data/bin/console +6 -0
  13. data/bin/setup +6 -0
  14. data/lib/open_api-loader.rb +1 -0
  15. data/lib/open_api/loader.rb +44 -0
  16. data/lib/open_api/loader/collector.rb +61 -0
  17. data/lib/open_api/loader/denormalizer.rb +27 -0
  18. data/lib/open_api/loader/denormalizer/parameters.rb +46 -0
  19. data/lib/open_api/loader/denormalizer/security.rb +35 -0
  20. data/lib/open_api/loader/denormalizer/servers.rb +36 -0
  21. data/lib/open_api/loader/denormalizer/variables.rb +54 -0
  22. data/lib/open_api/loader/reader.rb +47 -0
  23. data/lib/open_api/loader/ref.rb +85 -0
  24. data/lib/open_api/loader/translator.rb +50 -0
  25. data/lib/open_api/loader/translator/clean_definitions.rb +16 -0
  26. data/lib/open_api/loader/translator/convert_bodies.rb +77 -0
  27. data/lib/open_api/loader/translator/convert_forms.rb +71 -0
  28. data/lib/open_api/loader/translator/convert_parameters.rb +135 -0
  29. data/lib/open_api/loader/translator/convert_responses.rb +63 -0
  30. data/lib/open_api/loader/translator/convert_security_schemes.rb +49 -0
  31. data/lib/open_api/loader/translator/convert_servers.rb +46 -0
  32. data/lib/open_api/loader/translator/convert_version.rb +12 -0
  33. data/lib/open_api/loader/translator/denormalize_consumes.rb +44 -0
  34. data/lib/open_api/loader/translator/denormalize_parameters.rb +55 -0
  35. data/lib/open_api/loader/translator/denormalize_produces.rb +53 -0
  36. data/open_api-loader.gemspec +25 -0
  37. data/spec/fixtures/oas2/collected.yaml +1012 -0
  38. data/spec/fixtures/oas2/denormalized.yaml +1462 -0
  39. data/spec/fixtures/oas2/loaded.yaml +564 -0
  40. data/spec/fixtures/oas2/models.yaml +118 -0
  41. data/spec/fixtures/oas2/source.json +1 -0
  42. data/spec/fixtures/oas2/source.yaml +569 -0
  43. data/spec/fixtures/oas2/translated.yaml +1396 -0
  44. data/spec/fixtures/oas3/collected.yaml +233 -0
  45. data/spec/fixtures/oas3/denormalized.yaml +233 -0
  46. data/spec/fixtures/oas3/source.json +1 -0
  47. data/spec/fixtures/oas3/source.yaml +217 -0
  48. data/spec/loader_spec.rb +53 -0
  49. data/spec/spec_helper.rb +17 -0
  50. data/spec/support/fixture_helpers.rb +18 -0
  51. data/spec/support/path_helpers.rb +27 -0
  52. data/spec/unit/collector_spec.rb +31 -0
  53. data/spec/unit/denormalizer_spec.rb +17 -0
  54. data/spec/unit/reader_spec.rb +53 -0
  55. data/spec/unit/ref_spec.rb +107 -0
  56. data/spec/unit/translator_spec.rb +15 -0
  57. metadata +232 -0
@@ -0,0 +1,35 @@
1
+ class OpenAPI::Loader::Denormalizer
2
+ #
3
+ # Denormalizes all the 'security' definitions
4
+ # by moving them from the root OpenAPI object
5
+ # right into the corresponding operation objects.
6
+ #
7
+ # @private
8
+ #
9
+ class Security < SimpleDelegator
10
+ def call
11
+ default = delete "security"
12
+ operations.each do |operation|
13
+ operation["security"] ||= default if default
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def paths
20
+ Enumerator.new do |yielder|
21
+ fetch("paths", {}).each_value do |path|
22
+ yielder << path if path.is_a? Hash
23
+ end
24
+ end
25
+ end
26
+
27
+ def operations
28
+ Enumerator.new do |yielder|
29
+ paths.each do |path|
30
+ path.each_value { |item| yielder << item if item.is_a? Hash }
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,36 @@
1
+ class OpenAPI::Loader::Denormalizer
2
+ #
3
+ # Denormalizes all the 'servers' definitions
4
+ # by moving them from the root OpenAPI object and path objects
5
+ # right into the corresponding operation objects.
6
+ #
7
+ # @private
8
+ #
9
+ class Servers < SimpleDelegator
10
+ def call
11
+ root_default = delete "servers"
12
+ paths.each do |path|
13
+ default = path.delete("servers") || root_default
14
+ operations(path).each do |operation|
15
+ operation["servers"] ||= default if default
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def paths
23
+ Enumerator.new do |yielder|
24
+ fetch("paths", {}).each_value do |path|
25
+ yielder << path if path.is_a? Hash
26
+ end
27
+ end
28
+ end
29
+
30
+ def operations(path)
31
+ Enumerator.new do |yielder|
32
+ path.each_value { |item| yielder << item if item.is_a? Hash }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,54 @@
1
+ class OpenAPI::Loader::Denormalizer
2
+ #
3
+ # Inserts server variables into server urls in all operations
4
+ #
5
+ # @private
6
+ #
7
+ class Variables < SimpleDelegator
8
+ def call
9
+ operations.each { |operation| convert(operation) }
10
+ end
11
+
12
+ private
13
+
14
+ def paths
15
+ Enumerator.new do |yielder|
16
+ fetch("paths", {}).each_value do |path|
17
+ yielder << path if path.is_a? Hash
18
+ end
19
+ end
20
+ end
21
+
22
+ def operations
23
+ Enumerator.new do |yielder|
24
+ paths.each do |path|
25
+ path.each_value { |item| yielder << item if item.is_a? Hash }
26
+ end
27
+ end
28
+ end
29
+
30
+ def convert(operation)
31
+ servers = operation.delete("servers")
32
+ return unless servers.is_a? Array
33
+ operation["servers"] = servers.flat_map { |server| substitute(server) }
34
+ end
35
+
36
+ def substitute(server)
37
+ url, variables = server.values_at "url", "variables"
38
+ combinations(url, variables).map { |url| { "url" => url } }
39
+ end
40
+
41
+ def combinations(url, variables)
42
+ return [url] unless variables.is_a? Hash
43
+
44
+ variables.inject([url]) do |urls, (key, var)|
45
+ enum = var["enum"]
46
+ if enum.is_a?(Array) && enum.any?
47
+ urls.flat_map { |url| enum.map { |val| url.gsub("{#{key}}", val) } }
48
+ else
49
+ urls
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,47 @@
1
+ module OpenAPI::Loader
2
+ #
3
+ # Loads data from [#source] file and strinfies all its keys
4
+ # @private
5
+ #
6
+ class Reader
7
+ extend Dry::Initializer
8
+ extend ConstructorShortcut[:call] # class-level .call
9
+
10
+ param :source
11
+
12
+ def call
13
+ stringify_keys(try_json || try_yaml)
14
+ end
15
+
16
+ private
17
+
18
+ def raw
19
+ @raw ||= begin
20
+ uri = URI source
21
+ path = Pathname source
22
+ uri.absolute? ? Net::HTTP.get(uri) : File.read(path)
23
+ end
24
+ end
25
+
26
+ def stringify_keys(data)
27
+ case data
28
+ when Hash
29
+ data.each_with_object({}) { |(k, v), o| o[k.to_s] = stringify_keys(v) }
30
+ when Array then data.map { |item| stringify_keys(item) }
31
+ else data
32
+ end
33
+ end
34
+
35
+ def try_json
36
+ JSON.load raw
37
+ rescue JSON::ParserError
38
+ nil
39
+ end
40
+
41
+ def try_yaml
42
+ YAML.load raw
43
+ rescue Psych::SyntaxError
44
+ nil
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,85 @@
1
+ module OpenAPI::Loader
2
+ #
3
+ # Describes json pointer used in "$ref"-erences
4
+ # @see RFC-6901 https://tools.ietf.org/html/rfc6901
5
+ #
6
+ # @private
7
+ #
8
+ class Ref < String
9
+ #
10
+ # Builds the pointer from a path
11
+ #
12
+ # @param [Array<#to_s>, #to_s] path
13
+ # @return [OpenAPI::Ref]
14
+ #
15
+ def self.[](*path)
16
+ path.flatten.compact.each_with_object("#") do |key, obj|
17
+ obj << "/#{key.to_s.gsub('~', '~0').gsub('/', '~1')}"
18
+ end
19
+ end
20
+
21
+ #
22
+ # The URI to the remote document
23
+ # @return [URI, nil]
24
+ #
25
+ def uri
26
+ URI split("#").first unless start_with? "#"
27
+ end
28
+
29
+ #
30
+ # The local pointer to the json
31
+ # @return [Array<String>]
32
+ #
33
+ def path
34
+ return [] if end_with? "#"
35
+ split(%r{#/?}).last.to_s.split("/").map do |item|
36
+ PARSER.unescape(item).gsub("~1", "/").gsub("~0", "~")
37
+ end
38
+ end
39
+
40
+ #
41
+ # Extracs referred value from given source by local path (after #)
42
+ #
43
+ # @param [Hash]
44
+ # @return [Object]
45
+ #
46
+ def fetch_from(data)
47
+ read(data, [], *path)
48
+ end
49
+
50
+ private
51
+
52
+ PARSER = URI::Parser.new.freeze
53
+
54
+ def initialize(source)
55
+ source = "#{source}#" if source.count("#").zero?
56
+ super(source)
57
+ return if count("#") == 1 && (end_with?("#") || self["#/"])
58
+ raise ArgumentError, "Invalid reference '#{source}'"
59
+ end
60
+
61
+ def read(data, head, key = nil, *rest)
62
+ return data unless key
63
+ case data
64
+ when Array then read_array(data, head, key, rest)
65
+ when Hash then read_hash(data, head, key, rest)
66
+ else raise_error(*head, key)
67
+ end
68
+ end
69
+
70
+ def read_array(data, head, key, rest)
71
+ int = key.to_i
72
+ raise_error(*head, key) unless int.to_s == key.to_s && data.count > int
73
+ read(data[int], [*head, key], *rest)
74
+ end
75
+
76
+ def read_hash(data, head, key, rest)
77
+ raise_error(*head, key) unless data.key? key
78
+ read(data[key], [*head, key], *rest)
79
+ end
80
+
81
+ def raise_error(*path)
82
+ raise KeyError, "Cannot find value by reference '#{self.class[*path]}'"
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,50 @@
1
+ module OpenAPI::Loader
2
+ #
3
+ # Translates OAS2 to OAS3 specification
4
+ # @private
5
+ #
6
+ class Translator
7
+ extend Dry::Initializer
8
+ extend ConstructorShortcut[:call] # class-level .call
9
+
10
+ param :source
11
+
12
+ def call
13
+ return source unless oas2?
14
+ WRAPPERS.each { |wrapper| wrapper.new(source).call }
15
+ source
16
+ end
17
+
18
+ private
19
+
20
+ def oas2?
21
+ source.is_a?(Hash) && source["swagger"].to_s.start_with?("2")
22
+ end
23
+
24
+ require_relative "translator/clean_definitions"
25
+ require_relative "translator/convert_bodies"
26
+ require_relative "translator/convert_forms"
27
+ require_relative "translator/convert_parameters"
28
+ require_relative "translator/convert_responses"
29
+ require_relative "translator/convert_security_schemes"
30
+ require_relative "translator/convert_servers"
31
+ require_relative "translator/convert_version"
32
+ require_relative "translator/denormalize_consumes"
33
+ require_relative "translator/denormalize_parameters"
34
+ require_relative "translator/denormalize_produces"
35
+
36
+ WRAPPERS = [
37
+ CleanDefinitions,
38
+ DenormalizeParameters,
39
+ DenormalizeConsumes,
40
+ DenormalizeProduces,
41
+ ConvertForms,
42
+ ConvertBodies,
43
+ ConvertParameters,
44
+ ConvertResponses,
45
+ ConvertServers,
46
+ ConvertSecuritySchemes,
47
+ ConvertVersion
48
+ ].freeze
49
+ end
50
+ end
@@ -0,0 +1,16 @@
1
+ class OpenAPI::Loader::Translator
2
+ #
3
+ # Cleans definitions that aren't in use any more, because all references
4
+ # to them were expanded by [OpenAPI::Loader::Collector].
5
+ #
6
+ # @private
7
+ #
8
+ class CleanDefinitions < SimpleDelegator
9
+ def call
10
+ KEYS.each { |key| delete(key) }
11
+ end
12
+
13
+ # Keys to be dropped
14
+ KEYS = %w[definitions parameters responses].freeze
15
+ end
16
+ end
@@ -0,0 +1,77 @@
1
+ class OpenAPI::Loader::Translator
2
+ #
3
+ # Builds 'requestBody' from 'consumes' and 'body' parameters
4
+ #
5
+ # @private
6
+ #
7
+ class ConvertBodies < SimpleDelegator
8
+ def call
9
+ operations.each { |item| update_body(item) }
10
+ end
11
+
12
+ private
13
+
14
+ FORMS = %w[application/x-www-form-urlencoded multipart/form-data].freeze
15
+
16
+ def paths
17
+ Enumerator.new do |yielder|
18
+ fetch("paths", {}).each_value do |path|
19
+ yielder << path if path.is_a? Hash
20
+ end
21
+ end
22
+ end
23
+
24
+ def operations
25
+ Enumerator.new do |yielder|
26
+ paths.each do |path|
27
+ path.each_value { |item| yielder << item if item.is_a? Hash }
28
+ end
29
+ end
30
+ end
31
+
32
+ def update_body(operation)
33
+ data, body = body_data(operation)
34
+ return unless data
35
+
36
+ body_encodings(operation).each do |encoding|
37
+ schema = encoding["/xml"] ? data : drop_xml(data)
38
+ operation["requestBody"] ||= Hash(body)
39
+ operation["requestBody"]["content"] ||= {}
40
+ operation["requestBody"]["content"][encoding] = { "schema" => schema }
41
+ end
42
+ end
43
+
44
+ def body_encodings(operation)
45
+ encodings = Array operation.delete("consumes")
46
+ forms, others = encodings.partition { |item| FORMS.include? item }
47
+ operation["consumes"] = forms if forms.any?
48
+ others
49
+ end
50
+
51
+ def body_data(operation)
52
+ param = body_params(operation).first
53
+ return unless param.is_a?(Hash)
54
+ schema = param.delete("schema")
55
+ content = param.select { |key, _| %w[description required].include? key }
56
+ [schema, content]
57
+ end
58
+
59
+ def body_params(operation)
60
+ params = Array operation.delete("parameters")
61
+ bodies, others = params.partition { |item| item["in"] == "body" }
62
+ operation["parameters"] = others if others.any?
63
+ bodies
64
+ end
65
+
66
+ def drop_xml(data)
67
+ case data
68
+ when Hash
69
+ data.each_with_object({}) do |(key, val), obj|
70
+ obj[key] = drop_xml(val) unless key == "xml"
71
+ end
72
+ when Array then data.map { |item| drop_xml(item) }
73
+ else data
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,71 @@
1
+ class OpenAPI::Loader::Translator
2
+ #
3
+ # Builds 'requestBody' from 'consumes' and 'formData' parameters
4
+ #
5
+ # @private
6
+ #
7
+ class ConvertForms < SimpleDelegator
8
+ def call
9
+ operations.each { |item| update_body(item) }
10
+ end
11
+
12
+ private
13
+
14
+ FORMS = %w[application/x-www-form-urlencoded multipart/form-data].freeze
15
+
16
+ def paths
17
+ Enumerator.new do |yielder|
18
+ fetch("paths", {}).each_value do |path|
19
+ yielder << path if path.is_a? Hash
20
+ end
21
+ end
22
+ end
23
+
24
+ def operations
25
+ Enumerator.new do |yielder|
26
+ paths.each do |path|
27
+ path.each_value { |item| yielder << item if item.is_a? Hash }
28
+ end
29
+ end
30
+ end
31
+
32
+ def update_body(operation)
33
+ schema = form_schema(operation)
34
+ form_encodings(operation).each do |encoding|
35
+ operation["requestBody"] ||= {}
36
+ operation["requestBody"]["content"] ||= {}
37
+ operation["requestBody"]["content"][encoding] = { "schema" => schema }
38
+ end
39
+ end
40
+
41
+ def form_encodings(operation)
42
+ encodings = Array operation.delete("consumes")
43
+ forms, others = encodings.partition { |item| FORMS.include? item }
44
+ operation["consumes"] = others if others.any?
45
+ forms
46
+ end
47
+
48
+ def form_schema(operation)
49
+ properties = form_properties(operation)
50
+ schema = { "type" => "object" }
51
+ schema["properties"] = properties if properties.any?
52
+ schema
53
+ end
54
+
55
+ def form_properties(operation)
56
+ form_params(operation).each_with_object({}) do |item, obj|
57
+ name = item["name"]
58
+ data = item.reject { |key| %w[in name].include? key }
59
+ data["type"] = "string" if data["type"] == "file"
60
+ obj[name] = data
61
+ end
62
+ end
63
+
64
+ def form_params(operation)
65
+ params = Array operation.delete("parameters")
66
+ forms, others = params.partition { |item| item["in"] == "formData" }
67
+ operation["parameters"] = others if others.any?
68
+ forms
69
+ end
70
+ end
71
+ end