Skip to content

Commit 59760af

Browse files
committed
Initial working version
1 parent 0590b98 commit 59760af

17 files changed

+284
-19
lines changed

Gemfile

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ gemspec
55

66
gem "rake", "~> 12.0"
77
gem "minitest", "~> 5.0"
8+
gem "webmock", "~> 3.18"

lib/teller.rb

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
require "teller/version"
22
require "teller/config"
3+
require "teller/resource"
4+
require "teller/collection_resource"
5+
require "teller/http"
6+
require "teller/client"
7+
38

49
module Teller
5-
class Error < StandardError; end
6-
# Your code goes here...
710
end

lib/teller/client.rb

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module Teller
2+
class Client
3+
URL = "https://api.teller.io".freeze
4+
5+
def initialize(overrides={})
6+
config = Teller::Config.dup
7+
config.setup(overrides)
8+
9+
client = Teller::HTTP.new(config)
10+
entrypoint = client.get(URL)
11+
@target = Teller::Resource.new(entrypoint, client)
12+
end
13+
14+
def method_missing(method, *args, &block)
15+
if @target.respond_to?(method)
16+
@target.send(method, *args, &block)
17+
else
18+
super
19+
end
20+
end
21+
22+
def respond_to_missing?(method, include_private = false)
23+
@target.respond_to?(method)
24+
end
25+
end
26+
end

lib/teller/collection_resource.rb

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
module Teller
2+
class CollectionResource
3+
def initialize(url, collection, client)
4+
@url = url
5+
@collection = collection.map { |i| Teller::Resource.new(i, client) }
6+
@client = client
7+
end
8+
9+
def get(id)
10+
url = @url + "/" + id
11+
Teller::Resource.new(url, @client.get(url), @client)
12+
end
13+
14+
def reload
15+
@collection = @client.get(@url).map { |i| Teller::Resource.new(i, @client) }
16+
self
17+
end
18+
19+
def method_missing(method, *args, &block)
20+
if @collection.respond_to?(method)
21+
@collection.send(method, *args, &block)
22+
else
23+
super
24+
end
25+
end
26+
27+
def respond_to_missing?(method, include_private = false)
28+
@collection.respond_to?(method) || super
29+
end
30+
31+
def inspect
32+
@collection
33+
end
34+
end
35+
end

lib/teller/config.rb

+22-14
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,34 @@ def setup(opts = {})
1414
end
1515

1616
def certificate=(path)
17-
begin
18-
File.open(path, 'r') do |file|
19-
@certificate = OpenSSL::X509::Certificate.new(file)
17+
if path
18+
begin
19+
File.open(path, 'r') do |file|
20+
@certificate = OpenSSL::X509::Certificate.new(file)
21+
end
22+
rescue Errno::ENOENT
23+
raise Error, "Certificate file not found: #{path}"
24+
rescue OpenSSL::X509::CertificateError
25+
raise Error, "Invalid certificate data in file: #{path}"
2026
end
21-
rescue Errno::ENOENT
22-
raise Error, "Certificate file not found: #{path}"
23-
rescue OpenSSL::X509::CertificateError
24-
raise Error, "Invalid certificate data in file: #{path}"
27+
else
28+
@certificate = nil
2529
end
2630
end
2731

2832
def private_key=(path)
29-
begin
30-
File.open(path, 'r') do |file|
31-
@private_key = OpenSSL::PKey.read(file, nil)
33+
if path
34+
begin
35+
File.open(path, 'r') do |file|
36+
@private_key = OpenSSL::PKey.read(file, nil)
37+
end
38+
rescue Errno::ENOENT
39+
raise Error, "Private key file not found: #{path}"
40+
rescue OpenSSL::PKey::PKeyError
41+
raise Error, "Invalid private key data in file: #{path}"
3242
end
33-
rescue Errno::ENOENT
34-
raise Error, "Private key file not found: #{path}"
35-
rescue OpenSSL::PKey::PKeyError
36-
raise Error, "Invalid private key data in file: #{path}"
43+
else
44+
@private_key = nil
3745
end
3846
end
3947

lib/teller/http.rb

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
class Teller::HTTP
2+
def initialize(config)
3+
@config = config
4+
end
5+
6+
def get(url)
7+
uri = URI.parse(url)
8+
9+
http = Net::HTTP.new(uri.host, uri.port)
10+
http.use_ssl = true
11+
12+
if @config.certificate && @config.private_key
13+
http.cert = @config.certificate
14+
http.key = @config.private_key
15+
end
16+
17+
request = Net::HTTP::Get.new(uri.request_uri)
18+
19+
if @config.access_token
20+
request.add_field('Authorization', "Basic #{Base64.strict_encode64("#{@config.access_token}:")}")
21+
end
22+
23+
response = http.request(request)
24+
25+
JSON.parse(response.body)
26+
end
27+
end

lib/teller/resource.rb

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
require 'json'
2+
require 'net/http'
3+
require 'uri'
4+
require 'base64'
5+
require 'ostruct'
6+
7+
class Teller::Resource < OpenStruct
8+
def initialize(state, client)
9+
@client = client
10+
super(state)
11+
end
12+
13+
def method_missing(method, *args, &block)
14+
if table[:links].key?(method.to_s)
15+
table[method] ||= subresource(table[:links][method.to_s])
16+
table[method]
17+
else
18+
super
19+
end
20+
end
21+
22+
def reload
23+
initialize(@client.get(@table[:links].self), @client)
24+
self
25+
end
26+
27+
def respond_to_missing?(method, include_private = false)
28+
table[:links].key?(method.to_s) || super
29+
end
30+
31+
private
32+
33+
def subresource(link)
34+
state = @client.get(link)
35+
36+
if state.is_a?(Array)
37+
Teller::CollectionResource.new(link, state, @client)
38+
else
39+
Teller::Resource.new(state, @client)
40+
end
41+
end
42+
end

test/client_test.rb

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
require "test_helper"
2+
3+
describe Teller::Config do
4+
let(:client) { Teller::Client.new(access_token: "test_token_ky6igyqi3qxa4") }
5+
6+
before do
7+
stub_request(:get, /^https:\/\/api.teller.io\//).
8+
with(basic_auth: ["test_token_ky6igyqi3qxa4", ""]).
9+
to_return do |request|
10+
file = request.uri.path == "/" ? "root" : request.uri.path
11+
File.open(File.join "test/fixtures/json", "#{file}.json")
12+
end
13+
end
14+
15+
it "use access tokens for bearer auth" do
16+
client.accounts
17+
18+
assert_requested :get, "https://api.teller.io/accounts",
19+
headers: {'Authorization'=>'Basic dGVzdF90b2tlbl9reTZpZ3lxaTNxeGE0Og=='}
20+
end
21+
22+
it "fetches each uri once and caches the response" do
23+
client.accounts
24+
client.accounts
25+
26+
assert_requested :get, "https://api.teller.io/accounts", times: 1
27+
end
28+
29+
it "reloads uris when required to" do
30+
client.accounts
31+
client.accounts.reload
32+
33+
assert_requested :get, "https://api.teller.io/accounts", times: 2
34+
end
35+
36+
it "maps response properties to object instance methods" do
37+
json = JSON.parse(File.read("test/fixtures/json/raw_account.json"))
38+
account = client.accounts.first
39+
40+
json.delete "links"
41+
42+
json.each do |k, v|
43+
_(account.send k).must_equal v
44+
end
45+
end
46+
47+
it "maps links to object instance methods and loads them on demand" do
48+
json = JSON.parse(File.read("test/fixtures/json/raw_account.json"))
49+
account = client.accounts.first
50+
51+
json["links"].each do |k, v|
52+
account.send k
53+
end
54+
55+
assert_requested :get, "https://api.teller.io/accounts/acc_oiin624kqjrg2mp2ea000", times: 1
56+
assert_requested :get, "https://api.teller.io/accounts/acc_oiin624kqjrg2mp2ea000/transactions", times: 1
57+
assert_requested :get, "https://api.teller.io/accounts/acc_oiin624kqjrg2mp2ea000/balances", times: 1
58+
end
59+
60+
it "can fetch arbitrary members of a resource collection" do
61+
client.accounts.get("acc_oiin624kqjrg2mp2ea000")
62+
63+
assert_requested :get, "https://api.teller.io/accounts/acc_oiin624kqjrg2mp2ea000", times: 1
64+
end
65+
end

test/config_test.rb

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
require "minitest/autorun"
2-
require "teller"
1+
require "test_helper"
32

43
describe Teller::Config do
54
let(:config) { Teller::Config }
@@ -67,5 +66,4 @@
6766
config.private_key = "test/fixtures/invalid.pem"
6867
end
6968
end
70-
7169
end

test/fixtures/json/accounts.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
HTTP/1.1 200 OK
2+
cache-control: max-age=0, private, must-revalidate
3+
content-length: 1664
4+
content-type: application/json; charset=utf-8
5+
date: Sun, 23 Jul 2023 16:19:07 GMT
6+
server: Teller API
7+
x-request-id: F3SLgV1PMVW-rD8AckEx
8+
9+
[{"type":"credit","subtype":"credit_card","status":"open","name":"Platinum Card","links":{"transactions":"https://api.teller.io/accounts/acc_oiin624kqjrg2mp2ea000/transactions","self":"https://api.teller.io/accounts/acc_oiin624kqjrg2mp2ea000","balances":"https://api.teller.io/accounts/acc_oiin624kqjrg2mp2ea000/balances"},"last_four":"7857","institution":{"name":"Security Credit Union","id":"security_cu"},"id":"acc_oiin624kqjrg2mp2ea000","enrollment_id":"enr_oiin624rqaojse22oe000","currency":"USD"},{"type":"depository","subtype":"checking","status":"open","name":"My Checking","links":{"transactions":"https://api.teller.io/accounts/acc_oiin624iajrg2mp2ea000/transactions","self":"https://api.teller.io/accounts/acc_oiin624iajrg2mp2ea000","details":"https://api.teller.io/accounts/acc_oiin624iajrg2mp2ea000/details","balances":"https://api.teller.io/accounts/acc_oiin624iajrg2mp2ea000/balances"},"last_four":"7346","institution":{"name":"Security Credit Union","id":"security_cu"},"id":"acc_oiin624iajrg2mp2ea000","enrollment_id":"enr_oiin624rqaojse22oe000","currency":"USD"},{"type":"depository","subtype":"savings","status":"open","name":"Essential Savings","links":{"transactions":"https://api.teller.io/accounts/acc_oiin624jqjrg2mp2ea000/transactions","self":"https://api.teller.io/accounts/acc_oiin624jqjrg2mp2ea000","details":"https://api.teller.io/accounts/acc_oiin624jqjrg2mp2ea000/details","balances":"https://api.teller.io/accounts/acc_oiin624jqjrg2mp2ea000/balances"},"last_four":"3528","institution":{"name":"Security Credit Union","id":"security_cu"},"id":"acc_oiin624jqjrg2mp2ea000","enrollment_id":"enr_oiin624rqaojse22oe000","currency":"USD"}]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
HTTP/1.1 200 OK
2+
cache-control: max-age=0, private, must-revalidate
3+
content-length: 491
4+
content-type: application/json; charset=utf-8
5+
date: Sun, 23 Jul 2023 16:24:10 GMT
6+
server: Teller API
7+
teller-enrollment-status: healthy
8+
x-request-id: F3SLx_tA8obguDgAPTqy
9+
10+
{"type":"credit","subtype":"credit_card","status":"open","name":"Platinum Card","links":{"transactions":"https://api.teller.io/accounts/acc_oiin624kqjrg2mp2ea000/transactions","self":"https://api.teller.io/accounts/acc_oiin624kqjrg2mp2ea000","balances":"https://api.teller.io/accounts/acc_oiin624kqjrg2mp2ea000/balances"},"last_four":"7857","institution":{"name":"Teller Bank","id":"teller_bank"},"id":"acc_oiin624kqjrg2mp2ea000","enrollment_id":"enr_oiin624rqaojse22oe000","currency":"USD"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
HTTP/1.1 200 OK
2+
cache-control: max-age=0, private, must-revalidate
3+
content-length: 236
4+
content-type: application/json; charset=utf-8
5+
date: Sun, 23 Jul 2023 16:42:06 GMT
6+
server: Teller API
7+
teller-enrollment-status: healthy
8+
x-request-id: F3SMwnf0l4m88CAAdnrx
9+
10+
{"links":{"self":"https://api.teller.io/accounts/acc_oiin624kqjrg2mp2ea000/balances","account":"https://api.teller.io/accounts/acc_oiin624kqjrg2mp2ea000"},"ledger":"4698.93","available":"301.07","account_id":"acc_oiin624kqjrg2mp2ea000"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
HTTP/1.1 404 Not Found
2+
cache-control: max-age=0, private, must-revalidate
3+
content-length: 96
4+
content-type: application/json; charset=utf-8
5+
date: Sun, 23 Jul 2023 16:42:30 GMT
6+
server: Teller API
7+
teller-enrollment-status: healthy
8+
x-request-id: F3SMyAceLKiHfL0APYgC
9+
10+
{"error":{"message":"The requested account details resource was not found.","code":"not_found"}}

test/fixtures/json/accounts/acc_oiin624kqjrg2mp2ea000/transactions.json

+10
Large diffs are not rendered by default.

test/fixtures/json/raw_account.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"type":"credit","subtype":"credit_card","status":"open","name":"Platinum Card","links":{"transactions":"https://api.teller.io/accounts/acc_oiin624kqjrg2mp2ea000/transactions","self":"https://api.teller.io/accounts/acc_oiin624kqjrg2mp2ea000","balances":"https://api.teller.io/accounts/acc_oiin624kqjrg2mp2ea000/balances"},"last_four":"7857","institution":{"name":"Security Credit Union","id":"security_cu"},"id":"acc_oiin624kqjrg2mp2ea000","enrollment_id":"enr_oiin624rqaojse22oe000","currency":"USD"}

test/fixtures/json/root.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
HTTP/1.1 200 OK
2+
cache-control: max-age=0, private, must-revalidate
3+
content-length: 55
4+
content-type: application/json; charset=utf-8
5+
date: Sun, 23 Jul 2023 16:16:13 GMT
6+
server: Teller API
7+
x-request-id: F3SLWNDU1QIOJFIAUQNC
8+
9+
{"links":{"accounts":"https://api.teller.io/accounts"}}

test/test_helper.rb

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
require "teller"
33

44
require "minitest/autorun"
5+
require 'webmock/minitest'

0 commit comments

Comments
 (0)