mirror of
https://github.com/aclindsa/ofxgo.git
synced 2024-11-24 04:00:04 -05:00
Merge pull request #16 from aclindsa/writable_responses
Allow for marshalling Response objects to strings containing XML/SGML
This commit is contained in:
commit
3e8a9c5a53
@ -283,4 +283,5 @@ func TestUnmarshalBankStatementResponse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkResponsesEqual(t, &expected, response)
|
checkResponsesEqual(t, &expected, response)
|
||||||
|
checkResponseRoundTrip(t, response)
|
||||||
}
|
}
|
||||||
|
26
common.go
26
common.go
@ -3,10 +3,36 @@ package ofxgo
|
|||||||
//go:generate ./generate_constants.py
|
//go:generate ./generate_constants.py
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/aclindsa/xml"
|
"github.com/aclindsa/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func writeHeader(b *bytes.Buffer, v ofxVersion) error {
|
||||||
|
// Write the header appropriate to our version
|
||||||
|
switch v {
|
||||||
|
case OfxVersion102, OfxVersion103, OfxVersion151, OfxVersion160:
|
||||||
|
b.WriteString(`OFXHEADER:100
|
||||||
|
DATA:OFXSGML
|
||||||
|
VERSION:` + v.String() + `
|
||||||
|
SECURITY:NONE
|
||||||
|
ENCODING:USASCII
|
||||||
|
CHARSET:1252
|
||||||
|
COMPRESSION:NONE
|
||||||
|
OLDFILEUID:NONE
|
||||||
|
NEWFILEUID:NONE
|
||||||
|
|
||||||
|
`)
|
||||||
|
case OfxVersion200, OfxVersion201, OfxVersion202, OfxVersion203, OfxVersion210, OfxVersion211, OfxVersion220:
|
||||||
|
b.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + "\n")
|
||||||
|
b.WriteString(`<?OFX OFXHEADER="200" VERSION="` + v.String() + `" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>` + "\n")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%d is not a valid OFX version string", v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Message represents an OFX message in a message set. it is used to ease
|
// Message represents an OFX message in a message set. it is used to ease
|
||||||
// marshalling and unmarshalling.
|
// marshalling and unmarshalling.
|
||||||
type Message interface {
|
type Message interface {
|
||||||
|
@ -161,4 +161,5 @@ NEWFILEUID:NONE
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkResponsesEqual(t, &expected, response)
|
checkResponsesEqual(t, &expected, response)
|
||||||
|
checkResponseRoundTrip(t, response)
|
||||||
}
|
}
|
||||||
|
1
go.mod
1
go.mod
@ -2,6 +2,7 @@ module github.com/aclindsa/ofxgo
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aclindsa/xml v0.0.0-20171002130543-5d4402bb4a20
|
github.com/aclindsa/xml v0.0.0-20171002130543-5d4402bb4a20
|
||||||
|
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c
|
||||||
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4 // indirect
|
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4 // indirect
|
||||||
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 // indirect
|
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 // indirect
|
||||||
golang.org/x/text v0.0.0-20180911161511-905a57155faa
|
golang.org/x/text v0.0.0-20180911161511-905a57155faa
|
||||||
|
2
go.sum
2
go.sum
@ -1,5 +1,7 @@
|
|||||||
github.com/aclindsa/xml v0.0.0-20171002130543-5d4402bb4a20 h1:wN3KlzWq56AIgOqFzYLYVih4zVyPDViCUeG5uZxJHq4=
|
github.com/aclindsa/xml v0.0.0-20171002130543-5d4402bb4a20 h1:wN3KlzWq56AIgOqFzYLYVih4zVyPDViCUeG5uZxJHq4=
|
||||||
github.com/aclindsa/xml v0.0.0-20171002130543-5d4402bb4a20/go.mod h1:DiEHtTD+e6zS3+R95F05Bfbcsfv13wZTi2M4LfAFLBE=
|
github.com/aclindsa/xml v0.0.0-20171002130543-5d4402bb4a20/go.mod h1:DiEHtTD+e6zS3+R95F05Bfbcsfv13wZTi2M4LfAFLBE=
|
||||||
|
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0=
|
||||||
|
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
|
||||||
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4 h1:Vk3wNqEZwyGyei9yq5ekj7frek2u7HUfffJ1/opblzc=
|
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4 h1:Vk3wNqEZwyGyei9yq5ekj7frek2u7HUfffJ1/opblzc=
|
||||||
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 h1:O33LKL7WyJgjN9CvxfTIomjIClbd/Kq86/iipowHQU0=
|
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 h1:O33LKL7WyJgjN9CvxfTIomjIClbd/Kq86/iipowHQU0=
|
||||||
|
216
invstmt.go
216
invstmt.go
@ -465,6 +465,7 @@ type InvBankTransaction struct {
|
|||||||
// security-related transactions themselves. It must be unmarshalled manually
|
// security-related transactions themselves. It must be unmarshalled manually
|
||||||
// due to the structure (don't know what kind of InvTransaction is coming next)
|
// due to the structure (don't know what kind of InvTransaction is coming next)
|
||||||
type InvTranList struct {
|
type InvTranList struct {
|
||||||
|
XMLName xml.Name `xml:"INVTRANLIST"`
|
||||||
DtStart Date
|
DtStart Date
|
||||||
DtEnd Date // This is the value that should be sent as <DTSTART> in the next InvStatementRequest to ensure that no transactions are missed
|
DtEnd Date // This is the value that should be sent as <DTSTART> in the next InvStatementRequest to ensure that no transactions are missed
|
||||||
InvTransactions []InvTransaction
|
InvTransactions []InvTransaction
|
||||||
@ -630,6 +631,119 @@ func (l *InvTranList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalXML handles marshalling an InvTranList element to an SGML/XML string
|
||||||
|
func (l *InvTranList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
invTranListElement := xml.StartElement{Name: xml.Name{Local: "INVTRANLIST"}}
|
||||||
|
if err := e.EncodeToken(invTranListElement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err := e.EncodeElement(&l.DtStart, xml.StartElement{Name: xml.Name{Local: "DTSTART"}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = e.EncodeElement(&l.DtEnd, xml.StartElement{Name: xml.Name{Local: "DTEND"}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, t := range l.InvTransactions {
|
||||||
|
start := xml.StartElement{Name: xml.Name{Local: t.TransactionType()}}
|
||||||
|
switch tran := t.(type) {
|
||||||
|
case BuyDebt:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case BuyMF:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case BuyOpt:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case BuyOther:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case BuyStock:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case ClosureOpt:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case Income:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case InvExpense:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case JrnlFund:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case JrnlSec:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case MarginInterest:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case Reinvest:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case RetOfCap:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case SellDebt:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case SellMF:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case SellOpt:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case SellOther:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case SellStock:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case Split:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case Transfer:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("Invalid INVTRANLIST child type: " + tran.TransactionType())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, tran := range l.BankTransactions {
|
||||||
|
err = e.EncodeElement(&tran, xml.StartElement{Name: xml.Name{Local: "INVBANKTRAN"}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(invTranListElement.End()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// InvPosition contains generic position information included in each of the
|
// InvPosition contains generic position information included in each of the
|
||||||
// other *Position types
|
// other *Position types
|
||||||
type InvPosition struct {
|
type InvPosition struct {
|
||||||
@ -770,6 +884,45 @@ func (p *PositionList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalXML handles marshalling a PositionList to an XML string
|
||||||
|
func (p *PositionList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
invPosListElement := xml.StartElement{Name: xml.Name{Local: "INVPOSLIST"}}
|
||||||
|
if err := e.EncodeToken(invPosListElement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, position := range *p {
|
||||||
|
start := xml.StartElement{Name: xml.Name{Local: position.PositionType()}}
|
||||||
|
switch pos := position.(type) {
|
||||||
|
case DebtPosition:
|
||||||
|
if err := e.EncodeElement(&pos, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case MFPosition:
|
||||||
|
if err := e.EncodeElement(&pos, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OptPosition:
|
||||||
|
if err := e.EncodeElement(&pos, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OtherPosition:
|
||||||
|
if err := e.EncodeElement(&pos, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case StockPosition:
|
||||||
|
if err := e.EncodeElement(&pos, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("Invalid INVPOSLIST child type: " + pos.PositionType())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(invPosListElement.End()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// InvBalance contains three (or optionally four) specified balances as well as
|
// InvBalance contains three (or optionally four) specified balances as well as
|
||||||
// a free-form list of generic balance information which may be provided by an
|
// a free-form list of generic balance information which may be provided by an
|
||||||
// FI.
|
// FI.
|
||||||
@ -1036,6 +1189,69 @@ func (o *OOList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalXML handles marshalling an OOList to an XML string
|
||||||
|
func (o *OOList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
ooListElement := xml.StartElement{Name: xml.Name{Local: "INVOOLIST"}}
|
||||||
|
if err := e.EncodeToken(ooListElement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, openorder := range *o {
|
||||||
|
start := xml.StartElement{Name: xml.Name{Local: openorder.OrderType()}}
|
||||||
|
switch oo := openorder.(type) {
|
||||||
|
case OOBuyDebt:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOBuyMF:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOBuyOpt:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOBuyOther:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOBuyStock:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOSellDebt:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOSellMF:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOSellOpt:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOSellOther:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOSellStock:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOSwitchMF:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("Invalid OOLIST child type: " + oo.OrderType())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(ooListElement.End()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ContribSecurity identifies current contribution allocation for a security in
|
// ContribSecurity identifies current contribution allocation for a security in
|
||||||
// a 401(k) account
|
// a 401(k) account
|
||||||
type ContribSecurity struct {
|
type ContribSecurity struct {
|
||||||
|
@ -602,6 +602,7 @@ func TestUnmarshalInvStatementResponse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkResponsesEqual(t, &expected, response)
|
checkResponsesEqual(t, &expected, response)
|
||||||
|
checkResponseRoundTrip(t, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalInvStatementResponse102(t *testing.T) {
|
func TestUnmarshalInvStatementResponse102(t *testing.T) {
|
||||||
@ -957,6 +958,7 @@ NEWFILEUID: NONE
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkResponsesEqual(t, &expected, response)
|
checkResponsesEqual(t, &expected, response)
|
||||||
|
checkResponseRoundTrip(t, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalInvTranList(t *testing.T) {
|
func TestUnmarshalInvTranList(t *testing.T) {
|
||||||
|
30
profile.go
30
profile.go
@ -3,6 +3,7 @@ package ofxgo
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/aclindsa/xml"
|
"github.com/aclindsa/xml"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProfileRequest represents a request for a server to provide a profile of its
|
// ProfileRequest represents a request for a server to provide a profile of its
|
||||||
@ -126,6 +127,35 @@ func (msl *MessageSetList) UnmarshalXML(d *xml.Decoder, start xml.StartElement)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalXML handles marshalling a MessageSetList element to an XML string
|
||||||
|
func (msl *MessageSetList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
messageSetListElement := xml.StartElement{Name: xml.Name{Local: "MSGSETLIST"}}
|
||||||
|
if err := e.EncodeToken(messageSetListElement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, messageset := range *msl {
|
||||||
|
if !strings.HasSuffix(messageset.Name, "V1") {
|
||||||
|
return errors.New("Expected MessageSet.Name to end with \"V1\"")
|
||||||
|
}
|
||||||
|
messageSetName := strings.TrimSuffix(messageset.Name, "V1")
|
||||||
|
messageSetElement := xml.StartElement{Name: xml.Name{Local: messageSetName}}
|
||||||
|
if err := e.EncodeToken(messageSetElement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
start := xml.StartElement{Name: xml.Name{Local: messageset.Name}}
|
||||||
|
if err := e.EncodeElement(&messageset, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(messageSetElement.End()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(messageSetListElement.End()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ProfileResponse contains a requested profile of the server's capabilities
|
// ProfileResponse contains a requested profile of the server's capabilities
|
||||||
// (which message sets and versions it supports, how to access them, which
|
// (which message sets and versions it supports, how to access them, which
|
||||||
// languages and which types of synchronization they support, etc.). Note that
|
// languages and which types of synchronization they support, etc.). Note that
|
||||||
|
@ -325,4 +325,5 @@ NEWFILEUID:NONE
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkResponsesEqual(t, &expected, response)
|
checkResponsesEqual(t, &expected, response)
|
||||||
|
checkResponseRoundTrip(t, response)
|
||||||
}
|
}
|
||||||
|
25
request.go
25
request.go
@ -3,7 +3,6 @@ package ofxgo
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"github.com/aclindsa/xml"
|
"github.com/aclindsa/xml"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -35,7 +34,7 @@ type Request struct {
|
|||||||
indent bool // Whether to indent the marshaled XML
|
indent bool // Whether to indent the marshaled XML
|
||||||
}
|
}
|
||||||
|
|
||||||
func marshalMessageSet(e *xml.Encoder, requests []Message, set messageType, version ofxVersion) error {
|
func encodeMessageSet(e *xml.Encoder, requests []Message, set messageType, version ofxVersion) error {
|
||||||
if len(requests) > 0 {
|
if len(requests) > 0 {
|
||||||
messageSetElement := xml.StartElement{Name: xml.Name{Local: set.String()}}
|
messageSetElement := xml.StartElement{Name: xml.Name{Local: set.String()}}
|
||||||
if err := e.EncodeToken(messageSetElement); err != nil {
|
if err := e.EncodeToken(messageSetElement); err != nil {
|
||||||
@ -80,25 +79,7 @@ func (oq *Request) Marshal() (*bytes.Buffer, error) {
|
|||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
|
|
||||||
// Write the header appropriate to our version
|
// Write the header appropriate to our version
|
||||||
switch oq.Version {
|
writeHeader(&b, oq.Version)
|
||||||
case OfxVersion102, OfxVersion103, OfxVersion151, OfxVersion160:
|
|
||||||
b.WriteString(`OFXHEADER:100
|
|
||||||
DATA:OFXSGML
|
|
||||||
VERSION:` + oq.Version.String() + `
|
|
||||||
SECURITY:NONE
|
|
||||||
ENCODING:USASCII
|
|
||||||
CHARSET:1252
|
|
||||||
COMPRESSION:NONE
|
|
||||||
OLDFILEUID:NONE
|
|
||||||
NEWFILEUID:NONE
|
|
||||||
|
|
||||||
`)
|
|
||||||
case OfxVersion200, OfxVersion201, OfxVersion202, OfxVersion203, OfxVersion210, OfxVersion211, OfxVersion220:
|
|
||||||
b.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + "\n")
|
|
||||||
b.WriteString(`<?OFX OFXHEADER="200" VERSION="` + oq.Version.String() + `" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>` + "\n")
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%d is not a valid OFX version string", oq.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
encoder := xml.NewEncoder(&b)
|
encoder := xml.NewEncoder(&b)
|
||||||
if oq.indent {
|
if oq.indent {
|
||||||
@ -145,7 +126,7 @@ NEWFILEUID:NONE
|
|||||||
{oq.Image, ImageRq},
|
{oq.Image, ImageRq},
|
||||||
}
|
}
|
||||||
for _, set := range messageSets {
|
for _, set := range messageSets {
|
||||||
if err := marshalMessageSet(encoder, set.Messages, set.Type, oq.Version); err != nil {
|
if err := encodeMessageSet(encoder, set.Messages, set.Type, oq.Version); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
67
response.go
67
response.go
@ -369,3 +369,70 @@ func ParseResponse(reader io.Reader) (*Response, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Marshal this Response into its SGML/XML representation held in a bytes.Buffer
|
||||||
|
//
|
||||||
|
// If error is non-nil, this bytes.Buffer is ready to be sent to an OFX client
|
||||||
|
func (or *Response) Marshal() (*bytes.Buffer, error) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
|
||||||
|
// Write the header appropriate to our version
|
||||||
|
writeHeader(&b, or.Version)
|
||||||
|
|
||||||
|
encoder := xml.NewEncoder(&b)
|
||||||
|
encoder.Indent("", " ")
|
||||||
|
|
||||||
|
ofxElement := xml.StartElement{Name: xml.Name{Local: "OFX"}}
|
||||||
|
|
||||||
|
if err := encoder.EncodeToken(ofxElement); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok, err := or.Signon.Valid(or.Version); !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
signonMsgSet := xml.StartElement{Name: xml.Name{Local: SignonRs.String()}}
|
||||||
|
if err := encoder.EncodeToken(signonMsgSet); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := encoder.Encode(&or.Signon); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := encoder.EncodeToken(signonMsgSet.End()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
messageSets := []struct {
|
||||||
|
Messages []Message
|
||||||
|
Type messageType
|
||||||
|
}{
|
||||||
|
{or.Signup, SignupRs},
|
||||||
|
{or.Bank, BankRs},
|
||||||
|
{or.CreditCard, CreditCardRs},
|
||||||
|
{or.Loan, LoanRs},
|
||||||
|
{or.InvStmt, InvStmtRs},
|
||||||
|
{or.InterXfer, InterXferRs},
|
||||||
|
{or.WireXfer, WireXferRs},
|
||||||
|
{or.Billpay, BillpayRs},
|
||||||
|
{or.Email, EmailRs},
|
||||||
|
{or.SecList, SecListRs},
|
||||||
|
{or.PresDir, PresDirRs},
|
||||||
|
{or.PresDlv, PresDlvRs},
|
||||||
|
{or.Prof, ProfRs},
|
||||||
|
{or.Image, ImageRs},
|
||||||
|
}
|
||||||
|
for _, set := range messageSets {
|
||||||
|
if err := encodeMessageSet(encoder, set.Messages, set.Type, or.Version); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := encoder.EncodeToken(ofxElement.End()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := encoder.Flush(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &b, nil
|
||||||
|
}
|
||||||
|
@ -136,6 +136,20 @@ func checkResponsesEqual(t *testing.T, expected, actual *ofxgo.Response) {
|
|||||||
checkEqual(t, "", reflect.ValueOf(expected), reflect.ValueOf(actual))
|
checkEqual(t, "", reflect.ValueOf(expected), reflect.ValueOf(actual))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkResponseRoundTrip(t *testing.T, response *ofxgo.Response) {
|
||||||
|
b, err := response.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error re-marshaling OFX response: %s\n", err)
|
||||||
|
}
|
||||||
|
roundtripped, err := ofxgo.ParseResponse(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error re-parsing OFX response: %s\n", err)
|
||||||
|
}
|
||||||
|
checkResponsesEqual(t, response, roundtripped)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that these samples both parse without errors, and can be converted
|
||||||
|
// back and forth without changing.
|
||||||
func TestValidSamples(t *testing.T) {
|
func TestValidSamples(t *testing.T) {
|
||||||
fn := func(path string, info os.FileInfo, err error) error {
|
fn := func(path string, info os.FileInfo, err error) error {
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
@ -147,10 +161,11 @@ func TestValidSamples(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error opening %s: %s\n", path, err)
|
t.Fatalf("Unexpected error opening %s: %s\n", path, err)
|
||||||
}
|
}
|
||||||
_, err = ofxgo.ParseResponse(file)
|
response, err := ofxgo.ParseResponse(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error parsing OFX response in %s: %s\n", path, err)
|
t.Fatalf("Unexpected error parsing OFX response in %s: %s\n", path, err)
|
||||||
}
|
}
|
||||||
|
checkResponseRoundTrip(t, response)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
filepath.Walk("samples/valid_responses", fn)
|
filepath.Walk("samples/valid_responses", fn)
|
||||||
|
40
seclist.go
40
seclist.go
@ -221,6 +221,7 @@ func (i StockInfo) SecurityType() string {
|
|||||||
// SecurityList is a container for Security objects containaing information
|
// SecurityList is a container for Security objects containaing information
|
||||||
// about securities
|
// about securities
|
||||||
type SecurityList struct {
|
type SecurityList struct {
|
||||||
|
XMLName xml.Name `xml:"SECLIST"`
|
||||||
Securities []Security
|
Securities []Security
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,3 +291,42 @@ func (r *SecurityList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalXML handles marshalling a SecurityList to an SGML/XML string
|
||||||
|
func (r *SecurityList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
secListElement := xml.StartElement{Name: xml.Name{Local: "SECLIST"}}
|
||||||
|
if err := e.EncodeToken(secListElement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, s := range r.Securities {
|
||||||
|
start := xml.StartElement{Name: xml.Name{Local: s.SecurityType()}}
|
||||||
|
switch sec := s.(type) {
|
||||||
|
case DebtInfo:
|
||||||
|
if err := e.EncodeElement(&sec, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case MFInfo:
|
||||||
|
if err := e.EncodeElement(&sec, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OptInfo:
|
||||||
|
if err := e.EncodeElement(&sec, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OtherInfo:
|
||||||
|
if err := e.EncodeElement(&sec, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case StockInfo:
|
||||||
|
if err := e.EncodeElement(&sec, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("Invalid SECLIST child type: " + sec.SecurityType())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(secListElement.End()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -155,4 +155,5 @@ func TestUnmarshalAcctInfoResponse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkResponsesEqual(t, &expected, response)
|
checkResponsesEqual(t, &expected, response)
|
||||||
|
checkResponseRoundTrip(t, response)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user