tango-client 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private --protected lib/**/*.rb - README.md
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tango-client.gemspec
4
+ gemspec
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
@@ -0,0 +1,5 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
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
@@ -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
@@ -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