mirror of
https://github.com/aclindsa/ofxgo.git
synced 2024-11-22 03:30:04 -05:00
Generalize response parsing code
This removes the many decodeXXXMessageSet() functions and replaces them with a large map and a single generic decodeMessageSet() function. Also change Responses to satisfy the Message interface as pointer types (instead of the raw types), add the full set of top-level message sets (though most of them still lack any message-parsing ability), adjust the message set names to more closely mirror their OFX names, and fixup tests and the command-line client to match the above changes.
This commit is contained in:
parent
d822179446
commit
f185d78d29
33
banking.go
33
banking.go
@ -1,7 +1,6 @@
|
|||||||
package ofxgo
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"github.com/aclindsa/go/src/encoding/xml"
|
"github.com/aclindsa/go/src/encoding/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -150,41 +149,15 @@ type StatementResponse struct {
|
|||||||
MktgInfo String `xml:"STMTRS>MKTGINFO,omitempty"` // Marketing information
|
MktgInfo String `xml:"STMTRS>MKTGINFO,omitempty"` // Marketing information
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sr StatementResponse) Name() string {
|
func (sr *StatementResponse) Name() string {
|
||||||
return "STMTTRNRS"
|
return "STMTTRNRS"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sr StatementResponse) Valid() (bool, error) {
|
func (sr *StatementResponse) Valid() (bool, error) {
|
||||||
//TODO implement
|
//TODO implement
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sr StatementResponse) Type() messageType {
|
func (sr *StatementResponse) Type() messageType {
|
||||||
return BankRs
|
return BankRs
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeBankingMessageSet(d *xml.Decoder, start xml.StartElement) ([]Message, error) {
|
|
||||||
var msgs []Message
|
|
||||||
for {
|
|
||||||
tok, err := nextNonWhitespaceToken(d)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local {
|
|
||||||
// If we found the end of our starting element, we're done parsing
|
|
||||||
return msgs, nil
|
|
||||||
} else if startElement, ok := tok.(xml.StartElement); ok {
|
|
||||||
switch startElement.Name.Local {
|
|
||||||
case "STMTTRNRS":
|
|
||||||
var info StatementResponse
|
|
||||||
if err := d.DecodeElement(&info, &startElement); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
msgs = append(msgs, Message(info))
|
|
||||||
default:
|
|
||||||
return nil, errors.New("Unsupported banking response tag: " + startElement.Name.Local)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, errors.New("Didn't find an opening element")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -64,7 +64,7 @@ func TestMarshalBankStatementRequest(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Include: true,
|
Include: true,
|
||||||
}
|
}
|
||||||
request.Banking = append(request.Banking, &statementRequest)
|
request.Bank = append(request.Bank, &statementRequest)
|
||||||
|
|
||||||
request.SetClientFields(&client)
|
request.SetClientFields(&client)
|
||||||
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
||||||
@ -203,7 +203,7 @@ func TestUnmarshalBankStatementResponse(t *testing.T) {
|
|||||||
AvailBalAmt: (*ofxgo.Amount)(&availbalamt),
|
AvailBalAmt: (*ofxgo.Amount)(&availbalamt),
|
||||||
AvailDtAsOf: &availdtasof,
|
AvailDtAsOf: &availdtasof,
|
||||||
}
|
}
|
||||||
expected.Banking = append(expected.Banking, statementResponse)
|
expected.Bank = append(expected.Bank, &statementResponse)
|
||||||
|
|
||||||
response, err := ofxgo.ParseResponse(responseReader)
|
response, err := ofxgo.ParseResponse(responseReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -55,7 +55,7 @@ func download() {
|
|||||||
},
|
},
|
||||||
Include: true,
|
Include: true,
|
||||||
}
|
}
|
||||||
query.Banking = append(query.Banking, &statementRequest)
|
query.Bank = append(query.Bank, &statementRequest)
|
||||||
|
|
||||||
response, err := client.RequestNoParse(query)
|
response, err := client.RequestNoParse(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -40,7 +40,7 @@ func bankTransactions() {
|
|||||||
},
|
},
|
||||||
Include: true,
|
Include: true,
|
||||||
}
|
}
|
||||||
query.Banking = append(query.Banking, &statementRequest)
|
query.Bank = append(query.Bank, &statementRequest)
|
||||||
|
|
||||||
response, err := client.Request(query)
|
response, err := client.Request(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -54,12 +54,12 @@ func bankTransactions() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(response.Banking) < 1 {
|
if len(response.Bank) < 1 {
|
||||||
fmt.Println("No banking messages received")
|
fmt.Println("No banking messages received")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if stmt, ok := response.Banking[0].(ofxgo.StatementResponse); ok {
|
if stmt, ok := response.Bank[0].(*ofxgo.StatementResponse); ok {
|
||||||
fmt.Printf("Balance: %s %s (as of %s)\n", stmt.BalAmt, stmt.CurDef, stmt.DtAsOf)
|
fmt.Printf("Balance: %s %s (as of %s)\n", stmt.BalAmt, stmt.CurDef, stmt.DtAsOf)
|
||||||
fmt.Println("Transactions:")
|
fmt.Println("Transactions:")
|
||||||
for _, tran := range stmt.BankTranList.Transactions {
|
for _, tran := range stmt.BankTranList.Transactions {
|
||||||
|
@ -49,7 +49,7 @@ func ccDownload() {
|
|||||||
},
|
},
|
||||||
Include: true,
|
Include: true,
|
||||||
}
|
}
|
||||||
query.CreditCards = append(query.CreditCards, &statementRequest)
|
query.CreditCard = append(query.CreditCard, &statementRequest)
|
||||||
|
|
||||||
response, err := client.RequestNoParse(query)
|
response, err := client.RequestNoParse(query)
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ func ccTransactions() {
|
|||||||
},
|
},
|
||||||
Include: true,
|
Include: true,
|
||||||
}
|
}
|
||||||
query.CreditCards = append(query.CreditCards, &statementRequest)
|
query.CreditCard = append(query.CreditCard, &statementRequest)
|
||||||
|
|
||||||
response, err := client.Request(query)
|
response, err := client.Request(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -50,12 +50,12 @@ func ccTransactions() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(response.CreditCards) < 1 {
|
if len(response.CreditCard) < 1 {
|
||||||
fmt.Println("No banking messages received")
|
fmt.Println("No banking messages received")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if stmt, ok := response.CreditCards[0].(ofxgo.CCStatementResponse); ok {
|
if stmt, ok := response.CreditCard[0].(*ofxgo.CCStatementResponse); ok {
|
||||||
fmt.Printf("Balance: %s %s (as of %s)\n", stmt.BalAmt, stmt.CurDef, stmt.DtAsOf)
|
fmt.Printf("Balance: %s %s (as of %s)\n", stmt.BalAmt, stmt.CurDef, stmt.DtAsOf)
|
||||||
fmt.Println("Transactions:")
|
fmt.Println("Transactions:")
|
||||||
for _, tran := range stmt.BankTranList.Transactions {
|
for _, tran := range stmt.BankTranList.Transactions {
|
||||||
|
@ -54,7 +54,7 @@ func getAccounts() {
|
|||||||
|
|
||||||
fmt.Printf("\nFound the following accounts:\n\n")
|
fmt.Printf("\nFound the following accounts:\n\n")
|
||||||
|
|
||||||
if acctinfo, ok := response.Signup[0].(ofxgo.AcctInfoResponse); ok {
|
if acctinfo, ok := response.Signup[0].(*ofxgo.AcctInfoResponse); ok {
|
||||||
for _, acct := range acctinfo.AcctInfo {
|
for _, acct := range acctinfo.AcctInfo {
|
||||||
if acct.BankAcctInfo != nil {
|
if acct.BankAcctInfo != nil {
|
||||||
fmt.Printf("Bank Account:\n\tBankId: \"%s\"\n\tAcctId: \"%s\"\n\tAcctType: %s\n", acct.BankAcctInfo.BankAcctFrom.BankId, acct.BankAcctInfo.BankAcctFrom.AcctId, acct.BankAcctInfo.BankAcctFrom.AcctType)
|
fmt.Printf("Bank Account:\n\tBankId: \"%s\"\n\tAcctId: \"%s\"\n\tAcctType: %s\n", acct.BankAcctInfo.BankAcctFrom.BankId, acct.BankAcctInfo.BankAcctFrom.AcctId, acct.BankAcctInfo.BankAcctFrom.AcctType)
|
||||||
|
@ -58,7 +58,7 @@ func invDownload() {
|
|||||||
Include401K: true,
|
Include401K: true,
|
||||||
Include401KBal: true,
|
Include401KBal: true,
|
||||||
}
|
}
|
||||||
query.Investments = append(query.Investments, &statementRequest)
|
query.InvStmt = append(query.InvStmt, &statementRequest)
|
||||||
|
|
||||||
response, err := client.RequestNoParse(query)
|
response, err := client.RequestNoParse(query)
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ func invTransactions() {
|
|||||||
Include401K: true,
|
Include401K: true,
|
||||||
Include401KBal: true,
|
Include401KBal: true,
|
||||||
}
|
}
|
||||||
query.Investments = append(query.Investments, &statementRequest)
|
query.InvStmt = append(query.InvStmt, &statementRequest)
|
||||||
|
|
||||||
response, err := client.Request(query)
|
response, err := client.Request(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -59,12 +59,12 @@ func invTransactions() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(response.Investments) < 1 {
|
if len(response.InvStmt) < 1 {
|
||||||
fmt.Println("No investment messages received")
|
fmt.Println("No investment messages received")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if stmt, ok := response.Investments[0].(ofxgo.InvStatementResponse); ok {
|
if stmt, ok := response.InvStmt[0].(*ofxgo.InvStatementResponse); ok {
|
||||||
availCash := big.Rat(stmt.InvBal.AvailCash)
|
availCash := big.Rat(stmt.InvBal.AvailCash)
|
||||||
if availCash.IsInt() && availCash.Num().Int64() != 0 {
|
if availCash.IsInt() && availCash.Num().Int64() != 0 {
|
||||||
fmt.Printf("Balance: %s %s (as of %s)\n", stmt.InvBal.AvailCash, stmt.CurDef, stmt.DtAsOf)
|
fmt.Printf("Balance: %s %s (as of %s)\n", stmt.InvBal.AvailCash, stmt.CurDef, stmt.DtAsOf)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package ofxgo
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"github.com/aclindsa/go/src/encoding/xml"
|
"github.com/aclindsa/go/src/encoding/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -57,41 +56,15 @@ type CCStatementResponse struct {
|
|||||||
MktgInfo String `xml:"CCSTMTRS>MKTGINFO,omitempty"` // Marketing information
|
MktgInfo String `xml:"CCSTMTRS>MKTGINFO,omitempty"` // Marketing information
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sr CCStatementResponse) Name() string {
|
func (sr *CCStatementResponse) Name() string {
|
||||||
return "CCSTMTTRNRS"
|
return "CCSTMTTRNRS"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sr CCStatementResponse) Valid() (bool, error) {
|
func (sr *CCStatementResponse) Valid() (bool, error) {
|
||||||
//TODO implement
|
//TODO implement
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sr CCStatementResponse) Type() messageType {
|
func (sr *CCStatementResponse) Type() messageType {
|
||||||
return CreditCardRs
|
return CreditCardRs
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeCCMessageSet(d *xml.Decoder, start xml.StartElement) ([]Message, error) {
|
|
||||||
var msgs []Message
|
|
||||||
for {
|
|
||||||
tok, err := nextNonWhitespaceToken(d)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local {
|
|
||||||
// If we found the end of our starting element, we're done parsing
|
|
||||||
return msgs, nil
|
|
||||||
} else if startElement, ok := tok.(xml.StartElement); ok {
|
|
||||||
switch startElement.Name.Local {
|
|
||||||
case "CCSTMTTRNRS":
|
|
||||||
var info CCStatementResponse
|
|
||||||
if err := d.DecodeElement(&info, &startElement); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
msgs = append(msgs, Message(info))
|
|
||||||
default:
|
|
||||||
return nil, errors.New("Unsupported banking response tag: " + startElement.Name.Local)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, errors.New("Didn't find an opening element")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -811,41 +811,15 @@ type InvStatementResponse struct {
|
|||||||
Inv401KBal *Inv401KBal `xml:"INVSTMTRS>INV401KBAL,omitempty"`
|
Inv401KBal *Inv401KBal `xml:"INVSTMTRS>INV401KBAL,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sr InvStatementResponse) Name() string {
|
func (sr *InvStatementResponse) Name() string {
|
||||||
return "INVSTMTTRNRS"
|
return "INVSTMTTRNRS"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sr InvStatementResponse) Valid() (bool, error) {
|
func (sr *InvStatementResponse) Valid() (bool, error) {
|
||||||
//TODO implement
|
//TODO implement
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sr InvStatementResponse) Type() messageType {
|
func (sr *InvStatementResponse) Type() messageType {
|
||||||
return InvStmtRs
|
return InvStmtRs
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeInvestmentsMessageSet(d *xml.Decoder, start xml.StartElement) ([]Message, error) {
|
|
||||||
var msgs []Message
|
|
||||||
for {
|
|
||||||
tok, err := nextNonWhitespaceToken(d)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local {
|
|
||||||
// If we found the end of our starting element, we're done parsing
|
|
||||||
return msgs, nil
|
|
||||||
} else if startElement, ok := tok.(xml.StartElement); ok {
|
|
||||||
switch startElement.Name.Local {
|
|
||||||
case "INVSTMTTRNRS":
|
|
||||||
var info InvStatementResponse
|
|
||||||
if err := d.DecodeElement(&info, &startElement); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
msgs = append(msgs, Message(info))
|
|
||||||
default:
|
|
||||||
return nil, errors.New("Unsupported investments response tag: " + startElement.Name.Local)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, errors.New("Didn't find an opening element")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
32
profile.go
32
profile.go
@ -125,41 +125,15 @@ type ProfileResponse struct {
|
|||||||
Email String `xml:"PROFRS>EMAIL,omitempty"`
|
Email String `xml:"PROFRS>EMAIL,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pr ProfileResponse) Name() string {
|
func (pr *ProfileResponse) Name() string {
|
||||||
return "PROFTRNRS"
|
return "PROFTRNRS"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pr ProfileResponse) Valid() (bool, error) {
|
func (pr *ProfileResponse) Valid() (bool, error) {
|
||||||
//TODO implement
|
//TODO implement
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pr ProfileResponse) Type() messageType {
|
func (pr *ProfileResponse) Type() messageType {
|
||||||
return ProfileRs
|
return ProfileRs
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeProfileMessageSet(d *xml.Decoder, start xml.StartElement) ([]Message, error) {
|
|
||||||
var msgs []Message
|
|
||||||
for {
|
|
||||||
tok, err := nextNonWhitespaceToken(d)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local {
|
|
||||||
// If we found the end of our starting element, we're done parsing
|
|
||||||
return msgs, nil
|
|
||||||
} else if startElement, ok := tok.(xml.StartElement); ok {
|
|
||||||
switch startElement.Name.Local {
|
|
||||||
case "PROFTRNRS":
|
|
||||||
var prof ProfileResponse
|
|
||||||
if err := d.DecodeElement(&prof, &startElement); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
msgs = append(msgs, Message(prof))
|
|
||||||
default:
|
|
||||||
return nil, errors.New("Unsupported profile response tag: " + startElement.Name.Local)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, errors.New("Didn't find an opening element")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
60
request.go
60
request.go
@ -12,19 +12,19 @@ type Request struct {
|
|||||||
Version string // OFX version string, overwritten in Client.Request()
|
Version string // OFX version string, overwritten in Client.Request()
|
||||||
Signon SignonRequest //<SIGNONMSGSETV1>
|
Signon SignonRequest //<SIGNONMSGSETV1>
|
||||||
Signup []Message //<SIGNUPMSGSETV1>
|
Signup []Message //<SIGNUPMSGSETV1>
|
||||||
Banking []Message //<BANKMSGSETV1>
|
Bank []Message //<BANKMSGSETV1>
|
||||||
CreditCards []Message //<CREDITCARDMSGSETV1>
|
CreditCard []Message //<CREDITCARDMSGSETV1>
|
||||||
//<LOANMSGSETV1>
|
Loan []Message //<LOANMSGSETV1>
|
||||||
Investments []Message //<INVSTMTMSGSETV1>
|
InvStmt []Message //<INVSTMTMSGSETV1>
|
||||||
//<INTERXFERMSGSETV1>
|
InterXfer []Message //<INTERXFERMSGSETV1>
|
||||||
//<WIREXFERMSGSETV1>
|
WireXfer []Message //<WIREXFERMSGSETV1>
|
||||||
//<BILLPAYMSGSETV1>
|
Billpay []Message //<BILLPAYMSGSETV1>
|
||||||
//<EMAILMSGSETV1>
|
Email []Message //<EMAILMSGSETV1>
|
||||||
Securities []Message //<SECLISTMSGSETV1>
|
SecList []Message //<SECLISTMSGSETV1>
|
||||||
//<PRESDIRMSGSETV1>
|
PresDir []Message //<PRESDIRMSGSETV1>
|
||||||
//<PRESDLVMSGSETV1>
|
PresDlv []Message //<PRESDLVMSGSETV1>
|
||||||
Profile []Message //<PROFMSGSETV1>
|
Profile []Message //<PROFMSGSETV1>
|
||||||
//<IMAGEMSGSETV1>
|
Image []Message //<IMAGEMSGSETV1>
|
||||||
|
|
||||||
indent bool // Whether to indent the marshaled XML
|
indent bool // Whether to indent the marshaled XML
|
||||||
}
|
}
|
||||||
@ -118,23 +118,29 @@ NEWFILEUID:NONE
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := marshalMessageSet(encoder, oq.Signup, SignupRq); err != nil {
|
messageSets := []struct {
|
||||||
|
Messages []Message
|
||||||
|
Type messageType
|
||||||
|
}{
|
||||||
|
{oq.Signup, SignupRq},
|
||||||
|
{oq.Bank, BankRq},
|
||||||
|
{oq.CreditCard, CreditCardRq},
|
||||||
|
{oq.Loan, LoanRq},
|
||||||
|
{oq.InvStmt, InvStmtRq},
|
||||||
|
{oq.InterXfer, InterXferRq},
|
||||||
|
{oq.WireXfer, WireXferRq},
|
||||||
|
{oq.Billpay, BillpayRq},
|
||||||
|
{oq.Email, EmailRq},
|
||||||
|
{oq.SecList, SecListRq},
|
||||||
|
{oq.PresDir, PresDirRq},
|
||||||
|
{oq.PresDlv, PresDlvRq},
|
||||||
|
{oq.Profile, ProfileRq},
|
||||||
|
{oq.Image, ImageRq},
|
||||||
|
}
|
||||||
|
for _, set := range messageSets {
|
||||||
|
if err := marshalMessageSet(encoder, set.Messages, set.Type); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := marshalMessageSet(encoder, oq.Banking, BankRq); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := marshalMessageSet(encoder, oq.CreditCards, CreditCardRq); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := marshalMessageSet(encoder, oq.Investments, InvStmtRq); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := marshalMessageSet(encoder, oq.Securities, SecListRq); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := marshalMessageSet(encoder, oq.Profile, ProfileRq); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := encoder.EncodeToken(ofxElement.End()); err != nil {
|
if err := encoder.EncodeToken(ofxElement.End()); err != nil {
|
||||||
|
145
response.go
145
response.go
@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"github.com/aclindsa/go/src/encoding/xml"
|
"github.com/aclindsa/go/src/encoding/xml"
|
||||||
"io"
|
"io"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -13,19 +14,19 @@ type Response struct {
|
|||||||
Version string // String for OFX header, defaults to 203
|
Version string // String for OFX header, defaults to 203
|
||||||
Signon SignonResponse //<SIGNONMSGSETV1>
|
Signon SignonResponse //<SIGNONMSGSETV1>
|
||||||
Signup []Message //<SIGNUPMSGSETV1>
|
Signup []Message //<SIGNUPMSGSETV1>
|
||||||
Banking []Message //<BANKMSGSETV1>
|
Bank []Message //<BANKMSGSETV1>
|
||||||
CreditCards []Message //<CREDITCARDMSGSETV1>
|
CreditCard []Message //<CREDITCARDMSGSETV1>
|
||||||
//<LOANMSGSETV1>
|
Loan []Message //<LOANMSGSETV1>
|
||||||
Investments []Message //<INVSTMTMSGSETV1>
|
InvStmt []Message //<INVSTMTMSGSETV1>
|
||||||
//<INTERXFERMSGSETV1>
|
InterXfer []Message //<INTERXFERMSGSETV1>
|
||||||
//<WIREXFERMSGSETV1>
|
WireXfer []Message //<WIREXFERMSGSETV1>
|
||||||
//<BILLPAYMSGSETV1>
|
Billpay []Message //<BILLPAYMSGSETV1>
|
||||||
//<EMAILMSGSETV1>
|
Email []Message //<EMAILMSGSETV1>
|
||||||
Securities []Message //<SECLISTMSGSETV1>
|
SecList []Message //<SECLISTMSGSETV1>
|
||||||
//<PRESDIRMSGSETV1>
|
PresDir []Message //<PRESDIRMSGSETV1>
|
||||||
//<PRESDLVMSGSETV1>
|
PresDlv []Message //<PRESDLVMSGSETV1>
|
||||||
Profile []Message //<PROFMSGSETV1>
|
Profile []Message //<PROFMSGSETV1>
|
||||||
//<IMAGEMSGSETV1>
|
Image []Message //<IMAGEMSGSETV1>
|
||||||
}
|
}
|
||||||
|
|
||||||
func (or *Response) readSGMLHeaders(r *bufio.Reader) error {
|
func (or *Response) readSGMLHeaders(r *bufio.Reader) error {
|
||||||
@ -180,6 +181,59 @@ func guessVersion(r *bufio.Reader) (bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var responseTypes = map[string]map[string]reflect.Type{
|
||||||
|
SignupRs.String(): map[string]reflect.Type{
|
||||||
|
(&AcctInfoResponse{}).Name(): reflect.TypeOf(AcctInfoResponse{})},
|
||||||
|
BankRs.String(): map[string]reflect.Type{
|
||||||
|
(&StatementResponse{}).Name(): reflect.TypeOf(StatementResponse{})},
|
||||||
|
CreditCardRs.String(): map[string]reflect.Type{
|
||||||
|
(&CCStatementResponse{}).Name(): reflect.TypeOf(CCStatementResponse{})},
|
||||||
|
LoanRs.String(): map[string]reflect.Type{},
|
||||||
|
InvStmtRs.String(): map[string]reflect.Type{
|
||||||
|
(&InvStatementResponse{}).Name(): reflect.TypeOf(InvStatementResponse{})},
|
||||||
|
InterXferRs.String(): map[string]reflect.Type{},
|
||||||
|
WireXferRs.String(): map[string]reflect.Type{},
|
||||||
|
BillpayRs.String(): map[string]reflect.Type{},
|
||||||
|
EmailRs.String(): map[string]reflect.Type{},
|
||||||
|
SecListRs.String(): map[string]reflect.Type{
|
||||||
|
(&SecListResponse{}).Name(): reflect.TypeOf(SecListResponse{}),
|
||||||
|
(&SecurityList{}).Name(): reflect.TypeOf(SecurityList{})},
|
||||||
|
PresDirRs.String(): map[string]reflect.Type{},
|
||||||
|
PresDlvRs.String(): map[string]reflect.Type{},
|
||||||
|
ProfileRs.String(): map[string]reflect.Type{
|
||||||
|
(&ProfileResponse{}).Name(): reflect.TypeOf(ProfileResponse{})},
|
||||||
|
ImageRs.String(): map[string]reflect.Type{},
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeMessageSet(d *xml.Decoder, start xml.StartElement, msgs *[]Message) error {
|
||||||
|
setTypes, ok := responseTypes[start.Name.Local]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Invalid message set: " + start.Name.Local)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
tok, err := nextNonWhitespaceToken(d)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local {
|
||||||
|
// If we found the end of our starting element, we're done parsing
|
||||||
|
return nil
|
||||||
|
} else if startElement, ok := tok.(xml.StartElement); ok {
|
||||||
|
responseType, ok := setTypes[startElement.Name.Local]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Unsupported response transaction for " + start.Name.Local + ": " + startElement.Name.Local)
|
||||||
|
}
|
||||||
|
response := reflect.New(responseType).Interface()
|
||||||
|
responseMessage := response.(Message)
|
||||||
|
if err := d.DecodeElement(responseMessage, &startElement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*msgs = append(*msgs, responseMessage)
|
||||||
|
} else {
|
||||||
|
return errors.New("Didn't find an opening element")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ParseResponse parses an OFX response in SGML or XML into a Response object
|
// ParseResponse parses an OFX response in SGML or XML into a Response object
|
||||||
// from the given io.Reader
|
// from the given io.Reader
|
||||||
//
|
//
|
||||||
@ -248,6 +302,23 @@ func ParseResponse(reader io.Reader) (*Response, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var messageSlices = map[string]*[]Message{
|
||||||
|
SignupRs.String(): &or.Signup,
|
||||||
|
BankRs.String(): &or.Bank,
|
||||||
|
CreditCardRs.String(): &or.CreditCard,
|
||||||
|
LoanRs.String(): &or.Loan,
|
||||||
|
InvStmtRs.String(): &or.InvStmt,
|
||||||
|
InterXferRs.String(): &or.InterXfer,
|
||||||
|
WireXferRs.String(): &or.WireXfer,
|
||||||
|
BillpayRs.String(): &or.Billpay,
|
||||||
|
EmailRs.String(): &or.Email,
|
||||||
|
SecListRs.String(): &or.SecList,
|
||||||
|
PresDirRs.String(): &or.PresDir,
|
||||||
|
PresDlvRs.String(): &or.PresDlv,
|
||||||
|
ProfileRs.String(): &or.Profile,
|
||||||
|
ImageRs.String(): &or.Image,
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
tok, err = nextNonWhitespaceToken(decoder)
|
tok, err = nextNonWhitespaceToken(decoder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -255,55 +326,13 @@ func ParseResponse(reader io.Reader) (*Response, error) {
|
|||||||
} else if ofxEnd, ok := tok.(xml.EndElement); ok && ofxEnd.Name.Local == "OFX" {
|
} else if ofxEnd, ok := tok.(xml.EndElement); ok && ofxEnd.Name.Local == "OFX" {
|
||||||
return &or, nil // found closing XML element, so we're done
|
return &or, nil // found closing XML element, so we're done
|
||||||
} else if start, ok := tok.(xml.StartElement); ok {
|
} else if start, ok := tok.(xml.StartElement); ok {
|
||||||
// TODO decode other types
|
slice, ok := messageSlices[start.Name.Local]
|
||||||
switch start.Name.Local {
|
if !ok {
|
||||||
case "SIGNUPMSGSRSV1":
|
return nil, errors.New("Invalid message set: " + start.Name.Local)
|
||||||
msgs, err := decodeSignupMessageSet(decoder, start)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
or.Signup = msgs
|
if err := decodeMessageSet(decoder, start, slice); err != nil {
|
||||||
case "BANKMSGSRSV1":
|
|
||||||
msgs, err := decodeBankingMessageSet(decoder, start)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
or.Banking = msgs
|
|
||||||
case "CREDITCARDMSGSRSV1":
|
|
||||||
msgs, err := decodeCCMessageSet(decoder, start)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
or.CreditCards = msgs
|
|
||||||
//case "LOANMSGSRSV1":
|
|
||||||
case "INVSTMTMSGSRSV1":
|
|
||||||
msgs, err := decodeInvestmentsMessageSet(decoder, start)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
or.Investments = msgs
|
|
||||||
//case "INTERXFERMSGSRSV1":
|
|
||||||
//case "WIREXFERMSGSRSV1":
|
|
||||||
//case "BILLPAYMSGSRSV1":
|
|
||||||
//case "EMAILMSGSRSV1":
|
|
||||||
case "SECLISTMSGSRSV1":
|
|
||||||
msgs, err := decodeSecuritiesMessageSet(decoder, start)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
or.Securities = msgs
|
|
||||||
//case "PRESDIRMSGSRSV1":
|
|
||||||
//case "PRESDLVMSGSRSV1":
|
|
||||||
case "PROFMSGSRSV1":
|
|
||||||
msgs, err := decodeProfileMessageSet(decoder, start)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
or.Profile = msgs
|
|
||||||
//case "IMAGEMSGSRSV1":
|
|
||||||
default:
|
|
||||||
return nil, errors.New("Unsupported message set: " + start.Name.Local)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("Found unexpected token")
|
return nil, errors.New("Found unexpected token")
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ func checkEqual(t *testing.T, fieldName string, expected, actual reflect.Value)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if expected.Type() != actual.Type() {
|
if expected.Type() != actual.Type() {
|
||||||
t.Fatalf("%s: Expected %s type for %s, found %s\n", t.Name(), expected.Type().Name(), fieldName, actual.Type().Name())
|
t.Fatalf("%s: Expected %s type for %s, found %s\n", t.Name(), expected.Type(), fieldName, actual.Type())
|
||||||
}
|
}
|
||||||
|
|
||||||
equalMethod := equalMethodOf(expected)
|
equalMethod := equalMethodOf(expected)
|
||||||
|
@ -50,16 +50,16 @@ type SecListResponse struct {
|
|||||||
// SECLISTRS is always empty, so we don't parse it here. The actual securities list will be in a top-level element parallel to SECLISTTRNRS
|
// SECLISTRS is always empty, so we don't parse it here. The actual securities list will be in a top-level element parallel to SECLISTTRNRS
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r SecListResponse) Name() string {
|
func (r *SecListResponse) Name() string {
|
||||||
return "SECLISTTRNRS"
|
return "SECLISTTRNRS"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r SecListResponse) Valid() (bool, error) {
|
func (r *SecListResponse) Valid() (bool, error) {
|
||||||
// TODO implement
|
// TODO implement
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r SecListResponse) Type() messageType {
|
func (r *SecListResponse) Type() messageType {
|
||||||
return SecListRs
|
return SecListRs
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,16 +175,16 @@ type SecurityList struct {
|
|||||||
Securities []Security
|
Securities []Security
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r SecurityList) Name() string {
|
func (r *SecurityList) Name() string {
|
||||||
return "SECLIST"
|
return "SECLIST"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r SecurityList) Valid() (bool, error) {
|
func (r *SecurityList) Valid() (bool, error) {
|
||||||
// TODO implement
|
// TODO implement
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r SecurityList) Type() messageType {
|
func (r *SecurityList) Type() messageType {
|
||||||
return SecListRs
|
return SecListRs
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,35 +236,3 @@ func (r *SecurityList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeSecuritiesMessageSet(d *xml.Decoder, start xml.StartElement) ([]Message, error) {
|
|
||||||
var msgs []Message
|
|
||||||
for {
|
|
||||||
tok, err := nextNonWhitespaceToken(d)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local {
|
|
||||||
// If we found the end of our starting element, we're done parsing
|
|
||||||
return msgs, nil
|
|
||||||
} else if startElement, ok := tok.(xml.StartElement); ok {
|
|
||||||
switch startElement.Name.Local {
|
|
||||||
case "SECLISTTRNRS":
|
|
||||||
var info SecListResponse
|
|
||||||
if err := d.DecodeElement(&info, &startElement); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
msgs = append(msgs, Message(info))
|
|
||||||
case "SECLIST":
|
|
||||||
var info SecurityList
|
|
||||||
if err := d.DecodeElement(&info, &startElement); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
msgs = append(msgs, Message(info))
|
|
||||||
default:
|
|
||||||
return nil, errors.New("Unsupported securities response tag: " + startElement.Name.Local)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, errors.New("Didn't find an opening element")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
35
signup.go
35
signup.go
@ -1,7 +1,6 @@
|
|||||||
package ofxgo
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/aclindsa/go/src/encoding/xml"
|
"github.com/aclindsa/go/src/encoding/xml"
|
||||||
)
|
)
|
||||||
@ -120,41 +119,15 @@ type AcctInfoResponse struct {
|
|||||||
AcctInfo []AcctInfo `xml:"ACCTINFORS>ACCTINFO,omitempty"`
|
AcctInfo []AcctInfo `xml:"ACCTINFORS>ACCTINFO,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (air AcctInfoResponse) Name() string {
|
func (air *AcctInfoResponse) Name() string {
|
||||||
return "ACCTINFORS"
|
return "ACCTINFOTRNRS"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (air AcctInfoResponse) Valid() (bool, error) {
|
func (air *AcctInfoResponse) Valid() (bool, error) {
|
||||||
//TODO implement
|
//TODO implement
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (air AcctInfoResponse) Type() messageType {
|
func (air *AcctInfoResponse) Type() messageType {
|
||||||
return SignupRs
|
return SignupRs
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeSignupMessageSet(d *xml.Decoder, start xml.StartElement) ([]Message, error) {
|
|
||||||
var msgs []Message
|
|
||||||
for {
|
|
||||||
tok, err := nextNonWhitespaceToken(d)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local {
|
|
||||||
// If we found the end of our starting element, we're done parsing
|
|
||||||
return msgs, nil
|
|
||||||
} else if startElement, ok := tok.(xml.StartElement); ok {
|
|
||||||
switch startElement.Name.Local {
|
|
||||||
case "ACCTINFOTRNRS":
|
|
||||||
var info AcctInfoResponse
|
|
||||||
if err := d.DecodeElement(&info, &startElement); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
msgs = append(msgs, Message(info))
|
|
||||||
default:
|
|
||||||
return nil, errors.New("Unsupported signup response tag: " + startElement.Name.Local)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, errors.New("Didn't find an opening element")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -150,7 +150,7 @@ func TestUnmarshalAcctInfoResponse(t *testing.T) {
|
|||||||
BankAcctInfo: &bankacctinfo,
|
BankAcctInfo: &bankacctinfo,
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
expected.Signup = append(expected.Signup, acctInfoResponse)
|
expected.Signup = append(expected.Signup, &acctInfoResponse)
|
||||||
|
|
||||||
response, err := ofxgo.ParseResponse(responseReader)
|
response, err := ofxgo.ParseResponse(responseReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user