open_api-loader 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|