diff --git a/bank_test.go b/bank_test.go index 3c30217..2576d77 100644 --- a/bank_test.go +++ b/bank_test.go @@ -45,7 +45,7 @@ func TestMarshalBankStatementRequest(t *testing.T) { var client = ofxgo.Client{ AppID: "OFXGO", AppVer: "0001", - SpecVersion: "203", + SpecVersion: ofxgo.OfxVersion203, } var request ofxgo.Request @@ -119,7 +119,7 @@ NEWFILEUID:NONE var client = ofxgo.Client{ AppID: "OFXGO", AppVer: "0001", - SpecVersion: "103", + SpecVersion: ofxgo.OfxVersion103, } var request ofxgo.Request @@ -213,7 +213,7 @@ func TestUnmarshalBankStatementResponse(t *testing.T) { `) var expected ofxgo.Response - expected.Version = "203" + expected.Version = ofxgo.OfxVersion203 expected.Signon.Status.Code = 0 expected.Signon.Status.Severity = "INFO" expected.Signon.DtServer = *ofxgo.NewDateGMT(2006, 1, 15, 11, 23, 03, 0) diff --git a/client.go b/client.go index eade6c7..32630cb 100644 --- a/client.go +++ b/client.go @@ -15,9 +15,9 @@ import ( type Client struct { // Request fields to overwrite with the client's values. If nonempty, // defaults are used - SpecVersion string // VERSION in header - AppID string // SONRQ>APPID - AppVer string // SONRQ>APPVER + SpecVersion ofxVersion // VERSION in header + AppID string // SONRQ>APPID + AppVer string // SONRQ>APPVER // Don't insert newlines or indentation when marshalling to SGML/XML NoIndent bool @@ -25,14 +25,13 @@ type Client struct { var defaultClient Client -// OfxVersion returns a string representation of the OFX specification version -// this Client will marshal Requests as. Defaults to "203" if the client's -// SpecVersion field is empty. -func (c *Client) OfxVersion() string { - if len(c.SpecVersion) > 0 { +// OfxVersion returns the OFX specification version this Client will marshal +// Requests as. Defaults to "203" if the client's SpecVersion field is empty. +func (c *Client) OfxVersion() ofxVersion { + if c.SpecVersion.Valid() { return c.SpecVersion } - return "203" + return OfxVersion203 } // ID returns this Client's OFX AppID field, defaulting to "OFXGO" if diff --git a/cmd/ofx/detect_settings.go b/cmd/ofx/detect_settings.go index f2cf644..2f16b3e 100644 --- a/cmd/ofx/detect_settings.go +++ b/cmd/ofx/detect_settings.go @@ -121,10 +121,15 @@ func detectSettings() { const anonymous = "anonymous00000000000000000000000" func tryProfile(appID, appVer, version string, noindent bool) bool { + ver, err := ofxgo.NewOfxVersion(version) + if err != nil { + fmt.Println("Error creating new OfxVersion enum:", err) + os.Exit(1) + } var client = ofxgo.Client{ AppID: appID, AppVer: appVer, - SpecVersion: version, + SpecVersion: ver, NoIndent: noindent, } diff --git a/cmd/ofx/util.go b/cmd/ofx/util.go index 7676934..fcf43b2 100644 --- a/cmd/ofx/util.go +++ b/cmd/ofx/util.go @@ -1,14 +1,21 @@ package main import ( + "fmt" "github.com/aclindsa/ofxgo" + "os" ) func newRequest() (*ofxgo.Client, *ofxgo.Request) { + ver, err := ofxgo.NewOfxVersion(ofxVersion) + if err != nil { + fmt.Println("Error creating new OfxVersion enum:", err) + os.Exit(1) + } var client = ofxgo.Client{ AppID: appID, AppVer: appVer, - SpecVersion: ofxVersion, + SpecVersion: ver, NoIndent: noIndentRequests, } diff --git a/constants.go b/constants.go index c6e6cb9..ae6915f 100644 --- a/constants.go +++ b/constants.go @@ -13,6 +13,80 @@ import ( "strings" ) +type ofxVersion uint + +// OfxVersion* constants represent the OFX specification version in use +const ( + OfxVersion102 ofxVersion = 1 + iota + OfxVersion103 + OfxVersion151 + OfxVersion160 + OfxVersion200 + OfxVersion201 + OfxVersion202 + OfxVersion203 + OfxVersion210 + OfxVersion211 + OfxVersion220 +) + +var ofxVersions = [...]string{"102", "103", "151", "160", "200", "201", "202", "203", "210", "211", "220"} + +func (e ofxVersion) Valid() bool { + // This check is mostly out of paranoia, ensuring e != 0 should be + // sufficient + return e >= OfxVersion102 && e <= OfxVersion220 +} + +func (e ofxVersion) String() string { + if e.Valid() { + return ofxVersions[e-1] + } + return fmt.Sprintf("invalid ofxVersion (%d)", e) +} + +func (e *ofxVersion) FromString(in string) error { + value := strings.TrimSpace(in) + + for i, s := range ofxVersions { + if s == value { + *e = ofxVersion(i + 1) + return nil + } + } + *e = 0 + return errors.New("Invalid OfxVersion: \"" + in + "\"") +} + +func (e *ofxVersion) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var value string + err := d.DecodeElement(&value, &start) + if err != nil { + return err + } + + return e.FromString(value) +} + +func (e *ofxVersion) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { + if !e.Valid() { + return nil + } + enc.EncodeElement(ofxVersions[*e-1], start) + return nil +} + +// NewOfxVersion returns returns an 'enum' value of type ofxVersion given its +// string representation +func NewOfxVersion(s string) (ofxVersion, error) { + var e ofxVersion + err := e.FromString(s) + if err != nil { + return 0, err + } + return e, nil +} + type acctType uint // AcctType* constants represent types of bank accounts diff --git a/constants_test.go b/constants_test.go index a8d7795..71f962a 100644 --- a/constants_test.go +++ b/constants_test.go @@ -13,6 +13,51 @@ import ( "testing" ) +func TestOfxVersion(t *testing.T) { + e, err := ofxgo.NewOfxVersion("102") + if err != nil { + t.Fatalf("Unexpected error creating new OfxVersion from string \"102\"\n") + } + if !e.Valid() { + t.Fatalf("OfxVersion unexpectedly invalid\n") + } + err = e.FromString("220") + if err != nil { + t.Fatalf("Unexpected error on OfxVersion.FromString(\"220\")\n") + } + if e.String() != "220" { + t.Fatalf("OfxVersion.String() expected to be \"220\"\n") + } + + marshalHelper(t, "220", &e) + + overwritten, err := ofxgo.NewOfxVersion("THISWILLNEVERBEAVALIDENUMSTRING") + if err == nil { + t.Fatalf("Expected error creating new OfxVersion from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") + } + if overwritten.Valid() { + t.Fatalf("OfxVersion created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") + } + if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { + t.Fatalf("OfxVersion created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") + } + + b, err := xml.Marshal(&overwritten) + if err != nil { + t.Fatalf("Unexpected error on xml.Marshal(OfxVersion): %s\n", err) + } + if string(b) != "" { + t.Fatalf("Expected empty string, got '%s'\n", string(b)) + } + + unmarshalHelper(t, "220", &e, &overwritten) + + err = xml.Unmarshal([]byte(""), &overwritten) + if err == nil { + t.Fatalf("Expected error unmarshalling garbage value\n") + } +} + func TestAcctType(t *testing.T) { e, err := ofxgo.NewAcctType("CHECKING") if err != nil { diff --git a/creditcard_test.go b/creditcard_test.go index e7263b0..26eadc8 100644 --- a/creditcard_test.go +++ b/creditcard_test.go @@ -44,7 +44,7 @@ func TestMarshalCCStatementRequest(t *testing.T) { var client = ofxgo.Client{ AppID: "OFXGO", AppVer: "0001", - SpecVersion: "203", + SpecVersion: ofxgo.OfxVersion203, } var request ofxgo.Request @@ -86,7 +86,7 @@ NEWFILEUID:NONE EDT := time.FixedZone("EDT", -4*60*60) EST := time.FixedZone("EST", -5*60*60) - expected.Version = "102" + expected.Version = ofxgo.OfxVersion102 expected.Signon.Status.Code = 0 expected.Signon.Status.Severity = "INFO" expected.Signon.Status.Message = "SUCCESS" diff --git a/generate_constants.py b/generate_constants.py index 6b16a8c..9cfe314 100755 --- a/generate_constants.py +++ b/generate_constants.py @@ -1,6 +1,9 @@ #!/usr/bin/env python enums = { + # OFX spec version + "OfxVersion": (["102", "103", "151", "160", "200", "201", "202", "203", "210", "211", "220"], "the OFX specification version in use"), + # Bank/general "AcctType": (["Checking", "Savings", "MoneyMrkt", "CreditLine", "CD"], "types of bank accounts"), "TrnType": (["Credit", "Debit", "Int", "Div", "Fee", "SrvChg", "Dep", "ATM", "POS", "Xfer", "Check", "Payment", "Cash", "DirectDep", "DirectDebit", "RepeatPmt", "Hold", "Other"], "types of transactions. INT, ATM, and POS depend on the signage of the account."), diff --git a/invstmt_test.go b/invstmt_test.go index c562fc6..6ff44bf 100644 --- a/invstmt_test.go +++ b/invstmt_test.go @@ -52,7 +52,7 @@ func TestMarshalInvStatementRequest(t *testing.T) { var client = ofxgo.Client{ AppID: "MYAPP", AppVer: "1234", - SpecVersion: "203", + SpecVersion: ofxgo.OfxVersion203, } var request ofxgo.Request @@ -322,7 +322,7 @@ func TestUnmarshalInvStatementResponse(t *testing.T) { `) var expected ofxgo.Response - expected.Version = "203" + expected.Version = ofxgo.OfxVersion203 expected.Signon.Status.Code = 0 expected.Signon.Status.Severity = "INFO" expected.Signon.DtServer = *ofxgo.NewDateGMT(2017, 4, 1, 20, 12, 44, 0) @@ -765,7 +765,7 @@ NEWFILEUID: NONE `) var expected ofxgo.Response - expected.Version = "102" + expected.Version = ofxgo.OfxVersion102 expected.Signon.Status.Code = 0 expected.Signon.Status.Severity = "INFO" expected.Signon.DtServer = *ofxgo.NewDateGMT(2017, 4, 3, 12, 0, 0, 0) diff --git a/profile_test.go b/profile_test.go index fe433ce..ea2d149 100644 --- a/profile_test.go +++ b/profile_test.go @@ -39,7 +39,7 @@ func TestMarshalProfileRequest(t *testing.T) { var client = ofxgo.Client{ AppID: "OFXGO", AppVer: "0001", - SpecVersion: "203", + SpecVersion: ofxgo.OfxVersion203, } var request ofxgo.Request @@ -215,7 +215,7 @@ NEWFILEUID:NONE `) var expected ofxgo.Response - expected.Version = "102" + expected.Version = ofxgo.OfxVersion102 expected.Signon.Status.Code = 0 expected.Signon.Status.Severity = "INFO" expected.Signon.DtServer = *ofxgo.NewDateGMT(2017, 4, 3, 9, 34, 58, 0) diff --git a/request.go b/request.go index 416896f..ffbdcd7 100644 --- a/request.go +++ b/request.go @@ -3,6 +3,7 @@ package ofxgo import ( "bytes" "errors" + "fmt" "github.com/aclindsa/go/src/encoding/xml" "time" ) @@ -14,7 +15,7 @@ import ( // error will be returned when Marshal() is called on this Request. type Request struct { URL string - Version string // OFX version string, overwritten in Client.Request() + Version ofxVersion // OFX version, overwritten in Client.Request() Signon SignonRequest // Signup []Message // Bank []Message // @@ -80,10 +81,10 @@ func (oq *Request) Marshal() (*bytes.Buffer, error) { // Write the header appropriate to our version switch oq.Version { - case "102", "103", "151", "160": + case OfxVersion102, OfxVersion103, OfxVersion151, OfxVersion160: b.WriteString(`OFXHEADER:100 DATA:OFXSGML -VERSION:` + oq.Version + ` +VERSION:` + oq.Version.String() + ` SECURITY:NONE ENCODING:USASCII CHARSET:1252 @@ -92,11 +93,11 @@ OLDFILEUID:NONE NEWFILEUID:NONE `) - case "200", "201", "202", "203", "210", "211", "220": + case OfxVersion200, OfxVersion201, OfxVersion202, OfxVersion203, OfxVersion210, OfxVersion211, OfxVersion220: b.WriteString(`` + "\n") - b.WriteString(`` + "\n") + b.WriteString(`` + "\n") default: - return nil, errors.New(oq.Version + " is not a valid OFX version string") + return nil, fmt.Errorf("%d is not a valid OFX version string", oq.Version) } encoder := xml.NewEncoder(&b) diff --git a/response.go b/response.go index 203675b..d2afa86 100644 --- a/response.go +++ b/response.go @@ -14,7 +14,7 @@ import ( // It can be inspected by using type assertions or switches on the message set // you're interested in. type Response struct { - Version string // String for OFX header, defaults to 203 + Version ofxVersion // OFX header version Signon SignonResponse // Signup []Message // Bank []Message // @@ -68,12 +68,14 @@ func (or *Response) readSGMLHeaders(r *bufio.Reader) error { return errors.New("OFX DATA header does not contain OFXSGML") } case "VERSION": - switch headervalue { - case "102", "103", "151", "160": - seenVersion = true - or.Version = headervalue - default: - return errors.New("Invalid OFX VERSION in header") + err := or.Version.FromString(headervalue) + if err != nil { + return err + } + seenVersion = true + + if or.Version > OfxVersion160 { + return errors.New("OFX VERSION > 160 in SGML header") } case "SECURITY": if headervalue != "NONE" { @@ -134,12 +136,14 @@ func (or *Response) readXMLHeaders(decoder *xml.Decoder) error { } 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") + err := or.Version.FromString(value) + if err != nil { + return err + } + seenVersion = true + + if or.Version < OfxVersion200 { + return errors.New("OFX VERSION < 200 in XML header") } case "SECURITY": if value != "NONE" { diff --git a/signon_test.go b/signon_test.go index ebc3934..527db42 100644 --- a/signon_test.go +++ b/signon_test.go @@ -9,7 +9,7 @@ func TestMarshalInvalidSignons(t *testing.T) { var client = ofxgo.Client{ AppID: "OFXGO", AppVer: "0001", - SpecVersion: "203", + SpecVersion: ofxgo.OfxVersion203, } var request ofxgo.Request diff --git a/signup_test.go b/signup_test.go index 06d8943..2b75d84 100644 --- a/signup_test.go +++ b/signup_test.go @@ -40,7 +40,7 @@ func TestMarshalAcctInfoRequest(t *testing.T) { var client = ofxgo.Client{ AppID: "OFXGO", AppVer: "0001", - SpecVersion: "203", + SpecVersion: ofxgo.OfxVersion203, } var request ofxgo.Request @@ -112,7 +112,7 @@ func TestUnmarshalAcctInfoResponse(t *testing.T) { `) var expected ofxgo.Response - expected.Version = "203" + expected.Version = ofxgo.OfxVersion203 expected.Signon.Status.Code = 0 expected.Signon.Status.Severity = "INFO" expected.Signon.DtServer = *ofxgo.NewDateGMT(2006, 1, 15, 11, 23, 03, 0)