atum 0.5.0
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/.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
|