mirror of
https://github.com/aclindsa/ofxgo.git
synced 2024-11-22 11:30:05 -05:00
Add parsing of profile messages, fix date parsing
Profile messages are still missing validation
This commit is contained in:
parent
689337d81d
commit
74b0ff7816
32
ofx.go
32
ofx.go
@ -4,7 +4,6 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"github.com/golang/go/src/encoding/xml"
|
"github.com/golang/go/src/encoding/xml"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -368,11 +367,36 @@ func (or *Response) Unmarshal(reader io.Reader, xmlVersion bool) error {
|
|||||||
return nil // found closing XML element, so we're done
|
return nil // found closing XML element, so we're done
|
||||||
} else if start, ok := tok.(xml.StartElement); ok {
|
} else if start, ok := tok.(xml.StartElement); ok {
|
||||||
// TODO decode other types
|
// TODO decode other types
|
||||||
fmt.Println("Found starting element for: " + start.Name.Local)
|
switch start.Name.Local {
|
||||||
|
// case "SIGNUPMSGSRSV1":
|
||||||
|
// msgs, err := DecodeSignupMessageSet(decoder, start)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// or.Signup = msgs
|
||||||
|
//case "BANKMSGSRSV1":
|
||||||
|
//case "CREDITCARDMSGSRSV1":
|
||||||
|
//case "LOANMSGSRSV1":
|
||||||
|
//case "INVSTMTMSGSRSV1":
|
||||||
|
//case "INTERXFERMSGSRSV1":
|
||||||
|
//case "WIREXFERMSGSRSV1":
|
||||||
|
//case "BILLPAYMSGSRSV1":
|
||||||
|
//case "EMAILMSGSRSV1":
|
||||||
|
//case "SECLISTMSGSRSV1":
|
||||||
|
//case "PRESDIRMSGSRSV1":
|
||||||
|
//case "PRESDLVMSGSRSV1":
|
||||||
|
case "PROFMSGSRSV1":
|
||||||
|
msgs, err := DecodeProfileMessageSet(decoder, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
or.Profile = msgs
|
||||||
|
//case "IMAGEMSGSRSV1":
|
||||||
|
default:
|
||||||
|
return errors.New("Unsupported message set: " + start.Name.Local)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return errors.New("Found unexpected token")
|
return errors.New("Found unexpected token")
|
||||||
}
|
}
|
||||||
|
|
||||||
decoder.Skip()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
129
profile.go
129
profile.go
@ -1,6 +1,7 @@
|
|||||||
package ofxgo
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/golang/go/src/encoding/xml"
|
"github.com/golang/go/src/encoding/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,3 +23,131 @@ func (r *ProfileRequest) Valid() (bool, error) {
|
|||||||
r.ClientRouting = "NONE"
|
r.ClientRouting = "NONE"
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SignonInfo struct {
|
||||||
|
XMLName xml.Name `xml:"SIGNONINFO"`
|
||||||
|
SignonRealm String `xml:"SIGNONREALM"`
|
||||||
|
Min Int `xml:"MIN"` // Minimum number of password characters
|
||||||
|
Max Int `xml:"MAX"` // Maximum number of password characters
|
||||||
|
Chartype String `xml:"CHARTYPE"` // ALPHAONLY, NUMERICONLY, ALPHAORNUMERIC, ALPHAANDNUMERIC
|
||||||
|
CaseSen Boolean `xml:"CASESEN"` // Password is case-sensitive?
|
||||||
|
Special Boolean `xml:"SPECIAL"` // Special characters allowed?
|
||||||
|
Spaces Boolean `xml:"SPACES"` // Spaces allowed?
|
||||||
|
Pinch Boolean `xml:"PINCH"` // Pin change <PINCHRQ> requests allowed
|
||||||
|
ChgPinFirst Boolean `xml:"CHGPINFIRST"` // Server requires user to change password at first signon
|
||||||
|
UserCred1Label String `xml:"USERCRED1LABEL,omitempty"` // Prompt for USERCRED1 (if this field is present, USERCRED1 is required)
|
||||||
|
UserCred2Label String `xml:"USERCRED2LABEL,omitempty"` // Prompt for USERCRED2 (if this field is present, USERCRED2 is required)
|
||||||
|
ClientUIDReq Boolean `xml:"CLIENTUIDREQ,omitempty"` // CLIENTUID required?
|
||||||
|
AuthTokenFirst Boolean `xml:"AUTHTOKENFIRST,omitempty"` // Server requires AUTHTOKEN as part of first signon
|
||||||
|
AuthTokenLabel String `xml:"AUTHTOKENLABEL,omitempty"`
|
||||||
|
AuthTokenInfoURL String `xml:"AUTHTOKENINFOURL,omitempty"`
|
||||||
|
MFAChallengeSupt Boolean `xml:"MFACHALLENGESUPT,omitempty"` // Server supports MFACHALLENGE
|
||||||
|
MFAChallengeFIRST Boolean `xml:"MFACHALLENGEFIRST,omitempty"` // Server requires MFACHALLENGE to be sent with first signon
|
||||||
|
AccessTokenReq Boolean `xml:"ACCESSTOKENREQ,omitempty"` // Server requires ACCESSTOKEN to be sent with all requests except profile
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageSet struct {
|
||||||
|
XMLName xml.Name // <xxxMSGSETVn>
|
||||||
|
Ver String `xml:"MSGSETCORE>VER"`
|
||||||
|
Url String `xml:"MSGSETCORE>URL"`
|
||||||
|
OfxSec String `xml:"MSGSETCORE>OFXSEC"`
|
||||||
|
TranspSec Boolean `xml:"MSGSETCORE>TRANSPSEC"`
|
||||||
|
SignonRealm String `xml:"MSGSETCORE>SIGNONREALM"` // Used to identify which SignonInfo to use for to this MessageSet
|
||||||
|
Language []String `xml:"MSGSETCORE>LANGUAGE"`
|
||||||
|
SyncMode String `xml:"MSGSETCORE>SYNCMODE"`
|
||||||
|
// TODO MessageSet-specific stuff?
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageSetList []MessageSet
|
||||||
|
|
||||||
|
func (msl *MessageSetList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
for {
|
||||||
|
var msgset MessageSet
|
||||||
|
tok, err := d.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local {
|
||||||
|
// If we found the end of our starting element, we're done parsing
|
||||||
|
return nil
|
||||||
|
} else if _, ok := tok.(xml.StartElement); ok {
|
||||||
|
// Found starting tag for <xxxMSGSET>. Get the next one (xxxMSGSETVn) and decode that struct
|
||||||
|
tok, err := d.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if versionStart, ok := tok.(xml.StartElement); ok {
|
||||||
|
if err := d.DecodeElement(&msgset, &versionStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errors.New("Invalid MSGSETLIST formatting")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eat ending tags for <xxxMSGSET>
|
||||||
|
tok, err = d.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if _, ok := tok.(xml.EndElement); !ok {
|
||||||
|
return errors.New("Invalid MSGSETLIST formatting")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errors.New("MSGSETLIST didn't find an opening xxxMSGSETVn element")
|
||||||
|
}
|
||||||
|
*msl = MessageSetList(append(*(*[]MessageSet)(msl), msgset))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfileResponse struct {
|
||||||
|
XMLName xml.Name `xml:"PROFTRNRS"`
|
||||||
|
TrnUID UID `xml:"TRNUID"`
|
||||||
|
MessageSetList MessageSetList `xml:"PROFRS>MSGSETLIST"`
|
||||||
|
SignonInfoList []SignonInfo `xml:"PROFRS>SIGNONINFOLIST>SIGNONINFO"`
|
||||||
|
DtProfup Date `xml:"PROFRS>DTPROFUP"`
|
||||||
|
Finame String `xml:"PROFRS>FINAME"`
|
||||||
|
Addr1 String `xml:"PROFRS>ADDR1"`
|
||||||
|
Addr2 String `xml:"PROFRS>ADDR2,omitempty"`
|
||||||
|
Addr3 String `xml:"PROFRS>ADDR3,omitempty"`
|
||||||
|
City String `xml:"PROFRS>CITY"`
|
||||||
|
State String `xml:"PROFRS>STATE"`
|
||||||
|
PostalCode String `xml:"PROFRS>POSTALCODE"`
|
||||||
|
Country String `xml:"PROFRS>COUNTRY"`
|
||||||
|
CsPhone String `xml:"PROFRS>CSPHONE,omitempty"`
|
||||||
|
TsPhone String `xml:"PROFRS>TSPHONE,omitempty"`
|
||||||
|
FaxPhone String `xml:"PROFRS>FAXPHONE,omitempty"`
|
||||||
|
URL String `xml:"PROFRS>URL,omitempty"`
|
||||||
|
Email String `xml:"PROFRS>EMAIL,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pr ProfileResponse) Name() string {
|
||||||
|
return "PROFTRNRS"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pr ProfileResponse) Valid() (bool, error) {
|
||||||
|
//TODO implement
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeProfileMessageSet(d *xml.Decoder, start xml.StartElement) ([]Message, error) {
|
||||||
|
var msgs []Message
|
||||||
|
for {
|
||||||
|
tok, err := d.Token()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local {
|
||||||
|
// If we found the end of our starting element, we're done parsing
|
||||||
|
return msgs, nil
|
||||||
|
} else if startElement, ok := tok.(xml.StartElement); ok {
|
||||||
|
switch startElement.Name.Local {
|
||||||
|
case "PROFTRNRS":
|
||||||
|
var prof ProfileResponse
|
||||||
|
if err := d.DecodeElement(&prof, &startElement); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
msgs = append(msgs, Message(prof))
|
||||||
|
default:
|
||||||
|
return nil, errors.New("Unsupported profile response tag: " + startElement.Name.Local)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("Didn't find an opening element")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
36
types.go
36
types.go
@ -27,6 +27,8 @@ func (oi *Int) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Date time.Time
|
||||||
|
|
||||||
var ofxDateFormats = []string{
|
var ofxDateFormats = []string{
|
||||||
"20060102150405.000",
|
"20060102150405.000",
|
||||||
"20060102150405",
|
"20060102150405",
|
||||||
@ -34,23 +36,25 @@ var ofxDateFormats = []string{
|
|||||||
"2006010215",
|
"2006010215",
|
||||||
"20060102",
|
"20060102",
|
||||||
}
|
}
|
||||||
var ofxDateZoneFormat = "20060102150405.000 -0700"
|
var ofxDateZoneRegex = regexp.MustCompile(`^([+-]?[0-9]+)(\.([0-9]{2}))?(:([A-Z]+))?$`)
|
||||||
var ofxDateZoneRegex = regexp.MustCompile(`^\[([+-]?[0-9]+)(\.([0-9]{2}))?(:([A-Z]+))?\]$`)
|
|
||||||
|
|
||||||
type Date time.Time
|
|
||||||
|
|
||||||
func (od *Date) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
func (od *Date) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
var value string
|
var value, zone, zoneFormat string
|
||||||
err := d.DecodeElement(&value, &start)
|
err := d.DecodeElement(&value, &start)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
value = strings.TrimSpace(value)
|
|
||||||
|
|
||||||
if len(value) > len(ofxDateFormats[0]) {
|
// Split the time zone off, if any
|
||||||
matches := ofxDateZoneRegex.FindStringSubmatch(value[len(ofxDateFormats[0]):])
|
split := strings.SplitN(value, "[", 2)
|
||||||
|
if len(split) == 2 {
|
||||||
|
value = split[0]
|
||||||
|
zoneFormat = " -0700"
|
||||||
|
zone = strings.TrimRight(split[1], "]")
|
||||||
|
|
||||||
|
matches := ofxDateZoneRegex.FindStringSubmatch(zone)
|
||||||
if matches == nil {
|
if matches == nil {
|
||||||
return errors.New("Invalid OFX Date Format")
|
return errors.New("Invalid OFX Date timezone format: " + zone)
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
var zonehours, zoneminutes int
|
var zonehours, zoneminutes int
|
||||||
@ -65,17 +69,11 @@ func (od *Date) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|||||||
}
|
}
|
||||||
zoneminutes = zoneminutes * 60 / 100
|
zoneminutes = zoneminutes * 60 / 100
|
||||||
}
|
}
|
||||||
value = value[:len(ofxDateFormats[0])] + " " + fmt.Sprintf("%+d%02d", zonehours, zoneminutes)
|
zone = fmt.Sprintf(" %+03d%02d", zonehours, zoneminutes)
|
||||||
t, err := time.Parse(ofxDateZoneFormat, value)
|
|
||||||
if err == nil {
|
|
||||||
tmpod := Date(t)
|
|
||||||
*od = tmpod
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, format := range ofxDateFormats {
|
for _, format := range ofxDateFormats {
|
||||||
t, err := time.Parse(format, value)
|
t, err := time.Parse(format+zoneFormat, value+zone)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
tmpod := Date(t)
|
tmpod := Date(t)
|
||||||
*od = tmpod
|
*od = tmpod
|
||||||
@ -85,8 +83,8 @@ func (od *Date) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|||||||
return errors.New("OFX: Couldn't parse date:" + value)
|
return errors.New("OFX: Couldn't parse date:" + value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (od *Date) String() string {
|
func (od Date) String() string {
|
||||||
t := time.Time(*od)
|
t := time.Time(od)
|
||||||
format := t.Format(ofxDateFormats[0])
|
format := t.Format(ofxDateFormats[0])
|
||||||
zonename, zoneoffset := t.Zone()
|
zonename, zoneoffset := t.Zone()
|
||||||
format += "[" + fmt.Sprintf("%+d", zoneoffset/3600)
|
format += "[" + fmt.Sprintf("%+d", zoneoffset/3600)
|
||||||
|
Loading…
Reference in New Issue
Block a user