ofx_for_ruby 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/.externalToolBuilders/Rebuild parsers.launch +10 -0
  2. data/.gitignore +9 -0
  3. data/.loadpath +5 -0
  4. data/.project +27 -0
  5. data/COPYING +674 -0
  6. data/Gemfile +2 -0
  7. data/README +47 -0
  8. data/RELEASE_NOTES +45 -0
  9. data/Rakefile +63 -0
  10. data/lib/ofx.rb +60 -0
  11. data/lib/ofx/1.0.2/banking_message_set.rb +261 -0
  12. data/lib/ofx/1.0.2/credit_card_statement_message_set.rb +269 -0
  13. data/lib/ofx/1.0.2/document.rb +34 -0
  14. data/lib/ofx/1.0.2/email_message_set.rb +34 -0
  15. data/lib/ofx/1.0.2/financial_institution_profile_message_set.rb +155 -0
  16. data/lib/ofx/1.0.2/header.rb +48 -0
  17. data/lib/ofx/1.0.2/interbank_funds_transfer_message_set.rb +30 -0
  18. data/lib/ofx/1.0.2/investment_security_list_message_set.rb +30 -0
  19. data/lib/ofx/1.0.2/investment_statement_message_set.rb +30 -0
  20. data/lib/ofx/1.0.2/lexer.rb +135 -0
  21. data/lib/ofx/1.0.2/message_set.rb +165 -0
  22. data/lib/ofx/1.0.2/ofx_102.racc +90 -0
  23. data/lib/ofx/1.0.2/ofx_102.rex +65 -0
  24. data/lib/ofx/1.0.2/parser.rb +215 -0
  25. data/lib/ofx/1.0.2/payment_message_set.rb +30 -0
  26. data/lib/ofx/1.0.2/serializer.rb +114 -0
  27. data/lib/ofx/1.0.2/signon_message_set.rb +121 -0
  28. data/lib/ofx/1.0.2/signup_message_set.rb +186 -0
  29. data/lib/ofx/1.0.2/statements.rb +54 -0
  30. data/lib/ofx/1.0.2/status.rb +30 -0
  31. data/lib/ofx/1.0.2/wire_funds_transfer_message_set.rb +30 -0
  32. data/lib/ofx/banking_message_set.rb +125 -0
  33. data/lib/ofx/credit_card_statement_message_set.rb +104 -0
  34. data/lib/ofx/document.rb +30 -0
  35. data/lib/ofx/email_message_set.rb +42 -0
  36. data/lib/ofx/file_unique_identifier.rb +28 -0
  37. data/lib/ofx/financial_client.rb +76 -0
  38. data/lib/ofx/financial_institution.rb +83 -0
  39. data/lib/ofx/financial_institution_profile_message_set.rb +67 -0
  40. data/lib/ofx/header.rb +115 -0
  41. data/lib/ofx/http/cacert.pem +3509 -0
  42. data/lib/ofx/http/ofx_http_client.rb +84 -0
  43. data/lib/ofx/interbank_funds_transfer_message_set.rb +33 -0
  44. data/lib/ofx/investment_security_list_message_set.rb +33 -0
  45. data/lib/ofx/investment_statement_message_set.rb +33 -0
  46. data/lib/ofx/message_set.rb +78 -0
  47. data/lib/ofx/payment_message_set.rb +33 -0
  48. data/lib/ofx/serializer.rb +39 -0
  49. data/lib/ofx/signon_message_set.rb +141 -0
  50. data/lib/ofx/signup_message_set.rb +84 -0
  51. data/lib/ofx/statements.rb +49 -0
  52. data/lib/ofx/status.rb +100 -0
  53. data/lib/ofx/transaction_unique_identifier.rb +28 -0
  54. data/lib/ofx/version.rb +82 -0
  55. data/lib/ofx/wire_funds_transfer_message_set.rb +33 -0
  56. data/ofx_for_ruby.gemspec +25 -0
  57. data/planning/OFX Specification completion.ods +0 -0
  58. data/test/capital_one/capital_one_helper.rb +77 -0
  59. data/test/capital_one/fixtures/README +10 -0
  60. data/test/capital_one/fixtures/fipid-5599.xml +98 -0
  61. data/test/capital_one/test_banking_statement.rb +108 -0
  62. data/test/capital_one/test_financial_institution_profile.rb +295 -0
  63. data/test/capital_one/test_ofx_http_client.rb +72 -0
  64. data/test/capital_one/test_signup_account_information.rb +100 -0
  65. data/test/citi/citi_helper.rb +77 -0
  66. data/test/citi/fixtures/README +9 -0
  67. data/test/citi/fixtures/fipid-6642.xml +98 -0
  68. data/test/citi/test_credit_card_statement.rb +159 -0
  69. data/test/citi/test_financial_institution_profile.rb +231 -0
  70. data/test/citi/test_ofx_http_client.rb +71 -0
  71. data/test/citi/test_signup_account_information.rb +85 -0
  72. data/test/test_helper.rb +4 -0
  73. data/test/test_ofx_version.rb +141 -0
  74. metadata +210 -0
@@ -0,0 +1,48 @@
1
+ # Copyright © 2007 Chris Guidry <chrisguidry@gmail.com>
2
+ #
3
+ # This file is part of OFX for Ruby.
4
+ #
5
+ # OFX for Ruby is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation; either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # OFX for Ruby is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ module OFX
19
+ class Header
20
+ def to_ofx_102_s
21
+ headers = ""
22
+
23
+ headers += "OFXHEADER:" + self.header_version.to_compact_s + "\n"
24
+ headers += "DATA:" + self.content_type.to_s + "\n"
25
+ headers += "VERSION:" + self.document_version.to_compact_s + "\n"
26
+ headers += "SECURITY:" + self.security.to_s + "\n"
27
+ headers += "ENCODING:" + self.content_encoding.to_s + "\n"
28
+ headers += "CHARSET:" + self.content_character_set.to_s + "\n"
29
+ headers += "COMPRESSION:" + self.compression.to_s + "\n"
30
+ headers += "OLDFILEUID:" + self.previous_unique_identifier.to_s + "\n"
31
+ headers += "NEWFILEUID:" + self.unique_identifier.to_s + "\n"
32
+
33
+ headers
34
+ end
35
+
36
+ def self.from_ofx_102_s(header_string)
37
+ header = OFX::Header.new
38
+
39
+ header_pattern = /^(\w+)\:(.*)$/
40
+ header_string.split("\n").each do |this_header|
41
+ header_match = header_pattern.match(this_header)
42
+ header[header_match[1]] = header_match[2]
43
+ end
44
+
45
+ header
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,30 @@
1
+ # Copyright © 2007 Chris Guidry <chrisguidry@gmail.com>
2
+ #
3
+ # This file is part of OFX for Ruby.
4
+ #
5
+ # OFX for Ruby is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation; either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # OFX for Ruby is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ module OFX
19
+ class InterbankFundsTransferMessageSet < MessageSet
20
+ end
21
+
22
+ class InterbankFundsTransferMessageSetProfile < MessageSetProfile
23
+ def self.from_ofx_102_hash(message_set_description_hash)
24
+ profile = OFX::InterbankFundsTransferMessageSetProfile.new
25
+ profile.message_set_class = OFX::InterbankFundsTransferMessageSet
26
+ profile.from_ofx_102_hash(message_set_description_hash)
27
+ profile
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # Copyright © 2007 Chris Guidry <chrisguidry@gmail.com>
2
+ #
3
+ # This file is part of OFX for Ruby.
4
+ #
5
+ # OFX for Ruby is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation; either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # OFX for Ruby is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ module OFX
19
+ class InvestmentSecurityListMessageSet < MessageSet
20
+ end
21
+
22
+ class InvestmentSecurityListMessageSetProfile < MessageSetProfile
23
+ def self.from_ofx_102_hash(message_set_description_hash)
24
+ profile = OFX::InvestmentSecurityListMessageSetProfile.new
25
+ profile.message_set_class = OFX::InvestmentSecurityListMessageSet
26
+ profile.from_ofx_102_hash(message_set_description_hash)
27
+ profile
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # Copyright © 2007 Chris Guidry <chrisguidry@gmail.com>
2
+ #
3
+ # This file is part of OFX for Ruby.
4
+ #
5
+ # OFX for Ruby is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation; either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # OFX for Ruby is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ module OFX
19
+ class InvestmentStatementMessageSet < MessageSet
20
+ end
21
+
22
+ class InvestmentStatementMessageSetProfile < MessageSetProfile
23
+ def self.from_ofx_102_hash(message_set_description_hash)
24
+ profile = OFX::InvestmentStatementMessageSetProfile.new
25
+ profile.message_set_class = OFX::InvestmentStatementMessageSet
26
+ profile.from_ofx_102_hash(message_set_description_hash)
27
+ profile
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,135 @@
1
+ #--
2
+ # DO NOT MODIFY!!!!
3
+ # This file is automatically generated by rex 1.0.2
4
+ # from lexical definition file "ofx_102.rex".
5
+ #++
6
+
7
+ require 'racc/parser'
8
+ # Copyright © 2007 Chris Guidry <chrisguidry@gmail.com>
9
+ #
10
+ # This file is part of OFX for Ruby.
11
+ #
12
+ # OFX for Ruby is free software; you can redistribute it and/or modify
13
+ # it under the terms of the GNU General Public License as published by
14
+ # the Free Software Foundation; either version 3 of the License, or
15
+ # (at your option) any later version.
16
+ #
17
+ # OFX for Ruby is distributed in the hope that it will be useful,
18
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ # GNU General Public License for more details.
21
+ #
22
+ # You should have received a copy of the GNU General Public License
23
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+
25
+ module OFX
26
+ module OFX102
27
+ class Parser < Racc::Parser
28
+ require 'strscan'
29
+
30
+ class ScanError < StandardError ; end
31
+
32
+ attr_reader :lineno
33
+ attr_reader :filename
34
+
35
+ def scan_setup ; end
36
+
37
+ def action &block
38
+ yield
39
+ end
40
+
41
+ def scan_str( str )
42
+ scan_evaluate str
43
+ do_parse
44
+ end
45
+
46
+ def load_file( filename )
47
+ @filename = filename
48
+ open(filename, "r") do |f|
49
+ scan_evaluate f.read
50
+ end
51
+ end
52
+
53
+ def scan_file( filename )
54
+ load_file filename
55
+ do_parse
56
+ end
57
+
58
+ def next_token
59
+ @rex_tokens.shift
60
+ end
61
+
62
+ def scan_evaluate( str )
63
+ scan_setup
64
+ @rex_tokens = []
65
+ @lineno = 1
66
+ ss = StringScanner.new(str)
67
+ state = nil
68
+ until ss.eos?
69
+ text = ss.peek(1)
70
+ @lineno += 1 if text == "\n"
71
+ case state
72
+ when nil
73
+ case
74
+ when (text = ss.scan(/\<\//))
75
+ @rex_tokens.push action { state = :TAG; [:etag_in, text] }
76
+
77
+ when (text = ss.scan(/\</))
78
+ @rex_tokens.push action { state = :TAG; [:tag_in, text] }
79
+
80
+ when (text = ss.scan(/\s+(?=\S)/))
81
+ ;
82
+
83
+ when (text = ss.scan(/.*?(?=(\<|\<\/)+?)/))
84
+ @rex_tokens.push action { [:text, text] }
85
+
86
+ when (text = ss.scan(/.*\S(?=\s*$)/))
87
+ @rex_tokens.push action { [:text, text] }
88
+
89
+ when (text = ss.scan(/\s+(?=$)/))
90
+ ;
91
+
92
+ else
93
+ text = ss.string[ss.pos .. -1]
94
+ raise ScanError, "can not match: '" + text + "'"
95
+ end # if
96
+
97
+ when :TAG
98
+ case
99
+ when (text = ss.scan(/\>/))
100
+ @rex_tokens.push action { state = nil; [:tag_out, text] }
101
+
102
+ when (text = ss.scan(/[\w\-\.]+/))
103
+ @rex_tokens.push action { [:element, text] }
104
+
105
+ else
106
+ text = ss.string[ss.pos .. -1]
107
+ raise ScanError, "can not match: '" + text + "'"
108
+ end # if
109
+
110
+ else
111
+ raise ScanError, "undefined state: '" + state.to_s + "'"
112
+ end # case state
113
+ end # until ss
114
+ end # def scan_evaluate
115
+
116
+ end # class
117
+
118
+ end # module OFX102
119
+ end # module OFX
120
+
121
+ =begin
122
+ if __FILE__ == $0
123
+ exit if ARGV.size != 1
124
+ filename = ARGV.shift
125
+ rex = OFX::OFX102::Parser.new
126
+ begin
127
+ rex.load_file filename
128
+ while token = rex.next_token
129
+ p token
130
+ end
131
+ rescue
132
+ $stderr.printf "%s:%d:%s\n", rex.filename, rex.lineno, $!.message
133
+ end
134
+ end
135
+ =end
@@ -0,0 +1,165 @@
1
+ # Copyright © 2007 Chris Guidry <chrisguidry@gmail.com>
2
+ #
3
+ # This file is part of OFX for Ruby.
4
+ #
5
+ # OFX for Ruby is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation; either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # OFX for Ruby is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ module OFX
19
+ class MessageSet
20
+ def to_ofx_102_s
21
+ request_message = requests.length > 0
22
+ request_message = responses.length == 0
23
+
24
+ message = ''
25
+
26
+ if request_message
27
+ message += " <#{ofx_102_message_set_name}MSGSRQV#{version}>\n"
28
+ requests.each do |request|
29
+ message += request.to_ofx_102_s + "\n"
30
+ end
31
+ message += " </#{ofx_102_message_set_name}MSGSRQV#{version}>\n"
32
+ else
33
+ message += " <#{ofx_102_message_set_name}MSGSRSV#{version}>\n"
34
+ responses.each do |response|
35
+ message += response.to_ofx_102_s + "\n"
36
+ end
37
+ message += " </#{ofx_102_message_set_name}MSGSRSV#{version}>\n"
38
+ end
39
+
40
+ message
41
+ end
42
+
43
+ def ofx_102_message_set_name
44
+ raise NotImplementedError
45
+ end
46
+
47
+ def self.from_ofx_102_message_set_hash(message_set_name, message_set_hash)
48
+ match = /^(\w+)MSGSR([QS])V(\d+)$/.match(message_set_name)
49
+ raise NotImplementedError unless match
50
+
51
+ message_set_class = MESSAGE_SET_NAMES_TO_CLASSES[$1]
52
+ raise NotImplementedError unless message_set_class
53
+
54
+ return message_set_class.from_ofx_102_hash(message_set_hash)
55
+ end
56
+
57
+ def request_or_response_from_ofx_102_tag_name(response_or_request_name)
58
+ raise NotImplementedError
59
+ end
60
+
61
+ private
62
+ MESSAGE_SET_NAMES_TO_CLASSES =
63
+ {
64
+ 'SIGNON' => SignonMessageSet,
65
+ 'SIGNUP' => SignupMessageSet,
66
+ 'PROF' => FinancialInstitutionProfileMessageSet,
67
+ 'BANK' => BankingMessageSet,
68
+ 'CREDITCARD' => CreditCardStatementMessageSet
69
+ }
70
+ end
71
+
72
+ class MessageSetProfile
73
+ def from_ofx_102_hash(message_set_description_hash)
74
+ message_set_core_hash = message_set_description_hash['MSGSETCORE']
75
+ @version = OFX::Version.new(message_set_core_hash['VER'])
76
+ @service_provider_name = message_set_core_hash['SPNAME']
77
+ @message_url = URI.parse(message_set_core_hash['URL']) if message_set_core_hash['URL']
78
+ @required_ofx_security = message_set_core_hash['OFXSEC']
79
+ @requires_transport_security = message_set_core_hash['TRANSPSEC'] == 'Y' ? true : false
80
+ @signon_realm = message_set_core_hash['SIGNONREALM']
81
+ @language = message_set_core_hash['LANGUAGE']
82
+ @synchronization_mode = message_set_core_hash['SYNCMODE']
83
+ @supports_response_file_error_recovery = message_set_core_hash['RESPFILEER'] == 'Y' ? true : false
84
+ end
85
+ end
86
+
87
+ class Request
88
+ def to_ofx_102_s
89
+ request = ''
90
+ request += " <#{ofx_102_name}RQ>\n"
91
+
92
+ request += ofx_102_request_body + "\n"
93
+
94
+ request += " </#{ofx_102_name}RQ>"
95
+ request
96
+ end
97
+
98
+ def ofx_102_request_body
99
+ raise NotImplementedError
100
+ end
101
+ end
102
+
103
+ class TransactionalRequest < Request
104
+ def to_ofx_102_s
105
+ request = ''
106
+ request += " <#{ofx_102_name}TRNRQ>\n"
107
+
108
+ request += " <TRNUID>#{transaction_identifier}\n"
109
+ request += " <CLTCOOKIE>#{client_cookie}\n" if client_cookie
110
+ request += " <TAN>#{transaction_authorization_number}\n" if transaction_authorization_number
111
+
112
+ request += " <#{ofx_102_name}RQ>\n"
113
+
114
+ request += ofx_102_request_body + "\n"
115
+
116
+ request += " </#{ofx_102_name}RQ>\n"
117
+
118
+ request += " </#{ofx_102_name}TRNRQ>"
119
+ request
120
+ end
121
+
122
+ def ofx_102_request_body
123
+ raise NotImplementedError
124
+ end
125
+ end
126
+
127
+ class Response
128
+ def to_ofx_102_s
129
+ response = ''
130
+ response += " <#{ofx_102_name}RS>\n"
131
+
132
+ response += ofx_102_response_body + "\n"
133
+
134
+ response += " </#{ofx_102_name}RS>"
135
+ response
136
+ end
137
+
138
+ def ofx_102_response_body
139
+ raise NotImplementedError
140
+ end
141
+ end
142
+
143
+ class TransactionalResponse < Response
144
+ def to_ofx_102_s
145
+ response = ''
146
+ response += " <#{ofx_102_name}TRNRS>\n"
147
+
148
+ response += " <TRNUID>#{transaction_identifier}\n"
149
+ response += " <CLTCOOKIE>#{client_cookie}\n" if client_cookie
150
+
151
+ response += " <#{ofx_102_name}RS>\n"
152
+
153
+ response += ofx_102_response_body + "\n"
154
+
155
+ response += " </#{ofx_102_name}RS>\n"
156
+
157
+ response += " </#{ofx_102_name}TRNRS>"
158
+ response
159
+ end
160
+
161
+ def ofx_102_response_body
162
+ raise NotImplementedError
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,90 @@
1
+ # Copyright © 2007 Chris Guidry <chrisguidry@gmail.com>
2
+ #
3
+ # This file is part of OFX for Ruby.
4
+ #
5
+ # OFX for Ruby is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation; either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # OFX for Ruby is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ class OFX::OFX102::Parser
19
+
20
+ rule
21
+ target :
22
+ | ofx_body
23
+
24
+ ofx_body : tag_from contents tag_to
25
+
26
+ tag_from : tag_in element tag_out { start_tag(val[1]) }
27
+ tag_to : etag_in element tag_out { end_tag(val[1]) }
28
+
29
+ contents : /* none */
30
+ | contents content
31
+
32
+ content : text
33
+ | aggregate
34
+ | value
35
+
36
+ aggregate : tag_from contents tag_to
37
+ value : tag_from text { data_value(val[1]) }
38
+
39
+ end
40
+
41
+ ---- header ----
42
+ #
43
+ # generated by racc
44
+ #
45
+ require File.dirname(__FILE__) + '/lexer'
46
+
47
+ ---- inner ----
48
+
49
+ attr :documents
50
+ attr :ofx_hashes
51
+
52
+ def initialize
53
+ @documents = []
54
+ @ofx_hashes = []
55
+
56
+ @tags_stack = []
57
+ end
58
+
59
+ private
60
+ def start_tag(tag_name)
61
+ # push an empty aggregate/value tag
62
+ @tags_stack.push({ tag_name => {} })
63
+ end
64
+
65
+ def data_value(value)
66
+ # pop the most recent tag, and use it's name as the key in the next higher tag's hash
67
+ tag = @tags_stack.pop
68
+ @tags_stack.last[@tags_stack.last.keys[0]][tag.keys[0]] = CGI::unescapeHTML(value)
69
+ end
70
+
71
+ def end_tag(tag_name)
72
+ # pop the most recent tag, stop and recognize this as a full document if this is the last tag; otherwise,
73
+ # use this tag's naem as the key in the next higher tag's hash
74
+ tag = @tags_stack.pop
75
+ if (@tags_stack.empty?)
76
+ @ofx_hashes << tag
77
+ @documents << OFX::Document.from_ofx_102_hash(tag)
78
+ else
79
+ previous = @tags_stack.last[@tags_stack.last.keys[0]][tag.keys[0]]
80
+ if previous == nil
81
+ @tags_stack.last[@tags_stack.last.keys[0]][tag.keys[0]] = tag[tag.keys[0]]
82
+ elsif previous.kind_of?(Array)
83
+ previous << tag[tag.keys[0]]
84
+ else
85
+ @tags_stack.last[@tags_stack.last.keys[0]][tag.keys[0]] = [previous, tag[tag.keys[0]]]
86
+ end
87
+ end
88
+ end
89
+
90
+ ---- footer ----