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 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