atum 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.rubocop.yml +9 -0
- data/.rubocop_todo.yml +25 -0
- data/.travis.yml +9 -0
- data/Appraisals +9 -0
- data/CHANGELOG.md +5 -0
- data/CONTRIBUTING.md +9 -0
- data/CONTRIBUTORS.md +15 -0
- data/Gemfile +4 -0
- data/Guardfile +23 -0
- data/LICENSE.txt +22 -0
- data/README.md +95 -0
- data/Rakefile +13 -0
- data/TODO +3 -0
- data/atum.gemspec +37 -0
- data/bin/atum +41 -0
- data/circle.yml +7 -0
- data/gemfiles/faraday_0.8.9.gemfile +8 -0
- data/gemfiles/faraday_0.9.gemfile +8 -0
- data/lib/atum.rb +15 -0
- data/lib/atum/core.rb +13 -0
- data/lib/atum/core/client.rb +45 -0
- data/lib/atum/core/errors.rb +20 -0
- data/lib/atum/core/link.rb +77 -0
- data/lib/atum/core/paginator.rb +32 -0
- data/lib/atum/core/request.rb +55 -0
- data/lib/atum/core/resource.rb +15 -0
- data/lib/atum/core/response.rb +53 -0
- data/lib/atum/core/schema.rb +12 -0
- data/lib/atum/core/schema/api_schema.rb +62 -0
- data/lib/atum/core/schema/link_schema.rb +121 -0
- data/lib/atum/core/schema/parameter.rb +27 -0
- data/lib/atum/core/schema/parameter_choice.rb +28 -0
- data/lib/atum/core/schema/resource_schema.rb +51 -0
- data/lib/atum/generation.rb +15 -0
- data/lib/atum/generation/erb_context.rb +17 -0
- data/lib/atum/generation/errors.rb +6 -0
- data/lib/atum/generation/generator_link.rb +39 -0
- data/lib/atum/generation/generator_resource.rb +31 -0
- data/lib/atum/generation/generator_service.rb +73 -0
- data/lib/atum/generation/generators/base_generator.rb +57 -0
- data/lib/atum/generation/generators/client_generator.rb +16 -0
- data/lib/atum/generation/generators/module_generator.rb +17 -0
- data/lib/atum/generation/generators/resource_generator.rb +23 -0
- data/lib/atum/generation/generators/views/client.erb +26 -0
- data/lib/atum/generation/generators/views/module.erb +104 -0
- data/lib/atum/generation/generators/views/resource.erb +33 -0
- data/lib/atum/generation/options_parameter.rb +12 -0
- data/lib/atum/version.rb +3 -0
- data/spec/atum/core/client_spec.rb +26 -0
- data/spec/atum/core/errors_spec.rb +19 -0
- data/spec/atum/core/link_spec.rb +80 -0
- data/spec/atum/core/paginator_spec.rb +72 -0
- data/spec/atum/core/request_spec.rb +110 -0
- data/spec/atum/core/resource_spec.rb +66 -0
- data/spec/atum/core/response_spec.rb +127 -0
- data/spec/atum/core/schema/api_schema_spec.rb +49 -0
- data/spec/atum/core/schema/link_schema_spec.rb +91 -0
- data/spec/atum/core/schema/parameter_choice_spec.rb +40 -0
- data/spec/atum/core/schema/parameter_spec.rb +24 -0
- data/spec/atum/core/schema/resource_schema_spec.rb +24 -0
- data/spec/atum/generation/generator_link_spec.rb +62 -0
- data/spec/atum/generation/generator_resource_spec.rb +44 -0
- data/spec/atum/generation/generator_service_spec.rb +41 -0
- data/spec/atum/generation/generators/base_generator_spec.rb +75 -0
- data/spec/atum/generation/generators/client_generator_spec.rb +30 -0
- data/spec/atum/generation/generators/module_generator_spec.rb +37 -0
- data/spec/atum/generation/generators/resource_generator_spec.rb +46 -0
- data/spec/atum/generation/options_parameter_spec.rb +27 -0
- data/spec/fixtures/fruity_schema.json +161 -0
- data/spec/fixtures/sample_schema.json +139 -0
- data/spec/integration/client_integration_spec.rb +91 -0
- data/spec/spec_helper.rb +11 -0
- metadata +303 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
module Atum
|
2
|
+
module Core
|
3
|
+
class SchemaError < StandardError; end
|
4
|
+
|
5
|
+
class ResponseError < StandardError; end
|
6
|
+
|
7
|
+
class ApiError < StandardError
|
8
|
+
attr_reader :error
|
9
|
+
|
10
|
+
def initialize(error)
|
11
|
+
@error = error
|
12
|
+
if @error.key?('documentation_url')
|
13
|
+
super("#{@error['message']}, see #{@error['documentation_url']}")
|
14
|
+
else
|
15
|
+
super("#{@error['message']}")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'active_support/inflector'
|
3
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
4
|
+
|
5
|
+
module Atum
|
6
|
+
module Core
|
7
|
+
# A link invokes requests with an HTTP server.
|
8
|
+
class Link
|
9
|
+
# The amount limit is increased on each successive fetch in pagination
|
10
|
+
LIMIT_INCREMENT = 50
|
11
|
+
|
12
|
+
# Instantiate a link.
|
13
|
+
#
|
14
|
+
# @param url [String] The URL to use when making requests. Include the
|
15
|
+
# username and password to use with HTTP basic auth.
|
16
|
+
# @param link_schema [LinkSchema] The schema for this link.
|
17
|
+
# @param options [Hash] Configuration for the link. Possible keys
|
18
|
+
# include:
|
19
|
+
# - default_headers: Optionally, a set of headers to include in every
|
20
|
+
# request made by the client. Default is no custom headers.
|
21
|
+
def initialize(url, link_schema, options = {})
|
22
|
+
root_url, @path_prefix = unpack_url(url)
|
23
|
+
@connection = Faraday.new(url: root_url)
|
24
|
+
@link_schema = link_schema
|
25
|
+
@headers = options[:default_headers] || {}
|
26
|
+
end
|
27
|
+
|
28
|
+
# Make a request to the server.
|
29
|
+
#
|
30
|
+
# @param parameters [Array] The list of parameters to inject into the
|
31
|
+
# path. A request body can be passed as the final parameter and will
|
32
|
+
# always be converted to JSON before being transmitted.
|
33
|
+
# @raise [ArgumentError] Raised if either too many or too few parameters
|
34
|
+
# were provided.
|
35
|
+
# @return [String,Object,Enumerator] A string for text responses, an
|
36
|
+
# object for JSON responses, or an enumerator for list responses.
|
37
|
+
def run(*parameters)
|
38
|
+
options = parameters.pop
|
39
|
+
raise ArgumentError, 'options must be a hash' unless options.is_a?(Hash)
|
40
|
+
|
41
|
+
options = default_options.deep_merge(options)
|
42
|
+
path = build_path(*parameters)
|
43
|
+
Request.new(@connection, @link_schema.method, path, options).request
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def default_options
|
49
|
+
{
|
50
|
+
headers: @headers,
|
51
|
+
envelope_name: @link_schema.resource_schema.name.pluralize
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_path(*parameters)
|
56
|
+
@path_prefix + @link_schema.construct_path(*parameters)
|
57
|
+
end
|
58
|
+
|
59
|
+
def apply_link_schema(hash)
|
60
|
+
definitions = @link_schema.resource_schema.definitions
|
61
|
+
hash.each do |k, v|
|
62
|
+
next unless definitions.fetch(k, {}).fetch('format', nil)
|
63
|
+
case definitions[k]['format']
|
64
|
+
when 'date-time'
|
65
|
+
hash[k] = Time.parse(v) unless v.nil?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
hash
|
69
|
+
end
|
70
|
+
|
71
|
+
def unpack_url(url)
|
72
|
+
path = URI.parse(url).path
|
73
|
+
[URI.join(url).to_s, path == '/' ? '' : path]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Atum
|
2
|
+
module Core
|
3
|
+
class Paginator
|
4
|
+
LIMIT_INCREMENT = 50
|
5
|
+
|
6
|
+
def initialize(request, initial_response, options)
|
7
|
+
@request = request
|
8
|
+
@options = options
|
9
|
+
@initial_response = initial_response
|
10
|
+
end
|
11
|
+
|
12
|
+
def enumerator
|
13
|
+
response = @initial_response
|
14
|
+
Enumerator.new do |yielder|
|
15
|
+
loop do
|
16
|
+
items = @request.unenvelope(response.body)
|
17
|
+
items.each { |item| yielder << item }
|
18
|
+
|
19
|
+
break if items.count < response.limit
|
20
|
+
|
21
|
+
new_options = @options.dup
|
22
|
+
new_options[:query] = @options.fetch(:query, {})
|
23
|
+
new_options[:query].merge!(after: response.meta['cursors']['after'],
|
24
|
+
limit: response.limit + LIMIT_INCREMENT)
|
25
|
+
|
26
|
+
response = @request.make_request(new_options)
|
27
|
+
end
|
28
|
+
end.lazy
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Atum
|
2
|
+
module Core
|
3
|
+
class Request
|
4
|
+
def initialize(connection, method, path, options)
|
5
|
+
@connection = connection
|
6
|
+
@method = method
|
7
|
+
@path = path
|
8
|
+
@headers = options[:headers] || {}
|
9
|
+
@body = options[:body] || nil
|
10
|
+
@query = options[:query] || {}
|
11
|
+
@envelope_name = options[:envelope_name] || 'data'
|
12
|
+
end
|
13
|
+
|
14
|
+
def request
|
15
|
+
response = Response.new(make_request)
|
16
|
+
return response.body unless response.json?
|
17
|
+
|
18
|
+
if response.limit.nil?
|
19
|
+
unenvelope(response.body)
|
20
|
+
else
|
21
|
+
Paginator.new(self, response, options).enumerator
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def make_request(opts = options)
|
26
|
+
body = opts[:body]
|
27
|
+
headers = opts.fetch(:headers, {})
|
28
|
+
query = opts[:query]
|
29
|
+
|
30
|
+
# TODO: Not sure this is required in faraday 0.9.0
|
31
|
+
if body.is_a?(Hash)
|
32
|
+
body = body.to_json
|
33
|
+
headers['Content-Type'] ||= 'application/json'
|
34
|
+
end
|
35
|
+
|
36
|
+
@connection.send(@method) do |request|
|
37
|
+
request.url @path
|
38
|
+
request.body = body
|
39
|
+
request.params = query
|
40
|
+
request.headers.merge!(headers)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def unenvelope(body)
|
45
|
+
body[@envelope_name] || body['data']
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def options
|
51
|
+
{ headers: @headers, body: @body, query: @query }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Atum
|
2
|
+
module Core
|
3
|
+
# A resource with methods mapped to API links.
|
4
|
+
class Resource
|
5
|
+
# Instantiate a resource.
|
6
|
+
#
|
7
|
+
# @param links [Hash<String,Link>] A hash that maps method names to links.
|
8
|
+
def initialize(links)
|
9
|
+
links.each do |name, link|
|
10
|
+
define_singleton_method(name) { |*args| link.run(*args) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Atum
|
2
|
+
module Core
|
3
|
+
class Response
|
4
|
+
def initialize(response)
|
5
|
+
@response = response
|
6
|
+
end
|
7
|
+
|
8
|
+
def body
|
9
|
+
json? ? handle_json : handle_raw
|
10
|
+
end
|
11
|
+
|
12
|
+
def json?
|
13
|
+
content_type = @response.headers['Content-Type'] ||
|
14
|
+
@response.headers['content-type'] || ''
|
15
|
+
content_type.include?('application/json')
|
16
|
+
end
|
17
|
+
|
18
|
+
def error?
|
19
|
+
@response.status >= 400
|
20
|
+
end
|
21
|
+
|
22
|
+
def meta
|
23
|
+
unless json?
|
24
|
+
raise ResponseError, 'Cannot fetch meta for non JSON response'
|
25
|
+
end
|
26
|
+
|
27
|
+
json_body.fetch('meta', {})
|
28
|
+
end
|
29
|
+
|
30
|
+
def limit
|
31
|
+
meta.fetch('limit', nil)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def json_body
|
37
|
+
@json_body ||= JSON.parse(@response.body).with_indifferent_access
|
38
|
+
end
|
39
|
+
|
40
|
+
def raw_body
|
41
|
+
@response.body
|
42
|
+
end
|
43
|
+
|
44
|
+
def handle_json
|
45
|
+
error? ? raise(ApiError, json_body['error']) : json_body
|
46
|
+
end
|
47
|
+
|
48
|
+
def handle_raw
|
49
|
+
error? ? raise(ApiError) : raw_body
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Atum
|
2
|
+
module Core
|
3
|
+
module Schema
|
4
|
+
end
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'atum/core/schema/api_schema'
|
9
|
+
require 'atum/core/schema/link_schema'
|
10
|
+
require 'atum/core/schema/resource_schema'
|
11
|
+
require 'atum/core/schema/parameter'
|
12
|
+
require 'atum/core/schema/parameter_choice'
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Atum
|
2
|
+
module Core
|
3
|
+
module Schema
|
4
|
+
class ApiSchema
|
5
|
+
attr_reader :schema
|
6
|
+
|
7
|
+
def initialize(schema)
|
8
|
+
@schema = schema
|
9
|
+
end
|
10
|
+
|
11
|
+
# Description of the API
|
12
|
+
def description
|
13
|
+
@schema['description']
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get the schema for a resource.
|
17
|
+
#
|
18
|
+
# @param name [String] The name of the resource.
|
19
|
+
# @raise [SchemaError] Raised if an unknown resource name is provided.
|
20
|
+
# @return ResourceSchema The resource schema for resource called name
|
21
|
+
def resource_schema_for(name)
|
22
|
+
unless resource_schema_hash.key?(name)
|
23
|
+
raise SchemaError, "Unknown resource '#{name}'."
|
24
|
+
end
|
25
|
+
|
26
|
+
resource_schema_hash[name]
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Array<ResourceSchema>] The resource schemata in this API.
|
30
|
+
def resource_schemas
|
31
|
+
resource_schema_hash.values
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get a simple human-readable representation of this client instance.
|
35
|
+
def inspect
|
36
|
+
"#<Atum::ApiSchema description=\"#{description}\">"
|
37
|
+
end
|
38
|
+
|
39
|
+
alias_method :to_s, :inspect
|
40
|
+
|
41
|
+
# Lookup a path in this schema.
|
42
|
+
#
|
43
|
+
# @param path [Array<String>] Array of keys, one for each to look up in
|
44
|
+
# the schema
|
45
|
+
# @return [Object] Value at the specifed path in this schema.
|
46
|
+
def lookup_path(*path)
|
47
|
+
path.reduce(@schema) { |a, e| a[e] }
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def resource_schema_hash
|
53
|
+
@resource_schema_hash ||= Hash[
|
54
|
+
@schema['definitions'].map do |key, value|
|
55
|
+
[key, ResourceSchema.new(self, value, key)]
|
56
|
+
end
|
57
|
+
]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module Atum
|
2
|
+
module Core
|
3
|
+
module Schema
|
4
|
+
class LinkSchema
|
5
|
+
attr_reader :resource_schema
|
6
|
+
|
7
|
+
# @param api_schema [ApiSchema] The schema for the whole API
|
8
|
+
# @param resource_schema [ResourceSchema] The schema of the resource this
|
9
|
+
# link belongs to.
|
10
|
+
# @param link_schema [Hash] The link's schema
|
11
|
+
def initialize(api_schema, resource_schema, link_schema_hash)
|
12
|
+
@api_schema = api_schema
|
13
|
+
@resource_schema = resource_schema
|
14
|
+
@link_schema_hash = link_schema_hash
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
@link_schema_hash['title'].downcase.gsub(' ', '_')
|
19
|
+
end
|
20
|
+
|
21
|
+
def description
|
22
|
+
@link_schema_hash['description']
|
23
|
+
end
|
24
|
+
|
25
|
+
def method
|
26
|
+
@link_schema_hash['method'].downcase.to_sym
|
27
|
+
end
|
28
|
+
|
29
|
+
def needs_request_body?
|
30
|
+
@link_schema_hash.key?('schema')
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get the parameters this link expects.
|
34
|
+
#
|
35
|
+
# @param parameters [Array] The names of the parameter definitions to
|
36
|
+
# convert to parameter names.
|
37
|
+
# @return [Array<Parameter|ParameterChoice>] A list of parameter instances
|
38
|
+
# that represent parameters to be injected into the link URL.
|
39
|
+
def parameters
|
40
|
+
expected_params.map do |parameter|
|
41
|
+
# URI decode parameters and strip the leading '{(' and trailing ')}'.
|
42
|
+
parameter = URI.unescape(parameter[2..-3])
|
43
|
+
generate_parameter(parameter)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Construct the URL and body for a call to this link
|
48
|
+
#
|
49
|
+
# @param params [Array] The list of parameters to inject into the
|
50
|
+
# path.
|
51
|
+
# @raise [ArgumentError] Raised if either too many or too few parameters
|
52
|
+
# were provided.
|
53
|
+
# @return [String,Object] A path and request body pair. The body value is
|
54
|
+
# nil if a payload wasn't included in the list of parameters.
|
55
|
+
def construct_path(*params)
|
56
|
+
if expected_params.count != params.count
|
57
|
+
raise ArgumentError, "Wrong number of arguments: #{params.count} " \
|
58
|
+
"for #{expected_params.count}"
|
59
|
+
end
|
60
|
+
|
61
|
+
href.gsub(PARAMETER_REGEX) { |_match| format_parameter(params.shift) }
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# Match parameters in definition strings.
|
67
|
+
PARAMETER_REGEX = /\{\([%\/a-zA-Z0-9_-]*\)\}/
|
68
|
+
|
69
|
+
def href
|
70
|
+
@link_schema_hash['href']
|
71
|
+
end
|
72
|
+
|
73
|
+
def expected_params
|
74
|
+
href.scan(PARAMETER_REGEX)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Unpack an 'anyOf' or 'oneOf' multi-parameter blob.
|
78
|
+
#
|
79
|
+
# @param parameters [Array<Hash>] An array of hashes containing '$ref'
|
80
|
+
# keys and definition values.
|
81
|
+
# @return [Array<Parameter>] An array of parameters extracted from the
|
82
|
+
# blob.
|
83
|
+
def unpack_multi_params(parameters)
|
84
|
+
parameters.map { |info| generate_parameter(info['$ref']) }
|
85
|
+
end
|
86
|
+
|
87
|
+
def generate_parameter(param)
|
88
|
+
path = param.split('/')[1..-1]
|
89
|
+
name = path[-1]
|
90
|
+
resource_name = path.size > 2 ? path[1] : nil
|
91
|
+
info = @api_schema.lookup_path(*path)
|
92
|
+
description = info['description']
|
93
|
+
|
94
|
+
if info.key?('anyOf')
|
95
|
+
ParameterChoice.new(resource_name, unpack_multi_params(info['anyOf']))
|
96
|
+
elsif info.key?('oneOf')
|
97
|
+
ParameterChoice.new(resource_name, unpack_multi_params(info['oneOf']))
|
98
|
+
else
|
99
|
+
Parameter.new(resource_name, name, description)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Convert a path parameter to a format suitable for use in a path.
|
104
|
+
#
|
105
|
+
# @param [Fixnum,String,TrueClass,FalseClass,Time] The parameter to format.
|
106
|
+
# @return [String] The formatted parameter.
|
107
|
+
def format_parameter(parameter)
|
108
|
+
parameter.instance_of?(Time) ? iso_format(parameter) : parameter.to_s
|
109
|
+
end
|
110
|
+
|
111
|
+
# Convert a time to an ISO 8601 combined data and time format.
|
112
|
+
#
|
113
|
+
# @param time [Time] The time to convert to ISO 8601 format.
|
114
|
+
# @return [String] An ISO 8601 date in `YYYY-MM-DDTHH:MM:SSZ` format.
|
115
|
+
def iso_format(time)
|
116
|
+
time.getutc.iso8601
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|