mirror of
https://github.com/aclindsa/ofxgo.git
synced 2024-11-22 11:30:05 -05:00
Add Client, split from Request
This commit is contained in:
parent
aeba01bf0f
commit
49cf745a20
181
ofx.go
181
ofx.go
@ -11,6 +11,95 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// Don't insert newlines or indentation when marshalling to SGML/XML
|
||||||
|
NoIndent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultClient Client
|
||||||
|
|
||||||
|
func (c *Client) OfxVersion() string {
|
||||||
|
if len(c.SpecVersion) > 0 {
|
||||||
|
return c.SpecVersion
|
||||||
|
} else {
|
||||||
|
return "203"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Id() String {
|
||||||
|
if len(c.AppId) > 0 {
|
||||||
|
return String(c.AppId)
|
||||||
|
} else {
|
||||||
|
return String("OFXGO")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Version() String {
|
||||||
|
if len(c.AppVer) > 0 {
|
||||||
|
return String(c.AppVer)
|
||||||
|
} else {
|
||||||
|
return String("0001")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) IndentRequests() bool {
|
||||||
|
return !c.NoIndent
|
||||||
|
}
|
||||||
|
|
||||||
|
func RawRequest(URL string, r io.Reader) (*http.Response, error) {
|
||||||
|
response, err := http.Post(URL, "application/x-ofx", r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode != 200 {
|
||||||
|
return nil, errors.New("OFXQuery request status: " + response.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request marshals a Request object into XML, makes an HTTP request against
|
||||||
|
// it's URL, and then unmarshals the response into a Reaponse object.
|
||||||
|
//
|
||||||
|
// Before being marshaled, some of the the Request object's values are
|
||||||
|
// overwritten, namely those dictated by the Client's configuration (Version,
|
||||||
|
// AppId, AppVer fields), and the client's curren time (DtClient). These are
|
||||||
|
// updated in place in the supplied Request object so they may later be
|
||||||
|
// inspected by the caller.
|
||||||
|
func (c *Client) Request(r *Request) (*Response, error) {
|
||||||
|
r.Signon.DtClient = Date(time.Now())
|
||||||
|
|
||||||
|
// Overwrite fields that the client controls
|
||||||
|
r.Version = c.OfxVersion()
|
||||||
|
r.Signon.AppId = c.Id()
|
||||||
|
r.Signon.AppVer = c.Version()
|
||||||
|
r.indent = c.IndentRequests()
|
||||||
|
|
||||||
|
b, err := r.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := RawRequest(r.URL, b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
ofxresp, err := ParseResponse(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ofxresp, nil
|
||||||
|
}
|
||||||
|
|
||||||
type Message interface {
|
type Message interface {
|
||||||
Name() string
|
Name() string
|
||||||
Valid() (bool, error)
|
Valid() (bool, error)
|
||||||
@ -34,6 +123,8 @@ type Request struct {
|
|||||||
//<PRESDLVMSGSETV1>
|
//<PRESDLVMSGSETV1>
|
||||||
Profile []Message //<PROFMSGSETV1>
|
Profile []Message //<PROFMSGSETV1>
|
||||||
//<IMAGEMSGSETV1>
|
//<IMAGEMSGSETV1>
|
||||||
|
|
||||||
|
indent bool // Whether to indent the marshaled XML
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oq *Request) marshalMessageSet(e *xml.Encoder, requests []Message, setname string) error {
|
func (oq *Request) marshalMessageSet(e *xml.Encoder, requests []Message, setname string) error {
|
||||||
@ -62,10 +153,7 @@ func (oq *Request) marshalMessageSet(e *xml.Encoder, requests []Message, setname
|
|||||||
func (oq *Request) Marshal() (*bytes.Buffer, error) {
|
func (oq *Request) Marshal() (*bytes.Buffer, error) {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
|
|
||||||
if len(oq.Version) == 0 {
|
// Write the header appropriate to our version
|
||||||
oq.Version = "203"
|
|
||||||
}
|
|
||||||
|
|
||||||
switch oq.Version {
|
switch oq.Version {
|
||||||
case "102", "103", "151", "160":
|
case "102", "103", "151", "160":
|
||||||
b.WriteString(`OFXHEADER:100
|
b.WriteString(`OFXHEADER:100
|
||||||
@ -87,7 +175,9 @@ NEWFILEUID:NONE
|
|||||||
}
|
}
|
||||||
|
|
||||||
encoder := xml.NewEncoder(&b)
|
encoder := xml.NewEncoder(&b)
|
||||||
encoder.Indent("", " ")
|
if oq.indent {
|
||||||
|
encoder.Indent("", " ")
|
||||||
|
}
|
||||||
|
|
||||||
ofxElement := xml.StartElement{Name: xml.Name{Local: "OFX"}}
|
ofxElement := xml.StartElement{Name: xml.Name{Local: "OFX"}}
|
||||||
|
|
||||||
@ -129,40 +219,6 @@ NEWFILEUID:NONE
|
|||||||
return &b, nil
|
return &b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oq *Request) Request() (*http.Response, error) {
|
|
||||||
oq.Signon.DtClient = Date(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
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.StatusCode != 200 {
|
|
||||||
return nil, errors.New("OFXQuery request status: " + response.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (oq *Request) RequestAndParse() (*Response, error) {
|
|
||||||
response, err := oq.Request()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
var ofxresp Response
|
|
||||||
if err := ofxresp.Unmarshal(response.Body); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ofxresp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Response struct {
|
type Response struct {
|
||||||
Version string // String for OFX header, defaults to 203
|
Version string // String for OFX header, defaults to 203
|
||||||
Signon SignonResponse //<SIGNONMSGSETV1>
|
Signon SignonResponse //<SIGNONMSGSETV1>
|
||||||
@ -348,17 +404,26 @@ func guessVersion(r *bufio.Reader) (bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (or *Response) Unmarshal(reader io.Reader) error {
|
// ParseResponse parses an OFX response in SGML or XML into a Response object
|
||||||
|
// from the given io.Reader
|
||||||
|
//
|
||||||
|
// It is commonly used as part of Client.Request(), but may be used on its own
|
||||||
|
// to parse already-downloaded OFX files (such as those from 'Web Connect'). It
|
||||||
|
// performs version autodetection if it can and attempts to be as forgiving as
|
||||||
|
// possible about the input format.
|
||||||
|
func ParseResponse(reader io.Reader) (*Response, error) {
|
||||||
|
var or Response
|
||||||
|
|
||||||
r := bufio.NewReaderSize(reader, guessVersionCheckBytes)
|
r := bufio.NewReaderSize(reader, guessVersionCheckBytes)
|
||||||
xmlVersion, err := guessVersion(r)
|
xmlVersion, err := guessVersion(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse SGML headers before creating XML decoder
|
// parse SGML headers before creating XML decoder
|
||||||
if !xmlVersion {
|
if !xmlVersion {
|
||||||
if err := or.readSGMLHeaders(r); err != nil {
|
if err := or.readSGMLHeaders(r); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,58 +439,58 @@ func (or *Response) Unmarshal(reader io.Reader) error {
|
|||||||
if xmlVersion {
|
if xmlVersion {
|
||||||
// parse the xml header
|
// parse the xml header
|
||||||
if err := or.readXMLHeaders(decoder); err != nil {
|
if err := or.readXMLHeaders(decoder); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tok, err := nextNonWhitespaceToken(decoder)
|
tok, err := nextNonWhitespaceToken(decoder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
} else if ofxStart, ok := tok.(xml.StartElement); !ok || ofxStart.Name.Local != "OFX" {
|
} else if ofxStart, ok := tok.(xml.StartElement); !ok || ofxStart.Name.Local != "OFX" {
|
||||||
return errors.New("Missing opening OFX xml element")
|
return nil, errors.New("Missing opening OFX xml element")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal the signon message
|
// Unmarshal the signon message
|
||||||
tok, err = nextNonWhitespaceToken(decoder)
|
tok, err = nextNonWhitespaceToken(decoder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
} else if signonStart, ok := tok.(xml.StartElement); ok && signonStart.Name.Local == "SIGNONMSGSRSV1" {
|
} else if signonStart, ok := tok.(xml.StartElement); ok && signonStart.Name.Local == "SIGNONMSGSRSV1" {
|
||||||
if err := decoder.Decode(&or.Signon); err != nil {
|
if err := decoder.Decode(&or.Signon); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return errors.New("Missing opening SIGNONMSGSRSV1 xml element")
|
return nil, errors.New("Missing opening SIGNONMSGSRSV1 xml element")
|
||||||
}
|
}
|
||||||
|
|
||||||
tok, err = nextNonWhitespaceToken(decoder)
|
tok, err = nextNonWhitespaceToken(decoder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
} else if signonEnd, ok := tok.(xml.EndElement); !ok || signonEnd.Name.Local != "SIGNONMSGSRSV1" {
|
} else if signonEnd, ok := tok.(xml.EndElement); !ok || signonEnd.Name.Local != "SIGNONMSGSRSV1" {
|
||||||
return errors.New("Missing closing SIGNONMSGSRSV1 xml element")
|
return nil, errors.New("Missing closing SIGNONMSGSRSV1 xml element")
|
||||||
}
|
}
|
||||||
if ok, err := or.Signon.Valid(); !ok {
|
if ok, err := or.Signon.Valid(); !ok {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
tok, err = nextNonWhitespaceToken(decoder)
|
tok, err = nextNonWhitespaceToken(decoder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
} else if ofxEnd, ok := tok.(xml.EndElement); ok && ofxEnd.Name.Local == "OFX" {
|
} else if ofxEnd, ok := tok.(xml.EndElement); ok && ofxEnd.Name.Local == "OFX" {
|
||||||
return nil // found closing XML element, so we're done
|
return &or, 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
|
||||||
switch start.Name.Local {
|
switch start.Name.Local {
|
||||||
case "SIGNUPMSGSRSV1":
|
case "SIGNUPMSGSRSV1":
|
||||||
msgs, err := DecodeSignupMessageSet(decoder, start)
|
msgs, err := DecodeSignupMessageSet(decoder, start)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
or.Signup = msgs
|
or.Signup = msgs
|
||||||
case "BANKMSGSRSV1":
|
case "BANKMSGSRSV1":
|
||||||
msgs, err := DecodeBankingMessageSet(decoder, start)
|
msgs, err := DecodeBankingMessageSet(decoder, start)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
or.Banking = msgs
|
or.Banking = msgs
|
||||||
//case "CREDITCARDMSGSRSV1":
|
//case "CREDITCARDMSGSRSV1":
|
||||||
@ -441,15 +506,15 @@ func (or *Response) Unmarshal(reader io.Reader) error {
|
|||||||
case "PROFMSGSRSV1":
|
case "PROFMSGSRSV1":
|
||||||
msgs, err := DecodeProfileMessageSet(decoder, start)
|
msgs, err := DecodeProfileMessageSet(decoder, start)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
or.Profile = msgs
|
or.Profile = msgs
|
||||||
//case "IMAGEMSGSRSV1":
|
//case "IMAGEMSGSRSV1":
|
||||||
default:
|
default:
|
||||||
return errors.New("Unsupported message set: " + start.Name.Local)
|
return nil, errors.New("Unsupported message set: " + start.Name.Local)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return errors.New("Found unexpected token")
|
return nil, errors.New("Found unexpected token")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
14
signon.go
14
signon.go
@ -7,15 +7,15 @@ import (
|
|||||||
|
|
||||||
type SignonRequest struct {
|
type SignonRequest struct {
|
||||||
XMLName xml.Name `xml:"SONRQ"`
|
XMLName xml.Name `xml:"SONRQ"`
|
||||||
DtClient Date `xml:"DTCLIENT"` // Overridden in Request.Request()
|
DtClient Date `xml:"DTCLIENT"` // Overwritten in Client.Request()
|
||||||
UserId String `xml:"USERID"`
|
UserId String `xml:"USERID"`
|
||||||
UserPass String `xml:"USERPASS,omitempty"`
|
UserPass String `xml:"USERPASS,omitempty"`
|
||||||
UserKey String `xml:"USERKEY,omitempty"`
|
UserKey String `xml:"USERKEY,omitempty"`
|
||||||
Language String `xml:"LANGUAGE"` // Defaults to ENG
|
Language String `xml:"LANGUAGE"` // Defaults to ENG
|
||||||
Org String `xml:"FI>ORG"`
|
Org String `xml:"FI>ORG"`
|
||||||
Fid String `xml:"FI>FID"`
|
Fid String `xml:"FI>FID"`
|
||||||
AppId String `xml:"APPID"` // Defaults to OFXGO
|
AppId String `xml:"APPID"` // Overwritten in Client.Request()
|
||||||
AppVer String `xml:"APPVER"` // Defaults to 0001
|
AppVer String `xml:"APPVER"` // Overwritten in Client.Request()
|
||||||
ClientUID UID `xml:"CLIENTUID,omitempty"`
|
ClientUID UID `xml:"CLIENTUID,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,14 +41,10 @@ func (r *SignonRequest) Valid() (bool, error) {
|
|||||||
} else if len(r.Language) != 3 {
|
} else if len(r.Language) != 3 {
|
||||||
return false, errors.New("SONRQ>LANGUAGE invalid length")
|
return false, errors.New("SONRQ>LANGUAGE invalid length")
|
||||||
}
|
}
|
||||||
if len(r.AppId) == 0 {
|
if len(r.AppId) < 1 || len(r.AppId) > 5 {
|
||||||
r.AppId = "OFXGO"
|
|
||||||
} else if len(r.AppId) > 5 {
|
|
||||||
return false, errors.New("SONRQ>APPID invalid length")
|
return false, errors.New("SONRQ>APPID invalid length")
|
||||||
}
|
}
|
||||||
if len(r.AppVer) == 0 {
|
if len(r.AppVer) < 1 || len(r.AppVer) > 4 {
|
||||||
r.AppVer = "0001"
|
|
||||||
} else if len(r.AppVer) > 4 {
|
|
||||||
return false, errors.New("SONRQ>APPVER invalid length")
|
return false, errors.New("SONRQ>APPVER invalid length")
|
||||||
}
|
}
|
||||||
if ok, err := r.ClientUID.Valid(); !ok {
|
if ok, err := r.ClientUID.Valid(); !ok {
|
||||||
|
Loading…
Reference in New Issue
Block a user