ribbon-flow 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 07298ede14e9864a03a7e433a43b9e387c540ee1
4
+ data.tar.gz: 757937ae1b637a82724df958ab7b6a0a641889dc
5
+ SHA512:
6
+ metadata.gz: a3950406361a0e8ac3f0e5ef27e3103466115a54ee6ab4dfd4dac89b34407a670ea5f9b7423ec03e98e8fd15064a1602bbcab940c859703afd7d1269b1fc637c
7
+ data.tar.gz: c1555ebc95da219a0f1d2b65b155039a49f8741eccd53d7086f712bb4fff39ecea6bf3248b051de1cff6a4fc6736c703d7b82ef2341f8469c7a7f07a6caa7feb
Binary file
@@ -0,0 +1,2 @@
1
+ ���)�&4��2�?A�� ��/��7�B����"I:���~�_u���ր��lh�ͷ���z�w�)c���d3p y��5PK��^�Y�6}�
2
+ �j��Sp�;�+����:��GY��%��� �#��'0C�uxF�e��Ԩ5��%Y�E��V&�\���W�v��z. �x^�\��ɩ�U}m�Z�
@@ -0,0 +1,31 @@
1
+
2
+ require 'flow/version'
3
+ require 'flow/errors'
4
+ require 'flow/config'
5
+ require 'flow/connection'
6
+ require 'flow/models'
7
+ require 'flow/response'
8
+
9
+ module Flow
10
+
11
+ def self.config
12
+ Flow::Config.instance
13
+ end
14
+
15
+ def self.connection
16
+ @__connection_cache ||= Flow::Connection.new(config.api_key, config.url)
17
+ end
18
+
19
+ def self.pool(pool_token)
20
+ Flow::Models::Pool.load(connection, pool_token)
21
+ end
22
+
23
+ def self.card(card_token)
24
+ Flow::Models::Card.load(connection, card_token)
25
+ end
26
+
27
+ def self.transaction(transaction_token)
28
+ Flow::Models::Transaction.load(connection, transaction_token)
29
+ end
30
+
31
+ end
@@ -0,0 +1,50 @@
1
+
2
+ require 'singleton'
3
+
4
+ module Flow
5
+ class Config
6
+ include Singleton
7
+
8
+ def initialize
9
+ @config = self.defaults
10
+ end
11
+
12
+ def defaults
13
+ {
14
+ url: 'https://flow.ribbon.co'.freeze,
15
+ api_key: nil
16
+ }
17
+ end
18
+
19
+ def method_missing(meth, *args, &block)
20
+ meth_str = meth.to_s
21
+
22
+ if /^(\w+)=$/.match(meth_str)
23
+ _set($1, args.first)
24
+ else
25
+ _get(meth)
26
+ end
27
+ end
28
+
29
+ def url=(url)
30
+ url = url[0..-2] if url[-1] == '/' # No trailing forward-slash
31
+ _set(:url, url)
32
+ end
33
+
34
+ def to_s
35
+ return @config.to_s
36
+ end
37
+
38
+ private
39
+
40
+ def _set(key, value)
41
+ # Changing the config value should require calling the
42
+ # _set method again.
43
+ return @config[key.to_sym] = value.dup.freeze
44
+ end
45
+
46
+ def _get(key)
47
+ return @config[key.to_sym]
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,187 @@
1
+
2
+ require 'rest-client'
3
+ require 'openssl'
4
+ require 'securerandom'
5
+
6
+ module Flow
7
+
8
+ class Connection
9
+ attr_reader :url
10
+ attr_reader :access_key
11
+ attr_reader :version
12
+
13
+ attr_reader :scheme
14
+ attr_reader :host
15
+ attr_reader :port
16
+
17
+ def initialize(api_key, url='https://flow.ribbon.co', version=1)
18
+ @version = version
19
+
20
+ @url = (url[-1] == '/' ? url[0..-2] : url).dup.freeze # Remove trailing slash.
21
+
22
+ if /^([A-Za-z0-9]{20})\.([A-Za-z0-9]{43})$/.match(api_key)
23
+ @access_key = $1.dup.freeze
24
+ @signing_key = $2.dup.freeze
25
+ else
26
+ raise Flow::Errors::ConnectionError, "Invalid API Key format."
27
+ end
28
+
29
+ URI.parse(@url).tap do |uri|
30
+ @scheme = uri.scheme
31
+ @host = uri.host
32
+ @port = uri.port
33
+ end
34
+
35
+ # RestClient::Resource.new(
36
+ # 'https://example.com',
37
+ # :ssl_client_cert => OpenSSL::X509::Certificate.new(File.read("cert.pem")),
38
+ # :ssl_client_key => OpenSSL::PKey::RSA.new(File.read("key.pem"), "passphrase, if any"),
39
+ # :ssl_ca_file => "ca_certificate.pem",
40
+ # :verify_ssl => OpenSSL::SSL::VERIFY_PEER
41
+ # )
42
+
43
+ @api = RestClient::Resource.new(
44
+ @url,
45
+ verify_ssl: true,
46
+ headers: {
47
+ "Content-Type" => "application/json"
48
+ }
49
+ ) {|response, request, result| response}
50
+ end
51
+
52
+ def get(path, params={})
53
+ path = _build_path(path)
54
+ auth_header = _authorization_header(:get, path, params)
55
+ response = @api[path].get(authorization: auth_header, params: params)
56
+ Flow::Response.new(self, response)
57
+ end
58
+
59
+ def post(path, params={}, &block)
60
+ if block_given?
61
+ body_hash = block.call
62
+ if body_hash && !body_hash.empty?
63
+ body = body_hash.to_json
64
+ end
65
+ end
66
+
67
+ path = _build_path(path)
68
+ auth_header = _authorization_header(:post, path, params, body)
69
+ response = @api[path].post(body, authorization: auth_header, params: params)
70
+
71
+ Flow::Response.new(self, response)
72
+ end
73
+
74
+ private
75
+
76
+ def _signing_key
77
+ @signing_key
78
+ end
79
+
80
+ def _build_path(path)
81
+ path = '/' + path unless path[0] == '/'
82
+
83
+ if path.start_with?('/api/')
84
+ return path
85
+ else
86
+ return "/api/v#{version}#{path}"
87
+ end
88
+ end
89
+
90
+ def _gen_nonce
91
+ begin
92
+ nonce = SecureRandom.hex(8)
93
+ end until _valid_nonce?(nonce)
94
+
95
+ return nonce.freeze
96
+ end
97
+
98
+ def _valid_nonce?(nonce)
99
+ record = @__nonce_record ||= {}
100
+ nonce = nonce.to_s
101
+
102
+ # Try to limit the record store to about 2MB
103
+ # of data, assuming 16 single-byte characters
104
+ # and 4 byte timestamps.
105
+ # Supports 3,333 requests per second before exceeding
106
+ # the limit.
107
+ if record.length > 100_000
108
+ threshold_ts = (Time.now.to_i - 30).freeze
109
+ record.delete_if {|k,v| v < threshold_ts}
110
+ end
111
+
112
+ if record[nonce] && record[nonce] >= (Time.now.to_i - 30)
113
+ return false
114
+ else
115
+ record[nonce] = Time.now.to_i.freeze
116
+ return true
117
+ end
118
+ end
119
+
120
+ def _authorization_header(verb, path, query_hash, body=nil)
121
+ timestamp = Time.now.to_i.freeze
122
+ nonce = _gen_nonce
123
+
124
+ # Calculate request hash
125
+ path = _uri_encode(path[0] == '/' ? path : ('/' + path), false)
126
+ query_string = _hash_to_query_string(query_hash)
127
+ body_hash = _sha256(body.to_s)
128
+ request_hash = _sha256(
129
+ verb.to_s.upcase + "\n" +
130
+ host + "\n" +
131
+ path + "\n" +
132
+ query_string + "\n" +
133
+ body_hash
134
+ )
135
+
136
+ # Construct the message
137
+ message = timestamp.to_s + nonce + request_hash
138
+
139
+ signature = _hmac_sha256_hex(_signing_key, message)
140
+
141
+ return "RBN1-HMAC-SHA256 #{access_key};#{timestamp};#{nonce};#{signature}"
142
+ end
143
+
144
+ ######################################################################
145
+ #
146
+ # Encoding and signing helper methods
147
+ #
148
+ ######################################################################
149
+
150
+ def _hmac_sha256_hex(key, message)
151
+ digest = OpenSSL::Digest::SHA256.new
152
+ OpenSSL::HMAC.hexdigest(digest, key, message)
153
+ end
154
+
155
+ def _sha256(str)
156
+ ::OpenSSL::Digest::SHA256.hexdigest(str)
157
+ end
158
+
159
+ def _uri_encode(string, encode_fslash=true)
160
+ retval = ""
161
+
162
+ string.chars.each do |ch|
163
+ if (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') ||
164
+ (ch >= '0' && ch <= '9') || ch == '-' || ch == '_' || ch == '.' || ch == '~'
165
+ retval << ch
166
+ elsif ch == '/'
167
+ retval << (encode_fslash ? '%2F' : ch)
168
+ else
169
+ retval << '%' << ch.bytes.first.to_s(16).upcase
170
+ end
171
+ end
172
+
173
+ return retval
174
+ end
175
+
176
+ def _hash_to_query_string(data)
177
+ pairs = []
178
+
179
+ data.sort{|a,b| a[0] <=> b[0]}.each do |key, value|
180
+ pairs << (_uri_encode(key.to_s) << '=' << _uri_encode(value.to_s))
181
+ end
182
+
183
+ pairs.join('&')
184
+ end
185
+ end
186
+
187
+ end
@@ -0,0 +1,12 @@
1
+
2
+
3
+ module Flow
4
+ module Errors
5
+
6
+ class FlowError < StandardError; end
7
+ class ConnectionError < FlowError; end
8
+ class ResponseError < FlowError; end
9
+ class LoadError < FlowError; end
10
+
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ require 'flow/models/model'
2
+ require 'flow/models/pool'
3
+ require 'flow/models/card'
4
+ require 'flow/models/transaction'
@@ -0,0 +1,39 @@
1
+ module Flow
2
+ module Models
3
+ class Card < Model
4
+
5
+ def self.load(connection, card_token)
6
+ r = connection.get("cards/#{card_token}")
7
+
8
+ raise Flow::Errors::LoadError.new(r.message) unless r.success?
9
+
10
+ return r.card
11
+ end
12
+
13
+ def ready?
14
+ self.status == 'ready'
15
+ end
16
+
17
+ def supported?
18
+ self.ready?
19
+ end
20
+
21
+ def unsupported?
22
+ self.status == 'unsupported'
23
+ end
24
+
25
+ def credit!(amount, options={})
26
+ r = connection.post("cards/#{self.token}/credits") do
27
+ {
28
+ amount: amount
29
+ }
30
+ end
31
+
32
+ raise Flow::Errors::ResponseError.new(r.message) unless r.success?
33
+
34
+ r.transaction
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
1
+
2
+ module Flow
3
+ module Models
4
+
5
+ class Model
6
+ attr_reader :model_hash
7
+ attr_reader :connection
8
+
9
+ def initialize(connection, model_hash)
10
+ @connection = connection
11
+ @model_hash = model_hash
12
+ end
13
+
14
+ def method_missing(meth, *args, &block)
15
+ if model_hash.key?(meth.to_s)
16
+ return Model.load_model(meth, connection, model_hash[meth.to_s])
17
+ end
18
+
19
+ super
20
+ end
21
+
22
+ def self.load_model(name, connection, model_hash)
23
+ name = name.to_sym
24
+
25
+ case name
26
+ when :pool
27
+ return Flow::Models::Pool.new(connection, model_hash)
28
+ when :card
29
+ return Flow::Models::Card.new(connection, model_hash)
30
+ when :transaction
31
+ return Flow::Models::Transaction.new(connection, model_hash)
32
+ else
33
+ return model_hash
34
+ end
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,15 @@
1
+ module Flow
2
+ module Models
3
+ class Pool < Model
4
+
5
+ def self.load(connection, pool_token)
6
+ r = connection.get("pools/#{pool_token}")
7
+
8
+ raise Flow::Errors::LoadError.new(r.message) unless r.success?
9
+
10
+ return r.pool
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ module Flow
2
+ module Models
3
+ class Transaction < Model
4
+
5
+ def self.load(connection, tx_token)
6
+ r = connection.get("transactions/#{tx_token}")
7
+
8
+ raise Flow::Errors::LoadError.new(r.message) unless r.success?
9
+
10
+ return r.transaction
11
+ end
12
+
13
+ def pending?
14
+ self.status == 'pending'
15
+ end
16
+
17
+ def scheduled?
18
+ self.status == 'scheduled'
19
+ end
20
+
21
+ def cleared?
22
+ self.status == 'cleared'
23
+ end
24
+
25
+ def failed?
26
+ self.status == 'failed'
27
+ end
28
+
29
+ def error?
30
+ self.status == 'error'
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,58 @@
1
+
2
+ require 'json'
3
+
4
+ module Flow
5
+
6
+ class Response
7
+ attr_reader :body
8
+ attr_reader :connection
9
+
10
+ def initialize(connection, rest_client_response)
11
+ @connection = connection
12
+ @response = rest_client_response
13
+
14
+ begin
15
+ body = @response.body
16
+ @body = JSON.parse(body) unless body.empty?
17
+ @body ||= {}
18
+ _deep_freeze(@body)
19
+ rescue JSON::ParserError => e
20
+ raise Flow::Errors::ResponseError, "Invalid JSON returned: #{e.message}"
21
+ end
22
+ end
23
+
24
+ def http_success?
25
+ @response.code >= 200 && @response.code < 300
26
+ end
27
+
28
+ def success?
29
+ body['status'] == 'success'
30
+ end
31
+
32
+ def error?
33
+ body['status'] == 'error'
34
+ end
35
+
36
+ def method_missing(meth, *args, &block)
37
+ if body.key?(meth.to_s)
38
+ return Flow::Models::Model.load_model(meth, connection, body[meth.to_s])
39
+ end
40
+
41
+ super
42
+ end
43
+
44
+
45
+ private
46
+
47
+ def _deep_freeze(obj)
48
+ if obj.respond_to?(:each)
49
+ obj.each do |elem|
50
+ _deep_freeze(elem)
51
+ end
52
+ end
53
+
54
+ obj.freeze
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,4 @@
1
+
2
+ module Flow
3
+ VERSION = '0.0.1'
4
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ribbon-flow
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Robert Honer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIDfDCCAmSgAwIBAgIBATANBgkqhkiG9w0BAQUFADBCMRQwEgYDVQQDDAtlbmdp
14
+ bmVlcmluZzEWMBQGCgmSJomT8ixkARkWBnJpYmJvbjESMBAGCgmSJomT8ixkARkW
15
+ AmNvMB4XDTE0MDYwOTE2MjY0NFoXDTE1MDYwOTE2MjY0NFowQjEUMBIGA1UEAwwL
16
+ ZW5naW5lZXJpbmcxFjAUBgoJkiaJk/IsZAEZFgZyaWJib24xEjAQBgoJkiaJk/Is
17
+ ZAEZFgJjbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOqAiBev9e7J
18
+ D3X3A+SXpIbnG71jweN7Q7+QMhscA6+s8X91aG2cvNPpk7OSMaoU7Q81v+I9VMYu
19
+ n0JhOh/O86nYv0Ig/FKcDRzZIGgY0bjJV/5mokQ45xgseUTjJ3ocqOXncRPNx4xm
20
+ Am3eyb43aDqUFnowXcH84ZXMTFew+dekov6644pN14u8aXGWPmZxOQhTOi5+ESAe
21
+ +RON7v2xRq1OFcupEHeKHMhAMCbOy19NqTq/1yA8Pd940OfVRisCnXoQqOCP9H86
22
+ gakm8XcLrUdJupV6OxCMd5NZ3itRTFEWOMy6ZRkj2IVdFQ4ZPQjIHR2f253DF7BW
23
+ dUStbmkKuyECAwEAAaN9MHswCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
24
+ BBYEFPjdKqT6jDUZj755LAOopw41RGO/MCAGA1UdEQQZMBeBFWVuZ2luZWVyaW5n
25
+ QHJpYmJvbi5jbzAgBgNVHRIEGTAXgRVlbmdpbmVlcmluZ0ByaWJib24uY28wDQYJ
26
+ KoZIhvcNAQEFBQADggEBACmYnS5b0mBhurs/U+oTUiY6nL4/xTx1poZxUNJ9Emag
27
+ y/vnnbMxiZXXz4Y6vWdFiOg02N6BiBrvyjzkleOyw41lcNDj9pH6HyLFAKR4lU5V
28
+ HKaahXt474aWouDfvENcaINNtgLrPAqisgRckuyzfM/bU1a8Aj2OSLsSEh69giTi
29
+ 0WxI115SHQG+8CyzpRQIBteomCInG1ymSyvEUxpytSW39KBVKViB84Ss5vcMvtv7
30
+ grVI0T63q4/t9yjlCPkbK1VTObseWNPA2/7twecdkCVN3VaWEml4xf2KiOwnKDfk
31
+ aZOXvndbbL+k3uaxs/Fpfsi0AD02HiceGjSOZFd9Wyk=
32
+ -----END CERTIFICATE-----
33
+ date: 2014-06-09 00:00:00.000000000 Z
34
+ dependencies:
35
+ - !ruby/object:Gem::Dependency
36
+ name: rest-client
37
+ requirement: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ description: With this gem, you can connect and perform any operation on Ribbon Flow.
50
+ It supports loading pool, card and transaction tokens. You can also easily credit
51
+ a card.
52
+ email: engineering@ribbon.co
53
+ executables: []
54
+ extensions: []
55
+ extra_rdoc_files: []
56
+ files:
57
+ - lib/flow/config.rb
58
+ - lib/flow/connection.rb
59
+ - lib/flow/errors.rb
60
+ - lib/flow/models/card.rb
61
+ - lib/flow/models/model.rb
62
+ - lib/flow/models/pool.rb
63
+ - lib/flow/models/transaction.rb
64
+ - lib/flow/models.rb
65
+ - lib/flow/response.rb
66
+ - lib/flow/version.rb
67
+ - lib/flow.rb
68
+ homepage: https://flow.ribbon.co
69
+ licenses:
70
+ - BSD
71
+ metadata: {}
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubyforge_project:
88
+ rubygems_version: 2.0.3
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: Ruby SDK for the Ribbon Flow API.
92
+ test_files: []
Binary file