open_api-loader 0.0.1

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