1
0
mirror of https://github.com/aclindsa/ofxgo.git synced 2024-12-26 23:02:29 -05:00

Initial commit

This commit is contained in:
Aaron Lindsay 2017-03-11 07:15:15 -05:00
commit 99cd8f7273
6 changed files with 928 additions and 0 deletions

23
acctinfo.go Normal file
View File

@ -0,0 +1,23 @@
package ofxgo
import (
"github.com/golang/go/src/encoding/xml"
)
type OfxAcctInfoRequest struct {
XMLName xml.Name `xml:"ACCTINFOTRNRQ"`
TrnUID OfxUID `xml:"TRNUID"`
CltCookie OfxInt `xml:"CLTCOOKIE"`
DtAcctup OfxDate `xml:"ACCTINFORQ>DTACCTUP"`
}
func (r *OfxAcctInfoRequest) Name() string {
return "ACCTINFOTRNRQ"
}
func (r *OfxAcctInfoRequest) Valid() (bool, error) {
if ok, err := r.TrnUID.Valid(); !ok {
return false, err
}
return true, nil
}

236
leaf_elements.go Normal file
View File

@ -0,0 +1,236 @@
package ofxgo
// A list of all the leaf elements in OFX 1.0.3 (the last SGML version of the
// spec). These are all the elements that are possibly left unclosed, and which
// can have no children of their own. Fortunately these two sets of elements
// are the same. We use this list when parsing to remove ambiguities about
// element nesting.
//
// Generated using the following command with the 1.0.3 SPEC .dtd file:
// # sed -rn 's/^<!ELEMENT\s+([A-Z]+)\s+-\s+o\s+%.*TYPE>.*$/\t"\1",/p' *.dtd | sort
var ofxLeafElements = []string{
"ACCESSKEY",
"ACCTID",
"ACCTKEY",
"ACCTREQUIRED",
"ACCTTYPE",
"ADJAMT",
"ADJDATE",
"ADJDESC",
"ADJNO",
"APPID",
"APPVER",
"AUTHTOKEN",
"AUTHTOKENFIRST",
"AUTHTOKENINFOURL",
"AUTHTOKENLABEL",
"AVAILACCTS",
"BALAMT",
"BALCLOSE",
"BALMIN",
"BALOPEN",
"BALTYPE",
"BANKID",
"BILLREFINFO",
"BRANCHID",
"CANADDPAYEE",
"CANBILLPAY",
"CANCELWND",
"CANEMAIL",
"CANMODMDLS",
"CANMODPMTS",
"CANMODXFERS",
"CANNOTIFY",
"CANPENDING",
"CANRECUR",
"CANSCHED",
"CANUSEDESC",
"CANUSERANGE",
"CASESEN",
"CHARTYPE",
"CHECKNUM",
"CHGPINFIRST",
"CHGUSERINFO",
"CHKANDDEB",
"CHKERROR",
"CHKNUMEND",
"CHKNUMSTART",
"CHKSTATUS",
"CITY",
"CLIENTACTREQ",
"CLIENTROUTING",
"CLIENTUID",
"CLIENTUIDREQ",
"CLOSINGAVAIL",
"CLTCOOKIE",
"CODE",
"CONFMSG",
"CORRECTACTION",
"CORRECTFITID",
"COUNTRY",
"CREDITLIMIT",
"CSPHONE",
"CURDEF",
"CURRATE",
"CURSYM",
"DATEBIRTH",
"DAYPHONE",
"DAYSTOPAY",
"DAYSWITH",
"DEBADJ",
"DEPANDCREDIT",
"DESC",
"DFLTDAYSTOPAY",
"DIFFFIRSTPMT",
"DIFFLASTPMT",
"DOMXFERFEE",
"DSCAMT",
"DSCDATE",
"DSCDESC",
"DSCRATE",
"DTACCTUP",
"DTASOF",
"DTAVAIL",
"DTCHANGED",
"DTCLIENT",
"DTCLOSE",
"DTCREATED",
"DTDUE",
"DTEND",
"DTEXPIRE",
"DTINFOCHG",
"DTNEXT",
"DTOPEN",
"DTPMTDUE",
"DTPMTPRC",
"DTPOSTED",
"DTPOSTEND",
"DTPOSTSTART",
"DTPROFUP",
"DTPURCHASE",
"DTSERVER",
"DTSTART",
"DTUSER",
"DTXFERPRC",
"DTXFERPRJ",
"EMAIL",
"EVEPHONE",
"EXTDPMTCHK",
"EXTDPMTFOR",
"FAXPHONE",
"FEE",
"FEEMSG",
"FICERTID",
"FID",
"FINALAMT",
"FINAME",
"FINCHG",
"FIRSTNAME",
"FITID",
"FREQ",
"FROM",
"GENUSERKEY",
"GETMIMESUP",
"HASEXTDPMT",
"IDSCOPE",
"INCIMAGES",
"INITIALAMT",
"INTLXFERFEE",
"INVALIDACCTTYPE",
"INVDATE",
"INVDESC",
"INVNO",
"INVPAIDAMT",
"INVTOTALAMT",
"LANGUAGE",
"LASTNAME",
"LITMAMT",
"LITMDESC",
"LOSTSYNC",
"MAILSUP",
"MAX",
"MEMO",
"MESSAGE",
"MFACHALLENGEFIRST",
"MFACHALLENGESUPT",
"MFAPHRASEA",
"MFAPHRASEID",
"MFAPHRASELABEL",
"MIDDLENAME",
"MIN",
"MINPMTDUE",
"MKTGINFO",
"MODELWND",
"MODPENDING",
"NAME",
"NEWUSERPASS",
"NINSTS",
"NONCE",
"OFXSEC",
"ORG",
"PAYACCT",
"PAYANDCREDIT",
"PAYEEID",
"PAYEELSTID",
"PAYINSTRUCT",
"PHONE",
"PINCH",
"PMTBYADDR",
"PMTBYPAYEEID",
"PMTBYXFER",
"PMTPRCCODE",
"POSTALCODE",
"POSTPROCWND",
"PROCDAYSOFF",
"PROCENDTM",
"PURANDADV",
"RECSRVRTID",
"REFNUM",
"REFRESH",
"REFRESHSUPT",
"REJECTIFMISSING",
"RESPFILEER",
"SECURITYNAME",
"SESSCOOKIE",
"SEVERITY",
"SIC",
"SIGNONREALM",
"SPACES",
"SPECIAL",
"SPNAME",
"SRVRTID",
"STATE",
"STPCHKFEE",
"STSVIAMODS",
"SUBJECT",
"SUPTXDL",
"SVC",
"SVCSTATUS",
"SYNCMODE",
"TAN",
"TAXID",
"TEMPPASS",
"TO",
"TOKEN",
"TOKENONLY",
"TOTALFEES",
"TOTALINT",
"TRANSPSEC",
"TRNAMT",
"TRNTYPE",
"TRNUID",
"TSKEYEXPIRE",
"TSPHONE",
"URL",
"USEHTML",
"USERID",
"USERKEY",
"USERPASS",
"VALUE",
"VER",
"XFERDAYSWITH",
"XFERDEST",
"XFERDFLTDAYSTOPAY",
"XFERPRCCODE",
"XFERSRC",
}

378
ofx.go Normal file
View File

@ -0,0 +1,378 @@
package ofxgo
import (
"bufio"
"bytes"
"errors"
"fmt"
"github.com/golang/go/src/encoding/xml"
"io"
"net/http"
"strings"
"time"
)
type OfxMessage interface {
Name() string
Valid() (bool, error)
}
type OfxRequest struct {
URL string
Version string // String for OFX header, defaults to 203
Signon OfxSignonRequest //<SIGNONMSGSETV1>
Signup []OfxMessage //<SIGNUPMSGSETV1>
//<BANKMSGSETV1>
//<CREDITCARDMSGSETV1>
//<LOANMSGSETV1>
//<INVSTMTMSGSETV1>
//<INTERXFERMSGSETV1>
//<WIREXFERMSGSETV1>
//<BILLPAYMSGSETV1>
//<EMAILMSGSETV1>
//<SECLISTMSGSETV1>
//<PRESDIRMSGSETV1>
//<PRESDLVMSGSETV1>
Profile []OfxMessage //<PROFMSGSETV1>
//<IMAGEMSGSETV1>
}
func (oq *OfxRequest) marshalMessageSet(e *xml.Encoder, requests []OfxMessage, 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 *OfxRequest) Marshal() (*bytes.Buffer, error) {
var b bytes.Buffer
if len(oq.Version) == 0 {
oq.Version = "203"
}
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(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + "\n")
b.WriteString(`<?OFX OFXHEADER="200" VERSION="` + oq.Version + `" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>` + "\n")
default:
return nil, errors.New(oq.Version + " is not a valid OFX version string")
}
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 := 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 := oq.marshalMessageSet(encoder, oq.Signup, "SIGNUPMSGSRQV1"); err != nil {
return nil, err
}
if err := oq.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
}
func (oq *OfxRequest) Request() (*OfxResponse, error) {
oq.Signon.Dtclient = OfxDate(time.Now())
b, err := oq.Marshal()
if err != nil {
return nil, err
}
response, err := http.Post(oq.URL, "application/x-ofx", b)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != 200 {
return nil, errors.New("OFXQuery request status: " + response.Status)
}
// Help the parser out by giving it a clue about what header format to
// expect
var xmlVersion bool = true
switch oq.Version {
case "102", "103", "151", "160":
xmlVersion = false
}
var ofxresp OfxResponse
if err := ofxresp.Unmarshal(response.Body, xmlVersion); err != nil {
return nil, err
}
return &ofxresp, nil
}
type OfxResponse struct {
Version string // String for OFX header, defaults to 203
Signon OfxSignonResponse //<SIGNONMSGSETV1>
Signup []OfxMessage //<SIGNUPMSGSETV1>
//<BANKMSGSETV1>
//<CREDITCARDMSGSETV1>
//<LOANMSGSETV1>
//<INVSTMTMSGSETV1>
//<INTERXFERMSGSETV1>
//<WIREXFERMSGSETV1>
//<BILLPAYMSGSETV1>
//<EMAILMSGSETV1>
//<SECLISTMSGSETV1>
//<PRESDIRMSGSETV1>
//<PRESDLVMSGSETV1>
Profile []OfxMessage //<PROFMSGSETV1>
//<IMAGEMSGSETV1>
}
func (or *OfxResponse) readSGMLHeaders(r *bufio.Reader) error {
var seenHeader, seenVersion bool = false, false
for {
line, err := r.ReadString('\n')
if err != nil {
return err
}
// r.ReadString leaves the '\n' on the end...
line = strings.TrimSpace(line)
if len(line) == 0 {
if seenHeader {
break
} else {
continue
}
}
header := strings.SplitN(line, ":", 2)
if header == nil || len(header) != 2 {
return errors.New("OFX headers malformed")
}
switch header[0] {
case "OFXHEADER":
if header[1] != "100" {
return errors.New("OFXHEADER is not 100")
}
seenHeader = true
case "DATA":
if header[1] != "OFXSGML" {
return errors.New("OFX DATA header does not contain OFXSGML")
}
case "VERSION":
switch header[1] {
case "102", "103", "151", "160":
seenVersion = true
or.Version = header[1]
default:
return errors.New("Invalid OFX VERSION in header")
}
case "SECURITY":
if header[1] != "NONE" {
return errors.New("OFX SECURITY header not NONE")
}
case "COMPRESSION":
if header[1] != "NONE" {
return errors.New("OFX COMPRESSION header not NONE")
}
case "ENCODING", "CHARSET", "OLDFILEUID", "NEWFILEUID":
// TODO check/handle these headers?
default:
return errors.New("Invalid OFX header: " + header[0])
}
}
if !seenVersion {
return errors.New("OFX VERSION header missing")
}
return nil
}
func (or *OfxResponse) readXMLHeaders(decoder *xml.Decoder) error {
tok, err := decoder.Token()
if err != nil {
return err
} else if xmlElem, ok := tok.(xml.ProcInst); !ok || xmlElem.Target != "xml" {
return errors.New("Missing xml processing instruction")
}
// parse the OFX header
tok, err = decoder.Token()
if err != nil {
return err
} else if ofxElem, ok := tok.(xml.ProcInst); ok && ofxElem.Target == "OFX" {
var seenHeader, seenVersion bool = false, false
headers := bytes.TrimSpace(ofxElem.Inst)
for len(headers) > 0 {
tmp := bytes.SplitN(headers, []byte("=\""), 2)
if len(tmp) != 2 {
return errors.New("Malformed OFX header")
}
header := string(tmp[0])
headers = tmp[1]
tmp = bytes.SplitN(headers, []byte("\""), 2)
if len(tmp) != 2 {
return errors.New("Malformed OFX header")
}
value := string(tmp[0])
headers = bytes.TrimSpace(tmp[1])
switch header {
case "OFXHEADER":
if value != "200" {
return errors.New("OFXHEADER is not 200")
}
seenHeader = true
case "VERSION":
switch value {
case "200", "201", "202", "203", "210", "211", "220":
seenVersion = true
or.Version = value
default:
return errors.New("Invalid OFX VERSION in header")
}
case "SECURITY":
if value != "NONE" {
return errors.New("OFX SECURITY header not NONE")
}
case "OLDFILEUID", "NEWFILEUID":
// TODO check/handle these headers?
default:
return errors.New("Invalid OFX header: " + header)
}
}
if !seenHeader {
return errors.New("OFXHEADER version missing")
}
if !seenVersion {
return errors.New("OFX VERSION header missing")
}
} else {
return errors.New("Missing xml 'OFX' processing instruction")
}
return nil
}
func (or *OfxResponse) Unmarshal(reader io.Reader, xmlVersion bool) error {
r := bufio.NewReader(reader)
// parse SGML headers before creating XML decoder
if !xmlVersion {
if err := or.readSGMLHeaders(r); err != nil {
return err
}
}
decoder := xml.NewDecoder(r)
if !xmlVersion {
decoder.Strict = false
decoder.AutoCloseAfterCharData = ofxLeafElements
}
if xmlVersion {
// parse the xml header
if err := or.readXMLHeaders(decoder); err != nil {
return err
}
}
tok, err := decoder.Token()
if err != nil {
return err
} else if ofxStart, ok := tok.(xml.StartElement); !ok || ofxStart.Name.Local != "OFX" {
return errors.New("Missing opening OFX xml element")
}
// Unmarshal the signon message
tok, err = decoder.Token()
if err != nil {
return err
} else if signonStart, ok := tok.(xml.StartElement); ok && signonStart.Name.Local == "SIGNONMSGSRSV1" {
if err := decoder.Decode(&or.Signon); err != nil {
return err
}
} else {
return errors.New("Missing opening SIGNONMSGSRSV1 xml element")
}
tok, err = decoder.Token()
if err != nil {
return err
} else if signonEnd, ok := tok.(xml.EndElement); !ok || signonEnd.Name.Local != "SIGNONMSGSRSV1" {
return errors.New("Missing closing SIGNONMSGSRSV1 xml element")
}
if ok, err := or.Signon.Valid(); !ok {
return err
}
for {
tok, err = decoder.Token()
if err != nil {
return err
} else if ofxEnd, ok := tok.(xml.EndElement); ok && ofxEnd.Name.Local == "OFX" {
return nil // found closing XML element, so we're done
} else if start, ok := tok.(xml.StartElement); ok {
// TODO decode other types
fmt.Println("Found starting element for: " + start.Name.Local)
} else {
return errors.New("Found unexpected token")
}
decoder.Skip()
}
}

24
profile.go Normal file
View File

@ -0,0 +1,24 @@
package ofxgo
import (
"github.com/golang/go/src/encoding/xml"
)
type OfxProfileRequest struct {
XMLName xml.Name `xml:"PROFTRNRQ"`
TrnUID OfxUID `xml:"TRNUID"`
ClientRouting OfxString `xml:"PROFRQ>CLIENTROUTING"` // Forced to NONE
DtProfup OfxDate `xml:"PROFRQ>DTPROFUP"`
}
func (r *OfxProfileRequest) Name() string {
return "PROFTRNRQ"
}
func (r *OfxProfileRequest) Valid() (bool, error) {
if ok, err := r.TrnUID.Valid(); !ok {
return false, err
}
r.ClientRouting = "NONE"
return true, nil
}

102
signon.go Normal file
View File

@ -0,0 +1,102 @@
package ofxgo
import (
"errors"
"github.com/golang/go/src/encoding/xml"
)
type OfxSignonRequest struct {
XMLName xml.Name `xml:"SONRQ"`
Dtclient OfxDate `xml:"DTCLIENT"` // Overridden in OfxRequest.Request()
UserId OfxString `xml:"USERID"`
UserPass OfxString `xml:"USERPASS,omitempty"`
UserKey OfxString `xml:"USERKEY,omitempty"`
Language OfxString `xml:"LANGUAGE"` // Defaults to ENG
Org OfxString `xml:"FI>ORG"`
Fid OfxString `xml:"FI>FID"`
AppId OfxString `xml:"APPID"` // Defaults to OFXGO
AppVer OfxString `xml:"APPVER"` // Defaults to 0001
ClientUID OfxUID `xml:"CLIENTUID,omitempty"`
}
func (r *OfxSignonRequest) Name() string {
return "SONRQ"
}
func (r *OfxSignonRequest) Valid() (bool, error) {
if len(r.UserId) < 1 || len(r.UserId) > 32 {
return false, errors.New("SONRQ>USERID invalid length")
}
if (len(r.UserPass) == 0) == (len(r.UserKey) == 0) {
return false, errors.New("One and only one of SONRQ>USERPASS and USERKEY must be supplied")
}
if len(r.UserPass) > 32 {
return false, errors.New("SONRQ>USERPASS invalid length")
}
if len(r.UserKey) > 64 {
return false, errors.New("SONRQ>USERKEY invalid length")
}
if len(r.Language) == 0 {
r.Language = "ENG"
} else if len(r.Language) != 3 {
return false, errors.New("SONRQ>LANGUAGE invalid length")
}
if len(r.AppId) == 0 {
r.AppId = "OFXGO"
} else if len(r.AppId) > 5 {
return false, errors.New("SONRQ>APPID invalid length")
}
if len(r.AppVer) == 0 {
r.AppVer = "0001"
} else if len(r.AppVer) > 4 {
return false, errors.New("SONRQ>APPVER invalid length")
}
if ok, err := r.ClientUID.Valid(); !ok {
if len(r.ClientUID) > 0 { // ClientUID isn't required
return false, err
}
}
return true, nil
}
type OfxStatus struct {
XMLName xml.Name `xml:"STATUS"`
Code OfxInt `xml:"CODE"`
Severity OfxString `xml:"SEVERITY"`
Message OfxString `xml:"MESSAGE,omitempty"`
}
func (s *OfxStatus) Valid() (bool, error) {
switch s.Severity {
case "INFO", "WARN", "ERROR":
return true, nil
default:
return false, errors.New("Invalid STATUS>SEVERITY")
}
}
type OfxSignonResponse struct {
XMLName xml.Name `xml:"SONRS"`
Status OfxStatus `xml:"STATUS"`
Dtserver OfxDate `xml:"DTSERVER"`
UserKey OfxString `xml:"USERKEY,omitempty"`
TsKeyExpire OfxDate `xml:"TSKEYEXPIRE,omitempty"`
Language OfxString `xml:"LANGUAGE"`
Dtprofup OfxDate `xml:"DTPROFUP,omitempty"`
Dtacctup OfxDate `xml:"DTACCTUP,omitempty"`
Org OfxString `xml:"FI>ORG"`
Fid OfxString `xml:"FI>FID"`
SessCookie OfxString `xml:"SESSCOOKIE,omitempty"`
AccessKey OfxString `xml:"ACCESSKEY,omitempty"`
}
func (r *OfxSignonResponse) Name() string {
return "SONRS"
}
func (r *OfxSignonResponse) Valid() (bool, error) {
if len(r.Language) != 3 {
return false, errors.New("SONRS>LANGUAGE invalid length: " + string(r.Language))
}
return r.Status.Valid()
}

165
types.go Normal file
View File

@ -0,0 +1,165 @@
package ofxgo
import (
"crypto/rand"
"errors"
"fmt"
"github.com/golang/go/src/encoding/xml"
"regexp"
"strconv"
"strings"
"time"
)
type OfxInt int64
func (oi *OfxInt) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string
err := d.DecodeElement(&value, &start)
if err != nil {
return err
}
i, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
if err != nil {
return err
}
*oi = (OfxInt)(i)
return nil
}
var ofxDateFormats = []string{
"20060102150405.000",
"20060102150405",
"200601021504",
"2006010215",
"20060102",
}
var ofxDateZoneFormat = "20060102150405.000 -0700"
var ofxDateZoneRegex = regexp.MustCompile(`^\[([+-]?[0-9]+)(\.([0-9]{2}))?(:([A-Z]+))?\]$`)
type OfxDate time.Time
func (od *OfxDate) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string
err := d.DecodeElement(&value, &start)
if err != nil {
return err
}
value = strings.TrimSpace(value)
if len(value) > len(ofxDateFormats[0]) {
matches := ofxDateZoneRegex.FindStringSubmatch(value[len(ofxDateFormats[0]):])
if matches == nil {
return errors.New("Invalid OFX Date Format")
}
var err error
var zonehours, zoneminutes int
zonehours, err = strconv.Atoi(matches[1])
if err != nil {
return err
}
if len(matches[3]) > 0 {
zoneminutes, err = strconv.Atoi(matches[1])
if err != nil {
return err
}
zoneminutes = zoneminutes * 60 / 100
}
value = value[:len(ofxDateFormats[0])] + " " + fmt.Sprintf("%+d%02d", zonehours, zoneminutes)
t, err := time.Parse(ofxDateZoneFormat, value)
if err == nil {
tmpod := OfxDate(t)
*od = tmpod
return nil
}
}
for _, format := range ofxDateFormats {
t, err := time.Parse(format, value)
if err == nil {
tmpod := OfxDate(t)
*od = tmpod
return nil
}
}
return errors.New("OFX: Couldn't parse date:" + value)
}
func (od *OfxDate) String() string {
t := time.Time(*od)
format := t.Format(ofxDateFormats[0])
zonename, zoneoffset := t.Zone()
format += "[" + fmt.Sprintf("%+d", zoneoffset/3600)
fractionaloffset := (zoneoffset % 3600) / 360
if fractionaloffset > 0 {
format += "." + fmt.Sprintf("%02d", fractionaloffset)
} else if fractionaloffset < 0 {
format += "." + fmt.Sprintf("%02d", -fractionaloffset)
}
return format + ":" + zonename + "]"
}
func (od *OfxDate) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(od.String(), start)
}
type OfxString string
func (os *OfxString) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string
err := d.DecodeElement(&value, &start)
if err != nil {
return err
}
*os = OfxString(strings.TrimSpace(value))
return nil
}
type OfxBoolean bool
func (ob *OfxBoolean) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string
err := d.DecodeElement(&value, &start)
if err != nil {
return err
}
tmpob := strings.TrimSpace(value)
switch tmpob {
case "Y":
*ob = OfxBoolean(true)
case "N":
*ob = OfxBoolean(false)
default:
return errors.New("Invalid OFX Boolean")
}
return nil
}
func (ob *OfxBoolean) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if *ob {
return e.EncodeElement("Y", start)
}
return e.EncodeElement("N", start)
}
type OfxUID string
func (ou *OfxUID) Valid() (bool, error) {
if len(*ou) != 36 {
return false, errors.New("UID not 36 characters long")
}
return true, nil
}
func RandomUID() (*OfxUID, error) {
uidbytes := make([]byte, 16)
n, err := rand.Read(uidbytes[:])
if err != nil {
return nil, err
}
if n != 16 {
return nil, errors.New("RandomUID failed to read 16 random bytes")
}
uid := OfxUID(fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", uidbytes[:4], uidbytes[4:6], uidbytes[6:8], uidbytes[8:10], uidbytes[10:]))
return &uid, nil
}