tango-client 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/README.md +76 -0
- data/Rakefile +5 -0
- data/lib/tango.rb +47 -0
- data/lib/tango/client.rb +91 -0
- data/lib/tango/default.rb +56 -0
- data/lib/tango/error.rb +78 -0
- data/lib/tango/request/json_encoded.rb +41 -0
- data/lib/tango/response/parse_json.rb +32 -0
- data/lib/tango/response/raise_error.rb +19 -0
- data/lib/tango/ssl/cacert.pem +3825 -0
- data/lib/tango/version.rb +3 -0
- data/spec/lib/tango/response/raise_error_spec.rb +0 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/faraday_stub.rb +42 -0
- data/spec/tango/client_spec.rb +42 -0
- data/spec/tango/error_spec.rb +32 -0
- data/spec/tango/request/json_encoded_spec.rb +29 -0
- data/spec/tango/response/parse_json_spec.rb +47 -0
- data/spec/tango/response/raise_error_spec.rb +41 -0
- data/tango-client.gemspec +28 -0
- metadata +212 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private --protected lib/**/*.rb - README.md
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# Tango::Client
|
2
|
+
|
3
|
+
HTTP client to ease using Tango API
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'tango-client'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install tango-client
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
### Use Tango class methods
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
|
25
|
+
require 'tango'
|
26
|
+
|
27
|
+
Tango.get_available_balance
|
28
|
+
# => 539694976
|
29
|
+
|
30
|
+
Tango.purchase_card(cardSku: 'tango-card',
|
31
|
+
cardValue: 100,
|
32
|
+
tcSend: false,
|
33
|
+
recipientName: nil,
|
34
|
+
recipientEmail: nil,
|
35
|
+
giftMessage: nil,
|
36
|
+
giftFrom: nil)
|
37
|
+
# => {
|
38
|
+
# :referenceOrderId=>"112-12226603-04",
|
39
|
+
# :cardToken=>"50bdb8ce341848.92673903",
|
40
|
+
# :cardNumber=>"7001-5040-0198-7543-015",
|
41
|
+
# :cardPin=>"971642",
|
42
|
+
# :claimUrl=>nil,
|
43
|
+
# :challengeKey=>"7001504001987543015"
|
44
|
+
# }
|
45
|
+
|
46
|
+
```
|
47
|
+
|
48
|
+
### Use Client instance
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
require 'tango'
|
52
|
+
|
53
|
+
client = Tango::Client.new(:username => 'myaccount', :password => 'mypassword')
|
54
|
+
|
55
|
+
client.get_available_balance
|
56
|
+
client.purchase_card({})
|
57
|
+
```
|
58
|
+
|
59
|
+
### Configuration
|
60
|
+
|
61
|
+
- `Tango` class methods are delegated to `Tango.client`, which options can
|
62
|
+
be changed by updating `Tango.options`. The changes are applied since next
|
63
|
+
request.
|
64
|
+
- `Tango::Client` instances are configured by initialization argument.
|
65
|
+
- Default username, password and endpoint can be configured by environment variable
|
66
|
+
`TANGO_USERNAME`, `TANGO_PASSWORD` and `TANGO_ENDPOINT`.
|
67
|
+
- Changes to `Tango::Default.options` will applied to following created new client
|
68
|
+
instance as default values.
|
69
|
+
|
70
|
+
## Contributing
|
71
|
+
|
72
|
+
1. Fork it
|
73
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
74
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
75
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
76
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/tango.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require "tango/version"
|
2
|
+
require "tango/default"
|
3
|
+
require "tango/client"
|
4
|
+
|
5
|
+
module Tango
|
6
|
+
class << self
|
7
|
+
|
8
|
+
# Delegates to a client. The client is re-initialized after any
|
9
|
+
# configuration option is changed.
|
10
|
+
#
|
11
|
+
# @return [Tango::Client]
|
12
|
+
def client
|
13
|
+
unless defined?(@client) && @client.options.hash == options.hash
|
14
|
+
@client = ::Tango::Client.new(options)
|
15
|
+
end
|
16
|
+
|
17
|
+
@client
|
18
|
+
end
|
19
|
+
|
20
|
+
# Global options. New created {Tango::Client} instances observe options here.
|
21
|
+
#
|
22
|
+
# The {#client} instance is re-initialized after options are changed.
|
23
|
+
#
|
24
|
+
# @return [Hash]
|
25
|
+
def options
|
26
|
+
@options ||= ::Tango::Default.options.dup
|
27
|
+
end
|
28
|
+
|
29
|
+
if RUBY_VERSION >= "1.9"
|
30
|
+
def respond_to_missing?(method_name, include_private=false)
|
31
|
+
client.respond_to?(method_name, include_private)
|
32
|
+
end
|
33
|
+
else
|
34
|
+
def respond_to?(method_name, include_private=false)
|
35
|
+
client.respond_to?(method_name, include_private) || super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def method_missing(method_name, *args, &block)
|
42
|
+
return super unless client.respond_to?(method_name)
|
43
|
+
client.send(method_name, *args, &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
data/lib/tango/client.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'tango/default'
|
2
|
+
require 'tango/error'
|
3
|
+
|
4
|
+
require 'uri'
|
5
|
+
require 'faraday'
|
6
|
+
|
7
|
+
module Tango
|
8
|
+
class Client
|
9
|
+
# @return [Hash]
|
10
|
+
attr_accessor :options
|
11
|
+
|
12
|
+
# Initialize an instance with specified options. Unless an option is
|
13
|
+
# specified, the corresponding value from {Tango::Default.options} is used.
|
14
|
+
#
|
15
|
+
# @param [Hash] options
|
16
|
+
# @option options
|
17
|
+
# @option options [String] username Tango account username
|
18
|
+
# @option options [String] password Tango account password
|
19
|
+
# @option options [String] endpoint API endpoint, the whole API URL prefix
|
20
|
+
# is "endpoint/version".
|
21
|
+
# @option options [String] version version part of API URL
|
22
|
+
# @option options [Faraday::Builder] middleware Faraday's middleware stack
|
23
|
+
# @option options [Hash] connection_options Further options for Faraday connection
|
24
|
+
def initialize(options = {})
|
25
|
+
@options = ::Tango::Default.options.merge(options)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Perform an HTTP POST request
|
29
|
+
def post(path, params = {})
|
30
|
+
request(:post, path, params)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get user available balance
|
34
|
+
# @return [Integer] balance in cents (100 = $1.00)
|
35
|
+
# @see https://github.com/tangocarddev/General/blob/master/Tango_Card_Service_API.md#getavailablebalance
|
36
|
+
def get_available_balance
|
37
|
+
response = post 'GetAvailableBalance'
|
38
|
+
balance = response[:body][:availableBalance]
|
39
|
+
end
|
40
|
+
|
41
|
+
# Purchase a card
|
42
|
+
# @params params [Hash] Request parameters. Parameter username and
|
43
|
+
# password are not used, specify them through client options.
|
44
|
+
# @return [Hash] "response" part of the returned JSON. All keys are symbols.
|
45
|
+
# @see https://github.com/tangocarddev/General/blob/master/Tango_Card_Service_API.md#purchasecard
|
46
|
+
def purchase_card(params = {})
|
47
|
+
response = post 'PurchaseCard', params
|
48
|
+
response[:body]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns a Faraday::Connection object
|
52
|
+
#
|
53
|
+
# @return [Faraday::Connection]
|
54
|
+
def connection
|
55
|
+
@connection ||= Faraday.new(endpoint, connection_options)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Constructs endpoint from options
|
59
|
+
#
|
60
|
+
# @return [String]
|
61
|
+
def endpoint
|
62
|
+
options.values_at(:endpoint, :version).join('/')
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def request(method, path, params = {})
|
68
|
+
path = path.sub(/^\//, '')
|
69
|
+
params = params.merge credentials
|
70
|
+
connection.send(method.to_sym, path, params).env
|
71
|
+
rescue Faraday::Error::ClientError
|
72
|
+
raise Tango::Error::ClientError
|
73
|
+
rescue MultiJson::DecodeError
|
74
|
+
raise Tango::Error::DecodeError
|
75
|
+
end
|
76
|
+
|
77
|
+
# Account credentials.
|
78
|
+
#
|
79
|
+
# @return [Hash] Account credentials
|
80
|
+
def credentials
|
81
|
+
{
|
82
|
+
:username => options[:username],
|
83
|
+
:password => options[:password]
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
def connection_options
|
88
|
+
options[:connection_options].merge(:builder => options[:middleware])
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'tango/version'
|
2
|
+
require 'tango/error'
|
3
|
+
require 'tango/request/json_encoded'
|
4
|
+
require 'tango/response/parse_json'
|
5
|
+
require 'tango/response/raise_error'
|
6
|
+
require 'faraday'
|
7
|
+
|
8
|
+
module Tango
|
9
|
+
module Default
|
10
|
+
INTEGRATION_ENDPOINT = 'https://int.tangocard.com' unless defined?(::Tango::Default::INTEGRATION_ENDPOINT)
|
11
|
+
PRODUCTION_ENDPOINT = 'https://api.tangocard.com' unless defined?(::Tango::Default::PRODUCTION_ENDPOINT)
|
12
|
+
|
13
|
+
ENDPOINT = INTEGRATION_ENDPOINT unless defined?(::Tango::Default::ENDPOINT)
|
14
|
+
|
15
|
+
MIDDLEWARE = Faraday::Builder.new do |builder|
|
16
|
+
# Encode request params into JSON for PUT/POST requests
|
17
|
+
builder.use ::Tango::Request::JsonEncoded
|
18
|
+
|
19
|
+
builder.use ::Tango::Response::RaiseError, ::Tango::Error::ClientError
|
20
|
+
|
21
|
+
# Parse response JSON
|
22
|
+
builder.use ::Tango::Response::ParseJson
|
23
|
+
|
24
|
+
builder.use ::Tango::Response::RaiseError, ::Tango::Error::ServerError
|
25
|
+
|
26
|
+
builder.adapter Faraday.default_adapter
|
27
|
+
|
28
|
+
end unless defined?(::Tango::Default::MIDDLEWARE)
|
29
|
+
|
30
|
+
CONNECTION_OPTIONS = {
|
31
|
+
:headers => {
|
32
|
+
:accept => 'application/json',
|
33
|
+
:user_agent => "TangoClient Ruby Gem #{::Tango::VERSION}"
|
34
|
+
},
|
35
|
+
:open_timeout => 5,
|
36
|
+
:raw => true,
|
37
|
+
:ssl => {
|
38
|
+
:ca_file => File.expand_path('../ssl/cacert.pem')
|
39
|
+
},
|
40
|
+
:timeout => 10,
|
41
|
+
} unless defined?(::Tango::Default::CONNECTION_OPTIONS)
|
42
|
+
|
43
|
+
VERSION = 'Version2' unless defined?(::Tango::Default::VERSION)
|
44
|
+
|
45
|
+
def self.options
|
46
|
+
@options ||= {
|
47
|
+
:username => ENV['TANGO_USERNAME'] || 'third_party_int@tangocard.com',
|
48
|
+
:password => ENV['TANGO_PASSWORD'] || 'integrateme',
|
49
|
+
:endpoint => ENV['TANGO_ENDPOINT'] || ENDPOINT,
|
50
|
+
:version => VERSION,
|
51
|
+
:middleware => MIDDLEWARE.dup,
|
52
|
+
:connection_options => CONNECTION_OPTIONS.clone
|
53
|
+
}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/tango/error.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
module Tango
|
2
|
+
class Error < StandardError
|
3
|
+
attr_accessor :status_code
|
4
|
+
|
5
|
+
# Creates a new Error object from status_code
|
6
|
+
#
|
7
|
+
# @param status_code [Integer] HTTP response status code
|
8
|
+
# @return [Tango::Error]
|
9
|
+
def self.from_status_code(status_code)
|
10
|
+
ex = new("Error: #{status_code}")
|
11
|
+
ex.status_code = status_code
|
12
|
+
ex
|
13
|
+
end
|
14
|
+
|
15
|
+
# Initializes a new Error object
|
16
|
+
#
|
17
|
+
# @param exception [Exception, String]
|
18
|
+
# @return [Tango::Error]
|
19
|
+
def initialize(exception = $!)
|
20
|
+
@wrapped_exception = exception
|
21
|
+
exception.respond_to?(:backtrace) ? super(exception.message) : super(exception.to_s)
|
22
|
+
end
|
23
|
+
|
24
|
+
def backtrace
|
25
|
+
@wrapped_exception.respond_to?(:backtrace) ? @wrapped_exception.backtrace : super
|
26
|
+
end
|
27
|
+
|
28
|
+
class ClientError < Tango::Error
|
29
|
+
def self.raise_on?(status_code)
|
30
|
+
400 <= status_code && status_code < 500
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class ServerError < Tango::Error;
|
35
|
+
def self.raise_on?(status_code)
|
36
|
+
500 <= status_code && status_code < 600
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_accessor :response
|
40
|
+
|
41
|
+
# Fetches value from response by key
|
42
|
+
#
|
43
|
+
# @param key [Symbol]
|
44
|
+
def [](key)
|
45
|
+
response[key] if response
|
46
|
+
end
|
47
|
+
|
48
|
+
# Convert response type to class name
|
49
|
+
def self.response_type_to_class_name(type)
|
50
|
+
type.split('_').collect(&:capitalize).join('')
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.from_response(body)
|
54
|
+
if body.is_a?(Hash) && body[:responseType]
|
55
|
+
type = body[:responseType]
|
56
|
+
class_name = response_type_to_class_name(type)
|
57
|
+
if Tango::Error.const_defined?(class_name)
|
58
|
+
ex = Tango::Error.const_get(class_name).new(body[:response])
|
59
|
+
else
|
60
|
+
ex = new(body[:response])
|
61
|
+
end
|
62
|
+
ex.response = body[:response]
|
63
|
+
ex
|
64
|
+
else
|
65
|
+
new("Invalid response: #{body.to_s}")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class InvCredential < ServerError; end
|
71
|
+
class InvInput < ServerError; end
|
72
|
+
class InsInv < ServerError; end
|
73
|
+
class InsFunds < ServerError; end
|
74
|
+
class SysError < ServerError; end
|
75
|
+
|
76
|
+
class DecodeError < Tango::Error; end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'multi_json'
|
3
|
+
|
4
|
+
module Tango
|
5
|
+
module Request
|
6
|
+
class JsonEncoded < Faraday::Response::Middleware
|
7
|
+
CONTENT_TYPE = 'Content-Type'.freeze
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :mime_type
|
11
|
+
end
|
12
|
+
self.mime_type = 'application/json'.freeze
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
match_content_type(env) do |data|
|
16
|
+
env[:body] = MultiJson.dump data
|
17
|
+
end
|
18
|
+
@app.call env
|
19
|
+
end
|
20
|
+
|
21
|
+
def match_content_type(env)
|
22
|
+
if process_request?(env)
|
23
|
+
env[:request_headers][CONTENT_TYPE] ||= self.class.mime_type
|
24
|
+
yield env[:body] unless env[:body].respond_to?(:to_str)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def process_request?(env)
|
29
|
+
type = request_type(env)
|
30
|
+
env[:body] and (type.empty? or type == self.class.mime_type)
|
31
|
+
end
|
32
|
+
|
33
|
+
def request_type(env)
|
34
|
+
type = env[:request_headers][CONTENT_TYPE].to_s
|
35
|
+
type = type.split(';', 2).first if type.index(';')
|
36
|
+
type
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'multi_json'
|
3
|
+
|
4
|
+
require 'tango/error'
|
5
|
+
|
6
|
+
module Tango
|
7
|
+
module Response
|
8
|
+
class ParseJson < Faraday::Response::Middleware
|
9
|
+
|
10
|
+
def parse(body)
|
11
|
+
case body
|
12
|
+
when /\A^\s*$\z/, nil
|
13
|
+
nil
|
14
|
+
else
|
15
|
+
json = MultiJson.load(body, :symbolize_keys => true)
|
16
|
+
unless json.is_a?(Hash) && json[:response].is_a?(Hash) && json[:responseType] == 'SUCCESS'
|
17
|
+
raise ::Tango::Error::ServerError.from_response(json)
|
18
|
+
end
|
19
|
+
|
20
|
+
json[:response]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def on_complete(env)
|
25
|
+
if respond_to?(:parse)
|
26
|
+
env[:body] = parse(env[:body])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|