diff --git a/banking.go b/banking.go index 0778e46..2c4a5d3 100644 --- a/banking.go +++ b/banking.go @@ -5,15 +5,6 @@ import ( "github.com/golang/go/src/encoding/xml" ) -type BankAcct struct { - XMLName xml.Name // BANKACCTTO or BANKACCTFROM - BankId String `xml:"BANKID"` - BranchId String `xml:"BRANCHID,omitempty"` // Unused in USA - AcctId String `xml:"ACCTID"` - AcctType String `xml:"ACCTTYPE"` // One of CHECKING, SAVINGS, MONEYMRKT, CREDITLINE, CD - AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA -} - type StatementRequest struct { XMLName xml.Name `xml:"STMTTRNRQ"` TrnUID UID `xml:"TRNUID"` diff --git a/common.go b/common.go new file mode 100644 index 0000000..7849bd1 --- /dev/null +++ b/common.go @@ -0,0 +1,27 @@ +package ofxgo + +import ( + "github.com/golang/go/src/encoding/xml" +) + +// Represents a top-level OFX message set (i.e. BANKMSGSRSV1) +type Message interface { + Name() string // The name of the OFX element this set represents + Valid() (bool, error) // Called before a Message is marshaled and after + // it's unmarshaled to ensure the request or response is valid +} + +type BankAcct struct { + XMLName xml.Name // BANKACCTTO or BANKACCTFROM + BankId String `xml:"BANKID"` + BranchId String `xml:"BRANCHID,omitempty"` // Unused in USA + AcctId String `xml:"ACCTID"` + AcctType String `xml:"ACCTTYPE"` // One of CHECKING, SAVINGS, MONEYMRKT, CREDITLINE, CD + AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA +} + +type CCAcct struct { + XMLName xml.Name // CCACCTTO or CCACCTFROM + AcctId String `xml:"ACCTID"` + AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA +} diff --git a/request.go b/request.go new file mode 100644 index 0000000..c8ffc7e --- /dev/null +++ b/request.go @@ -0,0 +1,121 @@ +package ofxgo + +import ( + "bytes" + "errors" + "github.com/golang/go/src/encoding/xml" +) + +type Request struct { + URL string + Version string // OFX version string, overwritten in Client.Request() + Signon SignonRequest // + Signup []Message // + Banking []Message // + // + // + // + // + // + // + // + // + // + // + Profile []Message // + // + + indent bool // Whether to indent the marshaled XML +} + +func marshalMessageSet(e *xml.Encoder, requests []Message, setname string) error { + if len(requests) > 0 { + messageSetElement := xml.StartElement{Name: xml.Name{Local: setname}} + if err := e.EncodeToken(messageSetElement); err != nil { + return err + } + + for _, request := range requests { + if ok, err := request.Valid(); !ok { + return err + } + if err := e.Encode(request); err != nil { + return err + } + } + + if err := e.EncodeToken(messageSetElement.End()); err != nil { + return err + } + } + return nil +} + +func (oq *Request) Marshal() (*bytes.Buffer, error) { + var b bytes.Buffer + + // Write the header appropriate to our version + switch oq.Version { + case "102", "103", "151", "160": + b.WriteString(`OFXHEADER:100 +DATA:OFXSGML +VERSION:` + oq.Version + ` +SECURITY:NONE +ENCODING:USASCII +CHARSET:1252 +COMPRESSION:NONE +OLDFILEUID:NONE +NEWFILEUID:NONE + +`) + case "200", "201", "202", "203", "210", "211", "220": + b.WriteString(`` + "\n") + b.WriteString(`` + "\n") + default: + return nil, errors.New(oq.Version + " is not a valid OFX version string") + } + + encoder := xml.NewEncoder(&b) + if oq.indent { + encoder.Indent("", " ") + } + + ofxElement := xml.StartElement{Name: xml.Name{Local: "OFX"}} + + if err := encoder.EncodeToken(ofxElement); err != nil { + return nil, err + } + + if ok, err := oq.Signon.Valid(); !ok { + return nil, err + } + signonMsgSet := xml.StartElement{Name: xml.Name{Local: "SIGNONMSGSRQV1"}} + if err := encoder.EncodeToken(signonMsgSet); err != nil { + return nil, err + } + if err := encoder.Encode(&oq.Signon); err != nil { + return nil, err + } + if err := encoder.EncodeToken(signonMsgSet.End()); err != nil { + return nil, err + } + + if err := marshalMessageSet(encoder, oq.Signup, "SIGNUPMSGSRQV1"); err != nil { + return nil, err + } + if err := marshalMessageSet(encoder, oq.Banking, "BANKMSGSRQV1"); err != nil { + return nil, err + } + if err := marshalMessageSet(encoder, oq.Profile, "PROFMSGSRQV1"); 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 +} diff --git a/ofx.go b/response.go similarity index 70% rename from ofx.go rename to response.go index 8793577..50cb71e 100644 --- a/ofx.go +++ b/response.go @@ -9,125 +9,6 @@ import ( "strings" ) -type Message interface { - Name() string - Valid() (bool, error) -} - -type Request struct { - URL string - Version string // OFX version string, overwritten in Client.Request() - Signon SignonRequest // - Signup []Message // - Banking []Message // - // - // - // - // - // - // - // - // - // - // - Profile []Message // - // - - indent bool // Whether to indent the marshaled XML -} - -func marshalMessageSet(e *xml.Encoder, requests []Message, setname string) error { - if len(requests) > 0 { - messageSetElement := xml.StartElement{Name: xml.Name{Local: setname}} - if err := e.EncodeToken(messageSetElement); err != nil { - return err - } - - for _, request := range requests { - if ok, err := request.Valid(); !ok { - return err - } - if err := e.Encode(request); err != nil { - return err - } - } - - if err := e.EncodeToken(messageSetElement.End()); err != nil { - return err - } - } - return nil -} - -func (oq *Request) Marshal() (*bytes.Buffer, error) { - var b bytes.Buffer - - // Write the header appropriate to our version - switch oq.Version { - case "102", "103", "151", "160": - b.WriteString(`OFXHEADER:100 -DATA:OFXSGML -VERSION:` + oq.Version + ` -SECURITY:NONE -ENCODING:USASCII -CHARSET:1252 -COMPRESSION:NONE -OLDFILEUID:NONE -NEWFILEUID:NONE - -`) - case "200", "201", "202", "203", "210", "211", "220": - b.WriteString(`` + "\n") - b.WriteString(`` + "\n") - default: - return nil, errors.New(oq.Version + " is not a valid OFX version string") - } - - encoder := xml.NewEncoder(&b) - if oq.indent { - encoder.Indent("", " ") - } - - ofxElement := xml.StartElement{Name: xml.Name{Local: "OFX"}} - - if err := encoder.EncodeToken(ofxElement); err != nil { - return nil, err - } - - if ok, err := oq.Signon.Valid(); !ok { - return nil, err - } - signonMsgSet := xml.StartElement{Name: xml.Name{Local: "SIGNONMSGSRQV1"}} - if err := encoder.EncodeToken(signonMsgSet); err != nil { - return nil, err - } - if err := encoder.Encode(&oq.Signon); err != nil { - return nil, err - } - if err := encoder.EncodeToken(signonMsgSet.End()); err != nil { - return nil, err - } - - if err := marshalMessageSet(encoder, oq.Signup, "SIGNUPMSGSRQV1"); err != nil { - return nil, err - } - if err := marshalMessageSet(encoder, oq.Banking, "BANKMSGSRQV1"); err != nil { - return nil, err - } - if err := marshalMessageSet(encoder, oq.Profile, "PROFMSGSRQV1"); 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 -} - type Response struct { Version string // String for OFX header, defaults to 203 Signon SignonResponse // @@ -208,22 +89,6 @@ func (or *Response) readSGMLHeaders(r *bufio.Reader) error { return nil } -func nextNonWhitespaceToken(decoder *xml.Decoder) (xml.Token, error) { - for { - tok, err := decoder.Token() - if err != nil { - return nil, err - } else if chars, ok := tok.(xml.CharData); ok { - strippedBytes := bytes.TrimSpace(chars) - if len(strippedBytes) != 0 { - return tok, nil - } - } else { - return tok, nil - } - } -} - func (or *Response) readXMLHeaders(decoder *xml.Decoder) error { var tok xml.Token tok, err := nextNonWhitespaceToken(decoder) diff --git a/signup.go b/signup.go index f356010..f38054a 100644 --- a/signup.go +++ b/signup.go @@ -61,12 +61,6 @@ func (bai *BankAcctInfo) String() string { return fmt.Sprintf("%+v", *bai) } -type CCAcct struct { - XMLName xml.Name // CCACCTTO or CCACCTFROM - AcctId String `xml:"ACCTID"` - AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA -} - type CCAcctInfo struct { XMLName xml.Name `xml:"CCACCTINFO"` CCAcctFrom CCAcct `xml:"CCACCTFROM"` diff --git a/util.go b/util.go new file mode 100644 index 0000000..5aa73aa --- /dev/null +++ b/util.go @@ -0,0 +1,25 @@ +package ofxgo + +import ( + "bytes" + "github.com/golang/go/src/encoding/xml" +) + +// Returns the next available Token from the xml.Decoder that is not CharData +// made up entirely of whitespace. This is useful to skip whitespace when +// manually unmarshaling XML. +func nextNonWhitespaceToken(decoder *xml.Decoder) (xml.Token, error) { + for { + tok, err := decoder.Token() + if err != nil { + return nil, err + } else if chars, ok := tok.(xml.CharData); ok { + strippedBytes := bytes.TrimSpace(chars) + if len(strippedBytes) != 0 { + return tok, nil + } + } else { + return tok, nil + } + } +}