diff --git a/invstmt.go b/invstmt.go index bda7d55..1c91d44 100644 --- a/invstmt.go +++ b/invstmt.go @@ -468,7 +468,8 @@ type InvTranList struct { BankTransactions []InvBankTransaction } -// UnmarshalXML handles unmarshalling an InvTranList element from an XML string +// UnmarshalXML handles unmarshalling an InvTranList element from an SGML/XML +// string func (l *InvTranList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { for { tok, err := nextNonWhitespaceToken(d) diff --git a/profile.go b/profile.go index 0afaac2..03ff37e 100644 --- a/profile.go +++ b/profile.go @@ -5,6 +5,9 @@ import ( "github.com/aclindsa/go/src/encoding/xml" ) +// ProfileRequest represents a request for a server to provide a profile of its +// capabilities (which message sets and versions it supports, how to access +// them, which languages and which types of synchronization they support, etc.) type ProfileRequest struct { XMLName xml.Name `xml:"PROFTRNRQ"` TrnUID UID `xml:"TRNUID"` @@ -12,26 +15,35 @@ type ProfileRequest struct { TAN String `xml:"TAN,omitempty"` // Transaction authorization number // TODO `xml:"OFXEXTENSION,omitempty"` ClientRouting String `xml:"PROFRQ>CLIENTROUTING"` // Forced to NONE - DtProfUp Date `xml:"PROFRQ>DTPROFUP"` + DtProfUp Date `xml:"PROFRQ>DTPROFUP"` // Date and time client last received a profile update } +// Name returns the name of the top-level transaction XML/SGML element func (r *ProfileRequest) Name() string { return "PROFTRNRQ" } +// Valid returns (true, nil) if this struct would be valid OFX if marshalled +// into XML/SGML func (r *ProfileRequest) Valid() (bool, error) { // TODO implement r.ClientRouting = "NONE" return true, nil } +// Type returns which message set this message belongs to (which Request +// element of type []Message it should appended to) func (r *ProfileRequest) Type() messageType { return ProfRq } +// SignonInfo provides the requirements to login to a single signon realm. A +// signon realm consists of all MessageSets which can be accessed using one set +// of login credentials. Most FI's only use one signon realm to make it easier +// and less confusing for the user. type SignonInfo struct { XMLName xml.Name `xml:"SIGNONINFO"` - SignonRealm String `xml:"SIGNONREALM"` + SignonRealm String `xml:"SIGNONREALM"` // The SignonRealm for which this SignonInfo provides information. This SignonInfo is valid for all MessageSets with SignonRealm fields matching this one Min Int `xml:"MIN"` // Minimum number of password characters Max Int `xml:"MAX"` // Maximum number of password characters CharType charType `xml:"CHARTYPE"` // One of ALPHAONLY, NUMERICONLY, ALPHAORNUMERIC, ALPHAANDNUMERIC @@ -51,11 +63,13 @@ type SignonInfo struct { AccessTokenReq Boolean `xml:"ACCESSTOKENREQ,omitempty"` // Server requires ACCESSTOKEN to be sent with all requests except profile } +// MessageSet represents one message set supported by an FI and its +// capabilities type MessageSet struct { XMLName xml.Name // Name string // (copy of XMLName.Local) - Ver Int `xml:"MSGSETCORE>VER"` // Message set version - should always match 'n' in - Url String `xml:"MSGSETCORE>URL"` // URL where messages in this set are to be set + Ver Int `xml:"MSGSETCORE>VER"` // Message set version - should always match 'n' in of Name + URL String `xml:"MSGSETCORE>URL"` // URL where messages in this set are to be set OfxSec ofxSec `xml:"MSGSETCORE>OFXSEC"` // NONE or 'TYPE 1' TranspSec Boolean `xml:"MSGSETCORE>TRANSPSEC"` // Transport-level security must be used SignonRealm String `xml:"MSGSETCORE>SIGNONREALM"` // Used to identify which SignonInfo to use for to this MessageSet @@ -67,8 +81,11 @@ type MessageSet struct { // TODO MessageSet-specific stuff? } +// MessageSetList is a list of MessageSets (necessary because they must be +// manually parsed) type MessageSetList []MessageSet +// UnmarshalXML handles unmarshalling a MessageSetList element from an XML string func (msl *MessageSetList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { for { var msgset MessageSet @@ -106,6 +123,11 @@ func (msl *MessageSetList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) } } +// ProfileResponse contains a requested profile of the server's capabilities +// (which message sets and versions it supports, how to access them, which +// languages and which types of synchronization they support, etc.). Note that +// if the server does not support ClientRouting=NONE (as we always send with +// ProfileRequest), this may be an error) type ProfileResponse struct { XMLName xml.Name `xml:"PROFTRNRS"` TrnUID UID `xml:"TRNUID"` @@ -130,15 +152,19 @@ type ProfileResponse struct { Email String `xml:"PROFRS>EMAIL,omitempty"` } +// Name returns the name of the top-level transaction XML/SGML element func (pr *ProfileResponse) Name() string { return "PROFTRNRS" } +// Valid returns (true, nil) if this struct was valid OFX when unmarshalled func (pr *ProfileResponse) Valid() (bool, error) { //TODO implement return true, nil } +// Type returns which message set this message belongs to (which Response +// element of type []Message it belongs to) func (pr *ProfileResponse) Type() messageType { return ProfRs } diff --git a/profile_test.go b/profile_test.go index 29a3cce..fe433ce 100644 --- a/profile_test.go +++ b/profile_test.go @@ -232,7 +232,7 @@ NEWFILEUID:NONE ofxgo.MessageSet{ Name: "SIGNONMSGSETV1", Ver: 1, - Url: "https://ofx.example.com/cgi-ofx/exampleofx", + URL: "https://ofx.example.com/cgi-ofx/exampleofx", OfxSec: ofxgo.OfxSecNone, TranspSec: true, SignonRealm: "Example Trade", @@ -244,7 +244,7 @@ NEWFILEUID:NONE ofxgo.MessageSet{ Name: "SIGNUPMSGSETV1", Ver: 1, - Url: "https://ofx.example.com/cgi-ofx/exampleofx", + URL: "https://ofx.example.com/cgi-ofx/exampleofx", OfxSec: ofxgo.OfxSecNone, TranspSec: true, SignonRealm: "Example Trade", @@ -256,7 +256,7 @@ NEWFILEUID:NONE ofxgo.MessageSet{ Name: "INVSTMTMSGSETV1", Ver: 1, - Url: "https://ofx.example.com/cgi-ofx/exampleofx", + URL: "https://ofx.example.com/cgi-ofx/exampleofx", OfxSec: ofxgo.OfxSecNone, TranspSec: true, SignonRealm: "Example Trade", @@ -268,7 +268,7 @@ NEWFILEUID:NONE ofxgo.MessageSet{ Name: "SECLISTMSGSETV1", Ver: 1, - Url: "https://ofx.example.com/cgi-ofx/exampleofx", + URL: "https://ofx.example.com/cgi-ofx/exampleofx", OfxSec: ofxgo.OfxSecNone, TranspSec: true, SignonRealm: "Example Trade", @@ -280,7 +280,7 @@ NEWFILEUID:NONE ofxgo.MessageSet{ Name: "PROFMSGSETV1", Ver: 1, - Url: "https://ofx.example.com/cgi-ofx/exampleofx", + URL: "https://ofx.example.com/cgi-ofx/exampleofx", OfxSec: ofxgo.OfxSecNone, TranspSec: true, SignonRealm: "Example Trade", diff --git a/request.go b/request.go index 7331654..416896f 100644 --- a/request.go +++ b/request.go @@ -60,7 +60,8 @@ func marshalMessageSet(e *xml.Encoder, requests []Message, set messageType) erro return nil } -// Overwrite the fields in this Request object controlled by the Client +// SetClientFields overwrites the fields in this Request object controlled by +// the Client func (oq *Request) SetClientFields(c *Client) { oq.Signon.DtClient.Time = time.Now() diff --git a/seclist.go b/seclist.go index 9fb881f..f89c5dd 100644 --- a/seclist.go +++ b/seclist.go @@ -5,12 +5,17 @@ import ( "github.com/aclindsa/go/src/encoding/xml" ) +// SecurityID identifies a security by its CUSIP (for US-based FI's, others may +// use UniqueID types other than CUSIP) type SecurityID struct { XMLName xml.Name `xml:"SECID"` UniqueID String `xml:"UNIQUEID"` // CUSIP for US FI's UniqueIDType String `xml:"UNIQUEIDTYPE"` // Should always be "CUSIP" for US FI's } +// SecurityRequest represents a request for one security. It is specified with +// a SECID aggregate, a ticker symbol, or an FI assigned identifier (but no +// more than one of them at a time) type SecurityRequest struct { XMLName xml.Name `xml:"SECRQ"` // Only one of the next three should be present @@ -19,6 +24,8 @@ type SecurityRequest struct { FiID String `xml:"FIID,omitempty"` } +// SecListRequest represents a request for information (namely price) about one +// or more securities type SecListRequest struct { XMLName xml.Name `xml:"SECLISTTRNRQ"` TrnUID UID `xml:"TRNUID"` @@ -28,19 +35,29 @@ type SecListRequest struct { Securities []SecurityRequest `xml:"SECLISTRQ>SECRQ,omitempty"` } +// Name returns the name of the top-level transaction XML/SGML element func (r *SecListRequest) Name() string { return "SECLISTTRNRQ" } +// Valid returns (true, nil) if this struct would be valid OFX if marshalled +// into XML/SGML func (r *SecListRequest) Valid() (bool, error) { // TODO implement return true, nil } +// Type returns which message set this message belongs to (which Request +// element of type []Message it should appended to) func (r *SecListRequest) Type() messageType { return SecListRq } +// SecListResponse is always empty (except for the transaction UID, status, and +// optional client cookie). Its presence signifies that the SecurityList (a +// different element from this one) immediately after this element in +// Response.SecList was been generated in response to the same SecListRequest +// this is a response to. type SecListResponse struct { XMLName xml.Name `xml:"SECLISTTRNRS"` TrnUID UID `xml:"TRNUID"` @@ -50,23 +67,31 @@ 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 } +// Name returns the name of the top-level transaction XML/SGML element func (r *SecListResponse) Name() string { return "SECLISTTRNRS" } +// Valid returns (true, nil) if this struct was valid OFX when unmarshalled func (r *SecListResponse) Valid() (bool, error) { // TODO implement return true, nil } +// Type returns which message set this message belongs to (which Response +// element of type []Message it belongs to) func (r *SecListResponse) Type() messageType { return SecListRs } +// Security is satisfied by all *Info elements providing information about +// securities for SecurityList type Security interface { SecurityType() string } +// SecInfo represents the generic information about a security. It is included +// in most other *Info elements. type SecInfo struct { XMLName xml.Name `xml:"SECINFO"` SecID SecurityID `xml:"SECID"` @@ -80,6 +105,7 @@ type SecInfo struct { Memo String `xml:"MEMO,omitempty"` } +// DebtInfo provides information about a debt security type DebtInfo struct { XMLName xml.Name `xml:"DEBTINFO"` SecInfo SecInfo `xml:"SECINFO"` @@ -99,22 +125,29 @@ type DebtInfo struct { FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class } +// SecurityType returns a string representation of this security's type func (i DebtInfo) SecurityType() string { return "DEBTINFO" } +// AssetPortion represents the percentage of a mutual fund with the given asset +// classification type AssetPortion struct { XMLName xml.Name `xml:"PORTION"` AssetClass assetClass `xml:"ASSETCLASS"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER Percent Amount `xml:"PERCENT"` // Percentage of the fund that falls under this asset class } +// FiAssetPortion represents the percentage of a mutual fund with the given +// FI-defined asset classification (AssetPortion should be used for all asset +// classifications defined by the assetClass enum) type FiAssetPortion struct { XMLName xml.Name `xml:"FIPORTION"` FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class Percent Amount `xml:"PERCENT"` // Percentage of the fund that falls under this asset class } +// MFInfo provides information about a mutual fund type MFInfo struct { XMLName xml.Name `xml:"MFINFO"` SecInfo SecInfo `xml:"SECINFO"` @@ -125,10 +158,12 @@ type MFInfo struct { FiAssetClasses []FiAssetPortion `xml:"FIMFASSETCLASS>FIPORTION"` } +// SecurityType returns a string representation of this security's type func (i MFInfo) SecurityType() string { return "MFINFO" } +// OptInfo provides information about an option type OptInfo struct { XMLName xml.Name `xml:"OPTINFO"` SecInfo SecInfo `xml:"SECINFO"` @@ -141,10 +176,13 @@ type OptInfo struct { FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class } +// SecurityType returns a string representation of this security's type func (i OptInfo) SecurityType() string { return "OPTINFO" } +// OtherInfo provides information about a security type not covered by the +// other *Info elements type OtherInfo struct { XMLName xml.Name `xml:"OTHERINFO"` SecInfo SecInfo `xml:"SECINFO"` @@ -153,10 +191,12 @@ type OtherInfo struct { FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class } +// SecurityType returns a string representation of this security's type func (i OtherInfo) SecurityType() string { return "OTHERINFO" } +// StockInfo provides information about a security type type StockInfo struct { XMLName xml.Name `xml:"STOCKINFO"` SecInfo SecInfo `xml:"SECINFO"` @@ -167,27 +207,35 @@ type StockInfo struct { FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class } +// SecurityType returns a string representation of this security's type func (i StockInfo) SecurityType() string { return "STOCKINFO" } +// SecurityList is a container for Security objects containaing information +// about securities type SecurityList struct { Securities []Security } +// Name returns the name of the top-level transaction XML/SGML element func (r *SecurityList) Name() string { return "SECLIST" } +// Valid returns (true, nil) if this struct was valid OFX when unmarshalled func (r *SecurityList) Valid() (bool, error) { // TODO implement return true, nil } +// Type returns which message set this message belongs to (which Response +// element of type []Message it belongs to) func (r *SecurityList) Type() messageType { return SecListRs } +// UnmarshalXML handles unmarshalling a SecurityList from an SGML/XML string func (r *SecurityList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { for { tok, err := nextNonWhitespaceToken(d) diff --git a/signon.go b/signon.go index f2849c7..f9e7709 100644 --- a/signon.go +++ b/signon.go @@ -6,6 +6,8 @@ import ( "github.com/aclindsa/go/src/encoding/xml" ) +// SignonRequest identifies and authenticates a user to their FI and is +// provided with every Request type SignonRequest struct { XMLName xml.Name `xml:"SONRQ"` DtClient Date `xml:"DTCLIENT"` // Current time on client, overwritten in Client.Request() @@ -20,10 +22,13 @@ type SignonRequest struct { ClientUID UID `xml:"CLIENTUID,omitempty"` } +// Name returns the name of the top-level transaction XML/SGML element func (r *SignonRequest) Name() string { return "SONRQ" } +// Valid returns (true, nil) if this struct would be valid OFX if marshalled +// into XML/SGML func (r *SignonRequest) Valid() (bool, error) { if len(r.UserID) < 1 || len(r.UserID) > 32 { return false, errors.New("SONRQ>USERID invalid length") @@ -40,7 +45,7 @@ func (r *SignonRequest) Valid() (bool, error) { if len(r.Language) == 0 { r.Language = "ENG" } else if len(r.Language) != 3 { - return false, fmt.Errorf("SONRQ>LANGUAGE invalid length: \"%s\"\n", r.Language) + return false, fmt.Errorf("SONRQ>LANGUAGE invalid length: \"%s\"", r.Language) } if len(r.AppID) < 1 || len(r.AppID) > 5 { return false, errors.New("SONRQ>APPID invalid length") @@ -51,6 +56,8 @@ func (r *SignonRequest) Valid() (bool, error) { return true, nil } +// SignonResponse is provided with every Response and indicates the success or +// failure of the SignonRequest in the corresponding Request type SignonResponse struct { XMLName xml.Name `xml:"SONRS"` Status Status `xml:"STATUS"` @@ -66,13 +73,15 @@ type SignonResponse struct { AccessKey String `xml:"ACCESSKEY,omitempty"` } +// Name returns the name of the top-level transaction XML/SGML element func (r *SignonResponse) Name() string { return "SONRS" } +// Valid returns (true, nil) if this struct was valid OFX when unmarshalled func (r *SignonResponse) Valid() (bool, error) { if len(r.Language) != 3 { - return false, fmt.Errorf("SONRS>LANGUAGE invalid length: \"%s\"\n", r.Language) + return false, fmt.Errorf("SONRS>LANGUAGE invalid length: \"%s\"", r.Language) } return r.Status.Valid() } diff --git a/signup.go b/signup.go index c25dd2c..0de54c0 100644 --- a/signup.go +++ b/signup.go @@ -5,6 +5,8 @@ import ( "github.com/aclindsa/go/src/encoding/xml" ) +// AcctInfoRequest represents a request for the server to provide information +// for all of the user's available accounts at this FI type AcctInfoRequest struct { XMLName xml.Name `xml:"ACCTINFOTRNRQ"` TrnUID UID `xml:"TRNUID"` @@ -14,19 +16,25 @@ type AcctInfoRequest struct { DtAcctUp Date `xml:"ACCTINFORQ>DTACCTUP"` } +// Name returns the name of the top-level transaction XML/SGML element func (r *AcctInfoRequest) Name() string { return "ACCTINFOTRNRQ" } +// Valid returns (true, nil) if this struct would be valid OFX if marshalled +// into XML/SGML func (r *AcctInfoRequest) Valid() (bool, error) { // TODO implement return true, nil } +// Type returns which message set this message belongs to (which Request +// element of type []Message it should appended to) func (r *AcctInfoRequest) Type() messageType { return SignupRq } +// HolderInfo contains the information a FI has about an account-holder type HolderInfo struct { XMLName xml.Name FirstName String `xml:"FIRSTNAME"` @@ -45,6 +53,9 @@ type HolderInfo struct { HolderType holderType `xml:"HOLDERTYPE,omitempty"` // One of INDIVIDUAL, JOINT, CUSTODIAL, TRUST, OTHER } +// BankAcctInfo contains information about a bank account, including how to +// access it (BankAcct), and whether it supports downloading transactions +// (SupTxDl). type BankAcctInfo struct { XMLName xml.Name `xml:"BANKACCTINFO"` BankAcctFrom BankAcct `xml:"BANKACCTFROM"` @@ -59,11 +70,14 @@ type BankAcctInfo struct { SvcStatus svcStatus `xml:"SVCSTATUS"` // One of AVAIL (available, but not yet requested), PEND (requested, but not yet available), ACTIVE } -// Make pointers to these structs print nicely +// String makes pointers to BankAcctInfo structs print nicely func (bai *BankAcctInfo) String() string { return fmt.Sprintf("%+v", *bai) } +// CCAcctInfo contains information about a credit card account, including how +// to access it (CCAcct), and whether it supports downloading transactions +// (SupTxDl). type CCAcctInfo struct { XMLName xml.Name `xml:"CCACCTINFO"` CCAcctFrom CCAcct `xml:"CCACCTFROM"` @@ -74,11 +88,14 @@ type CCAcctInfo struct { SvcStatus svcStatus `xml:"SVCSTATUS"` // One of AVAIL (available, but not yet requested), PEND (requested, but not yet available), ACTIVE } -// Make pointers to these structs print nicely +// String makes pointers to CCAcctInfo structs print nicely func (ci *CCAcctInfo) String() string { return fmt.Sprintf("%+v", *ci) } +// InvAcctInfo contains information about an investment account, including how +// to access it (InvAcct), and whether it supports downloading transactions +// (SupTxDl). type InvAcctInfo struct { XMLName xml.Name `xml:"INVACCTINFO"` InvAcctFrom InvAcct `xml:"INVACCTFROM"` @@ -89,11 +106,14 @@ type InvAcctInfo struct { OptionLevel String `xml:"OPTIONLEVEL,omitempty"` // Text desribing option trading privileges } -// Make pointers to these structs print nicely +// String makes pointers to InvAcctInfo structs print nicely func (iai *InvAcctInfo) String() string { return fmt.Sprintf("%+v", *iai) } +// AcctInfo represents generic account information. It should contain one (and +// only one) *AcctInfo element corresponding to the tyep of account it +// represents. type AcctInfo struct { XMLName xml.Name `xml:"ACCTINFO"` Name String `xml:"NAME,omitempty"` @@ -101,6 +121,7 @@ type AcctInfo struct { Phone String `xml:"PHONE,omitempty"` PrimaryHolder HolderInfo `xml:"HOLDERINFO>PRIMARYHOLDER,omitempty"` SecondaryHolder HolderInfo `xml:"HOLDERINFO>SECONDARYHOLDER,omitempty"` + // Only one of the rest of the fields will be valid for any given AcctInfo BankAcctInfo *BankAcctInfo `xml:"BANKACCTINFO,omitempty"` CCAcctInfo *CCAcctInfo `xml:"CCACCTINFO,omitempty"` @@ -109,6 +130,8 @@ type AcctInfo struct { // TODO BPACCTINFO? } +// AcctInfoResponse contains the information about all a user's accounts +// accessible from this FI type AcctInfoResponse struct { XMLName xml.Name `xml:"ACCTINFOTRNRS"` TrnUID UID `xml:"TRNUID"` @@ -119,15 +142,19 @@ type AcctInfoResponse struct { AcctInfo []AcctInfo `xml:"ACCTINFORS>ACCTINFO,omitempty"` } +// Name returns the name of the top-level transaction XML/SGML element func (air *AcctInfoResponse) Name() string { return "ACCTINFOTRNRS" } +// Valid returns (true, nil) if this struct was valid OFX when unmarshalled func (air *AcctInfoResponse) Valid() (bool, error) { //TODO implement return true, nil } +// Type returns which message set this message belongs to (which Response +// element of type []Message it belongs to) func (air *AcctInfoResponse) Type() messageType { return SignupRs } diff --git a/types.go b/types.go index 0029380..cbb27b0 100644 --- a/types.go +++ b/types.go @@ -12,8 +12,11 @@ import ( "time" ) +// Int provides helper methods to unmarshal int64 values from SGML/XML type Int int64 +// UnmarshalXML handles unmarshalling an Int from an SGML/XML string. Leading +// and trailing whitespace is ignored. func (i *Int) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string @@ -33,14 +36,19 @@ func (i *Int) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { return nil } +// Equal returns true if the two Ints are equal in value func (i Int) Equal(o Int) bool { return i == o } +// Amount represents non-integer values (or at least values for fields that may +// not necessarily be integers) type Amount struct { big.Rat } +// UnmarshalXML handles unmarshalling an Amount from an SGML/XML string. +// Leading and trailing whitespace is ignored. func (a *Amount) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string @@ -61,18 +69,22 @@ func (a *Amount) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { return nil } +// String prints a string representation of an Amount func (a Amount) String() string { return strings.TrimRight(strings.TrimRight(a.FloatString(100), "0"), ".") } +// MarshalXML marshals an Amount to SGML/XML func (a *Amount) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeElement(a.String(), start) } +// Equal returns true if two Amounts are equal in value func (a Amount) Equal(o Amount) bool { return (&a).Cmp(&o.Rat) == 0 } +// Date represents OFX date/time values type Date struct { time.Time } @@ -86,6 +98,10 @@ var ofxDateFormats = []string{ } var ofxDateZoneRegex = regexp.MustCompile(`^([+-]?[0-9]+)(\.([0-9]{2}))?(:([A-Z]+))?$`) +// UnmarshalXML handles unmarshalling a Date from an SGML/XML string. It +// attempts to unmarshal the valid date formats in order of decreasing length +// and defaults to GMT if a time zone is not provided, as per the OFX spec. +// Leading and trailing whitespace is ignored. func (od *Date) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value, zone, zoneFormat string err := d.DecodeElement(&value, &start) @@ -148,6 +164,7 @@ func (od *Date) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { return errors.New("OFX: Couldn't parse date:" + value) } +// String returns a string representation of the Date abiding by the OFX spec func (od Date) String() string { format := od.Format(ofxDateFormats[0]) zonename, zoneoffset := od.Zone() @@ -165,31 +182,39 @@ func (od Date) String() string { if len(zonename) > 0 { return format + ":" + zonename + "]" - } else { - return format + "]" } + return format + "]" } +// MarshalXML marshals a Date to XML func (od *Date) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeElement(od.String(), start) } +// Equal returns true if the two Dates represent the same time (time zones are +// accounted for when comparing, but are not required to match) func (od Date) Equal(o Date) bool { return od.Time.Equal(o.Time) } +// NewDate returns a new Date object with the provided date, time, and timezone func NewDate(year int, month time.Month, day, hour, min, sec, nsec int, loc *time.Location) *Date { return &Date{Time: time.Date(year, month, day, hour, min, sec, nsec, loc)} } var gmt = time.FixedZone("GMT", 0) +// NewDateGMT returns a new Date object with the provided date and time in the +// GMT timezone func NewDateGMT(year int, month time.Month, day, hour, min, sec, nsec int) *Date { return &Date{Time: time.Date(year, month, day, hour, min, sec, nsec, gmt)} } +// String provides helper methods to unmarshal OFX string values from SGML/XML type String string +// UnmarshalXML handles unmarshalling a String from an SGML/XML string. Leading +// and trailing whitespace is ignored. func (os *String) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) @@ -200,16 +225,21 @@ func (os *String) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { return nil } +// String returns the string func (os *String) String() string { return string(*os) } +// Equal returns true if the two Strings are equal in value func (os String) Equal(o String) bool { return os == o } +// Boolean provides helper methods to unmarshal bool values from OFX SGML/XML type Boolean bool +// UnmarshalXML handles unmarshalling a Boolean from an SGML/XML string. +// Leading and trailing whitespace is ignored. func (ob *Boolean) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) @@ -228,6 +258,7 @@ func (ob *Boolean) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { return nil } +// MarshalXML marshals a Boolean to XML func (ob *Boolean) MarshalXML(e *xml.Encoder, start xml.StartElement) error { if *ob { return e.EncodeElement("Y", start) @@ -235,16 +266,21 @@ func (ob *Boolean) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeElement("N", start) } +// String returns a string representation of a Boolean value func (ob *Boolean) String() string { return fmt.Sprintf("%v", *ob) } +// Equal returns true if the two Booleans are the same func (ob Boolean) Equal(o Boolean) bool { return ob == o } +// UID represents an UID according to the OFX spec type UID string +// UnmarshalXML handles unmarshalling an UID from an SGML/XML string. Leading +// and trailing whitespace is ignored. func (ou *UID) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) @@ -255,8 +291,8 @@ func (ou *UID) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { return nil } -// The OFX specification recommends that UIDs follow the standard UUID -// 36-character format +// RecommendedFormat returns true iff this UID meets the OFX specification's +// recommendation that UIDs follow the standard UUID 36-character format func (ou UID) RecommendedFormat() (bool, error) { if len(ou) != 36 { return false, errors.New("UID not 36 characters long") @@ -267,10 +303,12 @@ func (ou UID) RecommendedFormat() (bool, error) { return true, nil } +// Equal returns true if the two UIDs are the same func (ou UID) Equal(o UID) bool { return ou == o } +// RandomUID creates a new randomly-generated UID func RandomUID() (*UID, error) { uidbytes := make([]byte, 16) n, err := rand.Read(uidbytes[:])