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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +15 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +28 -0
- data/.travis.yml +24 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +99 -0
- data/Rakefile +10 -0
- data/bin/console +6 -0
- data/bin/setup +6 -0
- data/lib/open_api-loader.rb +1 -0
- data/lib/open_api/loader.rb +44 -0
- data/lib/open_api/loader/collector.rb +61 -0
- data/lib/open_api/loader/denormalizer.rb +27 -0
- data/lib/open_api/loader/denormalizer/parameters.rb +46 -0
- data/lib/open_api/loader/denormalizer/security.rb +35 -0
- data/lib/open_api/loader/denormalizer/servers.rb +36 -0
- data/lib/open_api/loader/denormalizer/variables.rb +54 -0
- data/lib/open_api/loader/reader.rb +47 -0
- data/lib/open_api/loader/ref.rb +85 -0
- data/lib/open_api/loader/translator.rb +50 -0
- data/lib/open_api/loader/translator/clean_definitions.rb +16 -0
- data/lib/open_api/loader/translator/convert_bodies.rb +77 -0
- data/lib/open_api/loader/translator/convert_forms.rb +71 -0
- data/lib/open_api/loader/translator/convert_parameters.rb +135 -0
- data/lib/open_api/loader/translator/convert_responses.rb +63 -0
- data/lib/open_api/loader/translator/convert_security_schemes.rb +49 -0
- data/lib/open_api/loader/translator/convert_servers.rb +46 -0
- data/lib/open_api/loader/translator/convert_version.rb +12 -0
- data/lib/open_api/loader/translator/denormalize_consumes.rb +44 -0
- data/lib/open_api/loader/translator/denormalize_parameters.rb +55 -0
- data/lib/open_api/loader/translator/denormalize_produces.rb +53 -0
- data/open_api-loader.gemspec +25 -0
- data/spec/fixtures/oas2/collected.yaml +1012 -0
- data/spec/fixtures/oas2/denormalized.yaml +1462 -0
- data/spec/fixtures/oas2/loaded.yaml +564 -0
- data/spec/fixtures/oas2/models.yaml +118 -0
- data/spec/fixtures/oas2/source.json +1 -0
- data/spec/fixtures/oas2/source.yaml +569 -0
- data/spec/fixtures/oas2/translated.yaml +1396 -0
- data/spec/fixtures/oas3/collected.yaml +233 -0
- data/spec/fixtures/oas3/denormalized.yaml +233 -0
- data/spec/fixtures/oas3/source.json +1 -0
- data/spec/fixtures/oas3/source.yaml +217 -0
- data/spec/loader_spec.rb +53 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/fixture_helpers.rb +18 -0
- data/spec/support/path_helpers.rb +27 -0
- data/spec/unit/collector_spec.rb +31 -0
- data/spec/unit/denormalizer_spec.rb +17 -0
- data/spec/unit/reader_spec.rb +53 -0
- data/spec/unit/ref_spec.rb +107 -0
- data/spec/unit/translator_spec.rb +15 -0
- 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
|