tango-client 1.0.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.
- 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
|