libofx-ffi 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ofx.rb ADDED
@@ -0,0 +1,36 @@
1
+ require 'ffi'
2
+
3
+ require 'ofx/ffi'
4
+
5
+ require 'ofx/statement'
6
+ require 'ofx/account'
7
+ require 'ofx/transaction'
8
+
9
+ module OFX
10
+ AccountCallback = Proc.new do |acct_data, junk|
11
+ account = Account.new(acct_data)
12
+ @statement[account.number] = account
13
+ 0
14
+ end
15
+
16
+ TransactionCallback = Proc.new do |t, junk|
17
+ acct_data = FFI::AccountData.new t[:account_ptr]
18
+ acct_num = acct_data.account_number
19
+ @statement[acct_num].transactions << Transaction.new(t)
20
+ 0
21
+ end
22
+
23
+ def self.parse(file_name)
24
+ raise Errno::ENOENT unless File.exist? file_name
25
+
26
+ @statement = OFX::Statement.new
27
+
28
+ context = FFI.libofx_get_new_context
29
+ FFI.ofx_set_account_cb context, AccountCallback
30
+ FFI.ofx_set_transaction_cb context, TransactionCallback
31
+ FFI.ofx_proc_file context, file_name, 0
32
+ FFI.libofx_free_context context
33
+
34
+ @statement
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ module OFX
2
+ class Account
3
+ attr_reader :number, :type
4
+ attr_accessor :transactions
5
+
6
+ def initialize(ffi_account)
7
+ @number = ffi_account.account_number.to_s
8
+ @type = ffi_account.account_type
9
+
10
+ @transactions = []
11
+ end
12
+ end
13
+ end
data/lib/ofx/ffi.rb ADDED
@@ -0,0 +1,100 @@
1
+ require 'ofx/ffi/valid_access'
2
+
3
+ module OFX
4
+ module FFI
5
+ extend ::FFI::Library
6
+ ffi_lib 'ofx'
7
+
8
+ # LibofxContextPtr is void*
9
+ attach_function :libofx_get_new_context, [], :pointer
10
+ attach_function :libofx_free_context, [:pointer], :int
11
+
12
+ attach_function :ofx_proc_file, [:pointer, :pointer, :int], :int
13
+
14
+ AccountType = enum :checking, :savings, :money_market, :credit_line,
15
+ :cma, :credit_card, :investment
16
+ class AccountData < ::FFI::Struct
17
+ include ValidAccess
18
+
19
+ layout :account_id, [:char, 57],
20
+ :account_name, [:char, 255],
21
+ :account_id_valid, :int,
22
+ :account_type, AccountType,
23
+ :account_type_valid, :int,
24
+ :currency, [:char, 4],
25
+ :currency_valid, :int,
26
+ :account_number, [:char, 23],
27
+ :account_number_valid, :int,
28
+ :bank_id, [:char, 10],
29
+ :bank_id_valid, :int,
30
+ :broker_id, [:char, 23],
31
+ :broker_id_valid, :int,
32
+ :branch_id, [:char, 23],
33
+ :branch_id_valid, :int
34
+ end
35
+ callback :account_callback, [AccountData.by_value, :pointer], :int
36
+ attach_function :ofx_set_account_cb, [:pointer, :account_callback], :void
37
+
38
+
39
+ TransactionType = enum :credit, :debit, :interest, :dividend, :fee,
40
+ :service_change, :deposit, :atm, :point_of_sale,
41
+ :transfer, :check, :payment, :cash_withdrawal,
42
+ :direct_deposit, :direct_debit, :repeat_payment,
43
+ :other
44
+ class TransactionData < ::FFI::Struct
45
+ include ValidAccess
46
+
47
+ layout :account_id, [:char, 57],
48
+ :account_ptr, :pointer,
49
+ :account_id_valid, :int,
50
+ :transaction_type, TransactionType,
51
+ :transaction_type_valid, :int,
52
+ :investment_transaction_type, :int, # this is really an enum but i don't care about it right now
53
+ :investment_transaction_type_valid, :int,
54
+ :units, :double,
55
+ :units_valid, :int,
56
+ :unit_price, :double,
57
+ :units_price_valid, :int,
58
+ :amount, :double,
59
+ :amount_valid, :int,
60
+ :fi_id, [:char, 256],
61
+ :fi_id_valid, :int,
62
+ :unique_id, [:char, 33],
63
+ :unique_id_valid, :int,
64
+ :unique_id_type, [:char, 11],
65
+ :unique_id_type_valid, :int,
66
+ :security_data, :pointer,
67
+ :security_data_valid, :int,
68
+ :date_posted, :int, # really a time_t
69
+ :date_posted_valid, :int,
70
+ :date_initiated, :int, #time_t
71
+ :date_initiated_valid, :int,
72
+ :date_funds_available, :int, # time_t
73
+ :date_funds_available_valid, :int,
74
+ :fi_id_corrected, [:char, 256],
75
+ :fi_id_corrected_valid, :int,
76
+ :fi_id_corrected_action, :int, #really an enum
77
+ :fi_id_corrected_action_valid, :int,
78
+ :server_transaction_id, [:char, 37],
79
+ :server_transaction_id_valid, :int,
80
+ :check_number, [:char, 13],
81
+ :check_number_valid, :int,
82
+ :reference_number, [:char, 33],
83
+ :reference_number_valid, :int,
84
+ :standard_industrial_code, :long,
85
+ :standard_industrial_code_valid, :int,
86
+ :payee_id, [:char, 37],
87
+ :payee_id_valid, :int,
88
+ :name, [:char, 33],
89
+ :name_valid, :int,
90
+ :memo, [:char, 391],
91
+ :memo_valid, :int,
92
+ :commission, :double,
93
+ :commission_valid, :int,
94
+ :fees, :double,
95
+ :fees_valid, :int
96
+ end
97
+ callback :transaction_callback, [TransactionData.by_value, :pointer], :int
98
+ attach_function :ofx_set_transaction_cb, [:pointer, :transaction_callback], :void
99
+ end
100
+ end
@@ -0,0 +1,13 @@
1
+ module OFX
2
+ module FFI
3
+ module ValidAccess
4
+ def self.included(c)
5
+ class_eval do
6
+ def method_missing(attribute, *args)
7
+ self[:"#{attribute}_valid"] == 1 ? self[attribute] : nil
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ module OFX
2
+ class Statement
3
+ def initialize
4
+ @accounts = {}
5
+ end
6
+
7
+ def [](key)
8
+ # have to call to_s because the original key from ffi is a CharArray
9
+ # (equality doesn't do what i want)
10
+ @accounts[key.to_s]
11
+ end
12
+
13
+ def []=(key, val)
14
+ @accounts[key.to_s] = val
15
+ end
16
+
17
+ def each
18
+ @accounts.each do |number, account|
19
+ yield account
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ require 'time'
2
+
3
+ module OFX
4
+ class Transaction
5
+ attr_reader :number, :date, :type, :name, :memo, :amount
6
+
7
+ def initialize(ffi_transaction)
8
+ @number = ffi_transaction.fi_id.to_s
9
+ @date = Time.at ffi_transaction.date_posted
10
+ @memo = ffi_transaction.memo.to_s
11
+ @name = ffi_transaction.name.to_s
12
+ @type = ffi_transaction.transaction_type
13
+ @amount = ffi_transaction.amount
14
+ end
15
+ end
16
+ end
data/test/ofx_test.rb ADDED
@@ -0,0 +1,41 @@
1
+ require 'ofx'
2
+ require 'test/unit'
3
+
4
+ class OfxTest < Test::Unit::TestCase
5
+ def setup
6
+ @statement = OFX.parse 'test/test.ofx'
7
+ end
8
+
9
+ def test_ofx_file_must_exist
10
+ assert_raise Errno::ENOENT do
11
+ OFX.parse 'test/blah.ofx'
12
+ end
13
+ end
14
+
15
+ def test_account_must_exist
16
+ refute_nil @statement["987654321"]
17
+ end
18
+
19
+ def test_transactions_are_correct
20
+ transactions = @statement["987654321"].transactions
21
+
22
+ expected = [
23
+ { :type => :debit, :amount => -100, :number => "0000009",
24
+ :name => /AUTOMATIC WITHDRAWAL/, :memo => /AUTOMATIC WITHDRAWAL/,
25
+ :date => Time.new(2011, 1, 3, 11, 0, 0, "-07:00") },
26
+ { :type => :check, :amount => -146.64, :number => "0000008",
27
+ :name => "CHECK # 515",
28
+ :date => Time.new(2011, 1, 10, 11, 0, 0, "-07:00") },
29
+ { :type => :credit, :amount => 1600.01, :number => "0000007",
30
+ :name => /AUTOMATIC DEPOSIT/, :memo => /AUTOMATIC DEPOSIT/,
31
+ :date => Time.new(2011, 1, 13, 11, 0, 0, "-07:00") }
32
+ ]
33
+
34
+ transactions.each do |transaction|
35
+ expected.shift.each do |attribute, expected_value|
36
+ got = transaction.send(attribute)
37
+ assert expected_value === got, "expected #{expected_value}, but got #{got}"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,27 @@
1
+ require 'ofx/statement'
2
+ require 'test/unit'
3
+
4
+ class StatementTest < Test::Unit::TestCase
5
+ def setup
6
+ @s = OFX::Statement.new
7
+ @s[:foo] = 1
8
+ @s[:bar] = 2
9
+ end
10
+
11
+ def test_account_setting_and_getting
12
+ assert_equal 1, @s[:foo]
13
+ assert_equal 2, @s[:bar]
14
+ assert_nil @s[:baz]
15
+ end
16
+
17
+ def test_each
18
+ expected = ["foo", 1, "bar", 2]
19
+
20
+ @s.each do |k,v|
21
+ assert_equal expected.shift, k
22
+ assert_equal expected.shift, v
23
+ end
24
+
25
+ assert_equal [], expected
26
+ end
27
+ end
data/test/test.ofx ADDED
@@ -0,0 +1,81 @@
1
+ OFXHEADER:100
2
+ DATA:OFXSGML
3
+ VERSION:102
4
+ SECURITY:NONE
5
+ ENCODING:USASCII
6
+ CHARSET:1252
7
+ COMPRESSION:NONE
8
+ OLDFILEUID:NONE
9
+ NEWFILEUID:NONE
10
+
11
+ <OFX>
12
+ <SIGNONMSGSRSV1>
13
+ <SONRS>
14
+ <STATUS>
15
+ <CODE>0
16
+ <SEVERITY>INFO
17
+ </STATUS>
18
+ <DTSERVER>20110301225905.944
19
+ <LANGUAGE>ENG
20
+ <DTPROFUP>20050531060000.000
21
+ <FI>
22
+ <ORG>BLAH
23
+ <FID>1000
24
+ </FI>
25
+ <INTU.BID>99999
26
+ <INTU.USERID>12345678
27
+ </SONRS>
28
+ </SIGNONMSGSRSV1>
29
+ <BANKMSGSRSV1>
30
+ <STMTTRNRS>
31
+ <TRNUID>0
32
+ <STATUS>
33
+ <CODE>0
34
+ <SEVERITY>INFO
35
+ </STATUS>
36
+ <STMTRS>
37
+ <CURDEF>USD
38
+ <BANKACCTFROM>
39
+ <BANKID>345678910
40
+ <ACCTID>987654321
41
+ <ACCTTYPE>CHECKING
42
+ </BANKACCTFROM>
43
+ <BANKTRANLIST>
44
+ <DTSTART>20110101070000.000
45
+ <DTEND>20110301070000.000
46
+ <STMTTRN>
47
+ <TRNTYPE>DEBIT
48
+ <DTPOSTED>20110103120000.000
49
+ <TRNAMT>-100
50
+ <FITID>0000009
51
+ <NAME>AUTOMATIC WITHDRAWAL, BLAH BLAH
52
+ <MEMO>AUTOMATIC WITHDRAWAL, BLAH BLAH BLAH BLAH BLAH BLAH B
53
+ </STMTTRN>
54
+ <STMTTRN>
55
+ <TRNTYPE>CHECK
56
+ <DTPOSTED>20110110120000.000
57
+ <TRNAMT>-146.64
58
+ <FITID>0000008
59
+ <CHECKNUM>515
60
+ <NAME>CHECK # 515
61
+ </STMTTRN>
62
+ <STMTTRN>
63
+ <TRNTYPE>CREDIT
64
+ <DTPOSTED>20110113120000.000
65
+ <TRNAMT>1600.01
66
+ <FITID>0000007
67
+ <NAME>AUTOMATIC DEPOSIT, SOME EMPLOYER
68
+ <MEMO>AUTOMATIC DEPOSIT, SOME EMPLOYER
69
+ </STMTTRN>
70
+ </BANKTRANLIST>
71
+ <LEDGERBAL>
72
+ <BALAMT>9999.99
73
+ <DTASOF>20110301225905.944
74
+ </LEDGERBAL>
75
+ <AVAILBAL> <BALAMT>9999.99
76
+ <DTASOF>20110301225905.944
77
+ </AVAILBAL>
78
+ </STMTRS>
79
+ </STMTTRNRS>
80
+ </BANKMSGSRSV1>
81
+ </OFX>
@@ -0,0 +1,21 @@
1
+ require 'ofx/ffi/valid_access'
2
+ require 'test/unit'
3
+
4
+ class ValidAccessTest < Test::Unit::TestCase
5
+ def test_valid_access
6
+ h = {
7
+ :foo => 'blah',
8
+ :foo_valid => 1,
9
+ :bar => 'glah',
10
+ :bar_valid => 0,
11
+ :baz => 'zoing'
12
+ }
13
+ class << h
14
+ include OFX::FFI::ValidAccess
15
+ end
16
+
17
+ assert_equal 'blah', h.foo
18
+ assert_nil h.bar
19
+ assert_nil h.baz
20
+ end
21
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: libofx-ffi
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 3
9
+ version: 0.1.3
10
+ platform: ruby
11
+ authors:
12
+ - Cameron Matheson
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-03-26 00:00:00 -06:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: ffi
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 1
30
+ - 0
31
+ - 6
32
+ version: 1.0.6
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ description: Ruby/FFI wrapper for libofx
36
+ email: cameron.matheson@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - lib/ofx/ffi/valid_access.rb
45
+ - lib/ofx/statement.rb
46
+ - lib/ofx/transaction.rb
47
+ - lib/ofx/ffi.rb
48
+ - lib/ofx/account.rb
49
+ - lib/ofx.rb
50
+ - test/valid_access_test.rb
51
+ - test/statement_test.rb
52
+ - test/test.ofx
53
+ - test/ofx_test.rb
54
+ has_rdoc: true
55
+ homepage: http://github.com/cmatheson/libofx-ffi
56
+ licenses: []
57
+
58
+ post_install_message:
59
+ rdoc_options: []
60
+
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ segments:
69
+ - 0
70
+ version: "0"
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ segments:
77
+ - 0
78
+ version: "0"
79
+ requirements: []
80
+
81
+ rubyforge_project:
82
+ rubygems_version: 1.3.7
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: libofx-ffi parses financial documents supported by libofx
86
+ test_files: []
87
+