currency_cloud 0.5
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 +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +12 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +99 -0
- data/Guardfile +14 -0
- data/LICENSE.md +22 -0
- data/README.md +10 -0
- data/Rakefile +30 -0
- data/currency_cloud.gemspec +27 -0
- data/examples/server.rb +15 -0
- data/lib/currency_cloud.rb +26 -0
- data/lib/currency_cloud/actions/create.rb +9 -0
- data/lib/currency_cloud/actions/current.rb +9 -0
- data/lib/currency_cloud/actions/delete.rb +15 -0
- data/lib/currency_cloud/actions/find.rb +27 -0
- data/lib/currency_cloud/actions/retrieve.rb +9 -0
- data/lib/currency_cloud/actions/update.rb +12 -0
- data/lib/currency_cloud/errors/api_error.rb +38 -0
- data/lib/currency_cloud/errors/config_error.rb +5 -0
- data/lib/currency_cloud/errors/unexpected_error.rb +10 -0
- data/lib/currency_cloud/pagination.rb +7 -0
- data/lib/currency_cloud/request_handler.rb +68 -0
- data/lib/currency_cloud/resource.rb +73 -0
- data/lib/currency_cloud/resourceful_collection.rb +15 -0
- data/lib/currency_cloud/resources/account.rb +6 -0
- data/lib/currency_cloud/resources/balance.rb +8 -0
- data/lib/currency_cloud/resources/beneficiary.rb +14 -0
- data/lib/currency_cloud/resources/contact.rb +6 -0
- data/lib/currency_cloud/resources/conversion.rb +10 -0
- data/lib/currency_cloud/resources/payer.rb +6 -0
- data/lib/currency_cloud/resources/payment.rb +11 -0
- data/lib/currency_cloud/resources/rate.rb +21 -0
- data/lib/currency_cloud/resources/reference.rb +29 -0
- data/lib/currency_cloud/resources/settlement.rb +30 -0
- data/lib/currency_cloud/resources/transaction.rb +7 -0
- data/lib/currency_cloud/response_handler.rb +46 -0
- data/lib/currency_cloud/session.rb +62 -0
- data/lib/currency_cloud/version.rb +8 -0
- data/spec/currency_cloud_spec.rb +78 -0
- data/spec/integration/actions_spec.rb +101 -0
- data/spec/integration/authentication_spec.rb +37 -0
- data/spec/integration/errors_spec.rb +157 -0
- data/spec/integration/rates_spec.rb +39 -0
- data/spec/integration/reference_spec.rb +57 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/support/vcr_cassettes/Actions/can_create.yml +38 -0
- data/spec/support/vcr_cassettes/Actions/can_current.yml +37 -0
- data/spec/support/vcr_cassettes/Actions/can_delete.yml +39 -0
- data/spec/support/vcr_cassettes/Actions/can_find.yml +38 -0
- data/spec/support/vcr_cassettes/Actions/can_first.yml +75 -0
- data/spec/support/vcr_cassettes/Actions/can_retrieve.yml +38 -0
- data/spec/support/vcr_cassettes/Actions/can_update.yml +39 -0
- data/spec/support/vcr_cassettes/Authentication/can_be_closed.yml +67 -0
- data/spec/support/vcr_cassettes/Authentication/can_use_just_a_token.yml +36 -0
- data/spec/support/vcr_cassettes/Authentication/handles_session_timeout_error.yml +101 -0
- data/spec/support/vcr_cassettes/Authentication/happens_lazily.yml +34 -0
- data/spec/support/vcr_cassettes/Error/is_raised_on_a_bad_request.yml +39 -0
- data/spec/support/vcr_cassettes/Error/is_raised_on_a_forbidden_request.yml +35 -0
- data/spec/support/vcr_cassettes/Error/is_raised_on_an_internal_server_error.yml +69 -0
- data/spec/support/vcr_cassettes/Error/is_raised_on_incorrect_authentication_details.yml +39 -0
- data/spec/support/vcr_cassettes/Error/is_raised_when_a_resource_is_not_found.yml +37 -0
- data/spec/support/vcr_cassettes/Error/is_raised_when_too_many_requests_have_been_issued.yml +34 -0
- data/spec/support/vcr_cassettes/Rates/can_find.yml +36 -0
- data/spec/support/vcr_cassettes/Rates/can_provided_detailed_rate.yml +36 -0
- data/spec/support/vcr_cassettes/Reference/can_retrieve_beneficiary_required_details.yml +36 -0
- data/spec/support/vcr_cassettes/Reference/can_retrieve_conversion_dates.yml +46 -0
- data/spec/support/vcr_cassettes/Reference/can_retrieve_currencies.yml +47 -0
- data/spec/support/vcr_cassettes/Reference/can_retrieve_settlement_accounts.yml +39 -0
- metadata +230 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
module CurrencyCloud
|
2
|
+
|
3
|
+
class RequestHandler
|
4
|
+
|
5
|
+
attr_reader :session
|
6
|
+
|
7
|
+
def initialize(session = CurrencyCloud.session)
|
8
|
+
@session = session
|
9
|
+
end
|
10
|
+
|
11
|
+
def get(route, params={}, opts={})
|
12
|
+
retry_authenticate('get', route, params, opts) do |url, options|
|
13
|
+
HTTParty.get(url, options)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def post(route, params={}, opts={})
|
18
|
+
retry_authenticate('post', route, params, opts) do |url, options|
|
19
|
+
HTTParty.post(url, options)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def retry_authenticate(verb, route, params, opts)
|
25
|
+
should_retry = opts[:should_retry].nil? ? true : opts.delete(:should_retry)
|
26
|
+
description = "to #{verb}: #{route}"
|
27
|
+
|
28
|
+
options = process_options(verb, params, opts)
|
29
|
+
|
30
|
+
response = nil
|
31
|
+
retry_count = should_retry ? 0 : 2
|
32
|
+
while retry_count < 3
|
33
|
+
response = yield(full_url(route), options)
|
34
|
+
break unless response.code == 401 && should_retry
|
35
|
+
session.reauthenticate
|
36
|
+
retry_count += 1
|
37
|
+
end
|
38
|
+
|
39
|
+
process_response(response)
|
40
|
+
rescue => e
|
41
|
+
raise if e.class.ancestors.include?(ApiError) || e.is_a?(UnexpectedError)
|
42
|
+
raise UnexpectedError.new(e)
|
43
|
+
end
|
44
|
+
|
45
|
+
def process_options(verb, params, opts)
|
46
|
+
options = {:headers => headers }
|
47
|
+
params_key = verb == :get ? :query : :body
|
48
|
+
options[params_key] = params
|
49
|
+
options.merge!(opts)
|
50
|
+
# options[:debug_output] = $stdout
|
51
|
+
options
|
52
|
+
end
|
53
|
+
|
54
|
+
def headers
|
55
|
+
headers = {}
|
56
|
+
headers['X-Auth-Token'] = session.token if session.token
|
57
|
+
headers
|
58
|
+
end
|
59
|
+
|
60
|
+
def full_url(route)
|
61
|
+
"#{session.environment_url}/#{CurrencyCloud::ApiVersion}/" + route
|
62
|
+
end
|
63
|
+
|
64
|
+
def process_response(response)
|
65
|
+
CurrencyCloud::ResponseHandler.process(response)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module CurrencyCloud
|
4
|
+
|
5
|
+
class Resource
|
6
|
+
|
7
|
+
def self.resource(resource=nil)
|
8
|
+
@resource ||= resource
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.actions(*actions)
|
12
|
+
@actions ||= actions
|
13
|
+
@actions.each do |action|
|
14
|
+
self.class_eval do # self.class.instance_eval # metaclass.instance_eval
|
15
|
+
action_module = CurrencyCloud::Actions.const_get(action.to_s.capitalize)
|
16
|
+
self.extend(action_module)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(object)
|
22
|
+
@object = object
|
23
|
+
@changed_attributes = Set.new
|
24
|
+
set_accessors(keys)
|
25
|
+
end
|
26
|
+
|
27
|
+
def keys
|
28
|
+
@object.keys
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
"#<#{self.class}:0x#{self.object_id.to_s(16)} #{@object.inspect}>"
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def changed?
|
38
|
+
!@changed_attributes.empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
def metaclass
|
42
|
+
class << self; self; end
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_accessors(keys)
|
46
|
+
metaclass.instance_eval do
|
47
|
+
keys.each do |key|
|
48
|
+
define_method(key) { @object[key] }
|
49
|
+
define_method("#{key}=".to_sym) do |value|
|
50
|
+
@object[key] = value
|
51
|
+
@changed_attributes << key
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.get(url, params={})
|
58
|
+
new(request.get(build_url(url), params))
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.post(url, params={})
|
62
|
+
new(request.post(build_url(url), params))
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.build_url(url)
|
66
|
+
"#{self.resource}/#{url}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.request
|
70
|
+
RequestHandler.new
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module CurrencyCloud
|
2
|
+
class ResourcefulCollection
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :@collection, :[], :length, :empty?, :each
|
7
|
+
|
8
|
+
attr_reader :pagination
|
9
|
+
|
10
|
+
def initialize(resource, klass, collection)
|
11
|
+
@collection = collection[resource.to_s].map { |object| klass.new(object) }
|
12
|
+
@pagination = Pagination.new(collection['pagination'])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module CurrencyCloud
|
2
|
+
class Rates < Resource; end
|
3
|
+
|
4
|
+
class Rate < Resource
|
5
|
+
resource :rates
|
6
|
+
|
7
|
+
def self.find(params)
|
8
|
+
response = request.get("#{self.resource}/find", params)
|
9
|
+
|
10
|
+
rates = response['rates'].map do |currency_pair, bid_offer|
|
11
|
+
new(currency_pair: currency_pair, bid: bid_offer[0], offer: bid_offer[1])
|
12
|
+
end
|
13
|
+
|
14
|
+
Rates.new(currencies: rates, unavailable: response['unavailable'])
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.detailed(params)
|
18
|
+
get('detailed', params)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module CurrencyCloud
|
2
|
+
|
3
|
+
class Currency < Resource; end
|
4
|
+
class ConversionDates < Resource; end
|
5
|
+
class SettlementAccount < Resource; end
|
6
|
+
|
7
|
+
class Reference < Resource
|
8
|
+
|
9
|
+
resource :reference
|
10
|
+
|
11
|
+
def self.currencies
|
12
|
+
response = request.get(build_url("currencies"))
|
13
|
+
response['currencies'].map { |c| Currency.new(c)}
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.beneficiary_required_details(params={})
|
17
|
+
request.get(build_url("beneficiary_required_details"), params)['details']
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.conversion_dates(params)
|
21
|
+
ConversionDates.new(request.get(build_url("conversion_dates"), params))
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.settlement_accounts(params={})
|
25
|
+
response = request.get(build_url("settlement_accounts"), params)
|
26
|
+
response['settlement_accounts'].map { |s| SettlementAccount.new(s)}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module CurrencyCloud
|
2
|
+
|
3
|
+
class Settlement < Resource
|
4
|
+
|
5
|
+
resource :settlements
|
6
|
+
|
7
|
+
actions :create, :retrieve, :find, :delete
|
8
|
+
|
9
|
+
def add_conversion(conversion_id)
|
10
|
+
# TODO: Should just update state of current object using a refresh method?
|
11
|
+
new(request.post("#{self.resource}/#{self.id}/add_conversion", conversion_id: conversion_id))
|
12
|
+
end
|
13
|
+
|
14
|
+
def remove_conversion(conversion_id)
|
15
|
+
# TODO: Should just update state of current object using a refresh method?
|
16
|
+
new(request.post("#{self.resource}/#{self.id}/remove_conversion", conversion_id: conversion_id))
|
17
|
+
end
|
18
|
+
|
19
|
+
def release
|
20
|
+
# TODO: Should just update state of current object using a refresh method?
|
21
|
+
new(request.post("#{self.resource}/#{self.id}/release"))
|
22
|
+
end
|
23
|
+
|
24
|
+
def unrelease
|
25
|
+
# TODO: Should just update state of current object using a refresh method?
|
26
|
+
new(request.post("#{self.resource}/#{self.id}/unrelease"))
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module CurrencyCloud
|
2
|
+
|
3
|
+
class ResponseHandler
|
4
|
+
|
5
|
+
attr_reader :response
|
6
|
+
|
7
|
+
def self.process(response)
|
8
|
+
new(response).data
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(response)
|
12
|
+
@response = response
|
13
|
+
end
|
14
|
+
|
15
|
+
def data
|
16
|
+
if success?
|
17
|
+
return parsed_response
|
18
|
+
else
|
19
|
+
handle_failure
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def success?
|
26
|
+
[200, 202].include?(response.code)
|
27
|
+
end
|
28
|
+
|
29
|
+
def handle_failure
|
30
|
+
error_class = case response.code
|
31
|
+
when 400 then CurrencyCloud::BadRequestError
|
32
|
+
when 401 then CurrencyCloud::AuthenticationError
|
33
|
+
when 403 then CurrencyCloud::ForbiddenError
|
34
|
+
when 404 then CurrencyCloud::NotFoundError
|
35
|
+
when 429 then CurrencyCloud::TooManyRequestsError
|
36
|
+
when 500 then CurrencyCloud::InternalApplicationError
|
37
|
+
else CurrencyCloud::UnexpectedError
|
38
|
+
end
|
39
|
+
raise error_class.new(response)
|
40
|
+
end
|
41
|
+
|
42
|
+
def parsed_response
|
43
|
+
@parsed_response ||= JSON.parse(response.body)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module CurrencyCloud
|
2
|
+
|
3
|
+
class Session
|
4
|
+
|
5
|
+
Environments = { :production => 'https://api.thecurrencycloud.com',
|
6
|
+
:demonstration => 'https://devapi.thecurrencycloud.com',
|
7
|
+
:uat => 'https://api-uat1.ccycloud.com'}
|
8
|
+
|
9
|
+
attr_reader :environment, :login_id, :api_key
|
10
|
+
attr_accessor :token
|
11
|
+
|
12
|
+
def self.validate_environment(environment)
|
13
|
+
unless Environments.keys.include?(environment)
|
14
|
+
raise CurrencyCloud::ConfigError, "'#{environment}' is not a valid environment, must be one of: #{Environments.keys.join(", ")}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(environment, login_id, api_key, token)
|
19
|
+
@environment = environment
|
20
|
+
@login_id = login_id
|
21
|
+
@api_key = api_key
|
22
|
+
|
23
|
+
if token
|
24
|
+
self.class.validate_environment(environment)
|
25
|
+
@token = token
|
26
|
+
else
|
27
|
+
authenticate
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def environment_url
|
32
|
+
Environments[environment]
|
33
|
+
end
|
34
|
+
|
35
|
+
def close
|
36
|
+
request.post('authenticate/close_session')
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def authenticate
|
41
|
+
validate
|
42
|
+
params = {:login_id => login_id, :api_key => api_key}
|
43
|
+
@token = request.post('authenticate/api', params, :should_retry => false)['auth_token']
|
44
|
+
end
|
45
|
+
|
46
|
+
def reauthenticate
|
47
|
+
token = nil
|
48
|
+
authenticate
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def validate
|
53
|
+
self.class.validate_environment(environment)
|
54
|
+
raise CurrencyCloud::ConfigError, "login_id must be set using CurrencyCloud.login_id=" unless login_id
|
55
|
+
raise CurrencyCloud::ConfigError, "api_key must be set using CurrencyCloud.api_key=" unless api_key
|
56
|
+
end
|
57
|
+
|
58
|
+
def request
|
59
|
+
RequestHandler.new(self)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|