mirror of
https://github.com/aclindsa/ofxgo.git
synced 2024-11-22 03:30:04 -05:00
Make Client an interface instead of a struct
This makes it easier to maintain per-institution hacks that start interacting with each other if you try to do them all in the same client code. This commit also breaks out the existing Vanguard hack into its own Client implementation.
This commit is contained in:
parent
eb35a26986
commit
d8491bed1d
80
basic_client.go
Normal file
80
basic_client.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package ofxgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BasicClient provides a standard Client implementation suitable for most
|
||||||
|
// financial institutions. BasicClient uses default, non-zero settings, even if
|
||||||
|
// its fields are not initialized.
|
||||||
|
type BasicClient struct {
|
||||||
|
// Request fields to overwrite with the client's values. If nonempty,
|
||||||
|
// defaults are used
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// OfxVersion returns the OFX specification version this BasicClient will marshal
|
||||||
|
// Requests as. Defaults to "203" if the client's SpecVersion field is empty.
|
||||||
|
func (c *BasicClient) OfxVersion() ofxVersion {
|
||||||
|
if c.SpecVersion.Valid() {
|
||||||
|
return c.SpecVersion
|
||||||
|
}
|
||||||
|
return OfxVersion203
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns this BasicClient's OFX AppID field, defaulting to "OFXGO" if
|
||||||
|
// unspecified.
|
||||||
|
func (c *BasicClient) ID() String {
|
||||||
|
if len(c.AppID) > 0 {
|
||||||
|
return String(c.AppID)
|
||||||
|
}
|
||||||
|
return String("OFXGO")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returns this BasicClient's version number as a string, defaulting to
|
||||||
|
// "0001" if unspecified.
|
||||||
|
func (c *BasicClient) Version() String {
|
||||||
|
if len(c.AppVer) > 0 {
|
||||||
|
return String(c.AppVer)
|
||||||
|
}
|
||||||
|
return String("0001")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndentRequests returns true if the marshaled XML should be indented (and
|
||||||
|
// contain newlines, since the two are linked in the current implementation)
|
||||||
|
func (c *BasicClient) IndentRequests() bool {
|
||||||
|
return !c.NoIndent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BasicClient) RawRequest(URL string, r io.Reader) (*http.Response, error) {
|
||||||
|
if !strings.HasPrefix(URL, "https://") {
|
||||||
|
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BasicClient) RequestNoParse(r *Request) (*http.Response, error) {
|
||||||
|
return clientRequestNoParse(c, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BasicClient) Request(r *Request) (*Response, error) {
|
||||||
|
return clientRequest(c, r)
|
||||||
|
}
|
180
client.go
180
client.go
@ -1,7 +1,6 @@
|
|||||||
package ofxgo
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@ -18,122 +17,65 @@ type Client interface {
|
|||||||
Version() String
|
Version() String
|
||||||
IndentRequests() bool
|
IndentRequests() bool
|
||||||
|
|
||||||
// Used to initiate requests to servers
|
// Request marshals a Request object into XML, makes an HTTP request
|
||||||
|
// against it's URL, and then unmarshals the response into a Response
|
||||||
|
// object.
|
||||||
|
//
|
||||||
|
// Before being marshaled, some of the the Request object's values are
|
||||||
|
// overwritten, namely those dictated by the BasicClient's configuration
|
||||||
|
// (Version, AppID, AppVer fields), and the client's current time
|
||||||
|
// (DtClient). These are updated in place in the supplied Request object so
|
||||||
|
// they may later be inspected by the caller.
|
||||||
Request(r *Request) (*Response, error)
|
Request(r *Request) (*Response, error)
|
||||||
|
|
||||||
|
// RequestNoParse marshals a Request object into XML, makes an HTTP
|
||||||
|
// request, and returns the raw HTTP response. Unlike RawRequest(), it
|
||||||
|
// takes client settings into account. Unlike Request(), it doesn't parse
|
||||||
|
// the response into an ofxgo.Request object.
|
||||||
|
//
|
||||||
|
// Caveat: The caller is responsible for closing the http Response.Body
|
||||||
|
// (see the http module's documentation for more information)
|
||||||
RequestNoParse(r *Request) (*http.Response, error)
|
RequestNoParse(r *Request) (*http.Response, error)
|
||||||
}
|
|
||||||
|
|
||||||
// BasicClient provides a standard Client implementation suitable for most
|
|
||||||
// financial institutions. BasicClient uses default, non-zero settings, even if
|
|
||||||
// its fields are not initialized.
|
|
||||||
type BasicClient struct {
|
|
||||||
// Request fields to overwrite with the client's values. If nonempty,
|
|
||||||
// defaults are used
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// OfxVersion returns the OFX specification version this BasicClient will marshal
|
|
||||||
// Requests as. Defaults to "203" if the client's SpecVersion field is empty.
|
|
||||||
func (c *BasicClient) OfxVersion() ofxVersion {
|
|
||||||
if c.SpecVersion.Valid() {
|
|
||||||
return c.SpecVersion
|
|
||||||
}
|
|
||||||
return OfxVersion203
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID returns this BasicClient's OFX AppID field, defaulting to "OFXGO" if
|
|
||||||
// unspecified.
|
|
||||||
func (c *BasicClient) ID() String {
|
|
||||||
if len(c.AppID) > 0 {
|
|
||||||
return String(c.AppID)
|
|
||||||
}
|
|
||||||
return String("OFXGO")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Version returns this BasicClient's version number as a string, defaulting to
|
|
||||||
// "0001" if unspecified.
|
|
||||||
func (c *BasicClient) Version() String {
|
|
||||||
if len(c.AppVer) > 0 {
|
|
||||||
return String(c.AppVer)
|
|
||||||
}
|
|
||||||
return String("0001")
|
|
||||||
}
|
|
||||||
|
|
||||||
// IndentRequests returns true if the marshaled XML should be indented (and
|
|
||||||
// contain newlines, since the two are linked in the current implementation)
|
|
||||||
func (c *BasicClient) IndentRequests() bool {
|
|
||||||
return !c.NoIndent
|
|
||||||
}
|
|
||||||
|
|
||||||
// RawRequest is little more than a thin wrapper around http.Post
|
// RawRequest is little more than a thin wrapper around http.Post
|
||||||
//
|
//
|
||||||
// In most cases, you should probably be using Request() instead, but
|
// In most cases, you should probably be using Request() instead, but
|
||||||
// RawRequest can be useful if you need to read the raw unparsed http response
|
// RawRequest can be useful if you need to read the raw unparsed http
|
||||||
// yourself (perhaps for downloading an OFX file for use by an external
|
// response yourself (perhaps for downloading an OFX file for use by an
|
||||||
// program, or debugging server behavior), or have a handcrafted request you'd
|
// external program, or debugging server behavior), or have a handcrafted
|
||||||
// like to try.
|
// request you'd like to try.
|
||||||
//
|
//
|
||||||
// Caveats: RawRequest does *not* take client settings into account as
|
// Caveats: RawRequest does *not* take client settings into account as
|
||||||
// Client.Request() does, so your particular server may or may not like
|
// Client.Request() does, so your particular server may or may not like
|
||||||
// whatever we read from 'r'. The caller is responsible for closing the http
|
// whatever we read from 'r'. The caller is responsible for closing the
|
||||||
// Response.Body (see the http module's documentation for more information)
|
// http Response.Body (see the http module's documentation for more
|
||||||
func RawRequest(URL string, r io.Reader) (*http.Response, error) {
|
// information)
|
||||||
if !strings.HasPrefix(URL, "https://") {
|
RawRequest(URL string, r io.Reader) (*http.Response, error)
|
||||||
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := http.Post(URL, "application/x-ofx", r)
|
type clientCreationFunc func(*BasicClient) Client
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
// GetClient returns a new Client for a given URL. It attempts to find a
|
||||||
|
// specialized client for this URL, but simply returns the passed-in
|
||||||
|
// BasicClient if no such match is found.
|
||||||
|
func GetClient(URL string, bc *BasicClient) Client {
|
||||||
|
clients := []struct {
|
||||||
|
URL string
|
||||||
|
Func clientCreationFunc
|
||||||
|
}{
|
||||||
|
{"https://vesnc.vanguard.com/us/OfxDirectConnectServlet", NewVanguardClient},
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
if client.URL == strings.Trim(URL, "/") {
|
||||||
|
return client.Func(bc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bc
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.StatusCode != 200 {
|
// clientRequestNoParse can be used for building clients' RequestNoParse
|
||||||
return nil, errors.New("OFXQuery request status: " + response.Status)
|
// methods if they require fairly standard behavior
|
||||||
}
|
func clientRequestNoParse(c Client, r *Request) (*http.Response, error) {
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// rawRequestCookies is RawRequest with the added feature of sending cookies
|
|
||||||
func rawRequestCookies(URL string, r io.Reader, cookies []*http.Cookie) (*http.Response, error) {
|
|
||||||
if !strings.HasPrefix(URL, "https://") {
|
|
||||||
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
|
||||||
}
|
|
||||||
|
|
||||||
request, err := http.NewRequest("POST", URL, r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
request.Header.Set("Content-Type", "application/x-ofx")
|
|
||||||
for _, cookie := range cookies {
|
|
||||||
request.AddCookie(cookie)
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := http.DefaultClient.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.StatusCode != 200 {
|
|
||||||
return nil, errors.New("OFXQuery request status: " + response.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestNoParse marshals a Request object into XML, makes an HTTP request,
|
|
||||||
// and returns the raw HTTP response. Unlike RawRequest(), it takes client
|
|
||||||
// settings into account. Unlike Request(), it doesn't parse the response into
|
|
||||||
// an ofxgo.Request object.
|
|
||||||
//
|
|
||||||
// Caveat: The caller is responsible for closing the http Response.Body (see
|
|
||||||
// the http module's documentation for more information)
|
|
||||||
func (c *BasicClient) RequestNoParse(r *Request) (*http.Response, error) {
|
|
||||||
r.SetClientFields(c)
|
r.SetClientFields(c)
|
||||||
|
|
||||||
b, err := r.Marshal()
|
b, err := r.Marshal()
|
||||||
@ -141,34 +83,12 @@ func (c *BasicClient) RequestNoParse(r *Request) (*http.Response, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := RawRequest(r.URL, b)
|
return c.RawRequest(r.URL, b)
|
||||||
|
|
||||||
// Some financial institutions (cough, Vanguard, cough), require a cookie
|
|
||||||
// to be set on the http request, or they return empty responses.
|
|
||||||
// Fortunately, the initial response contains the cookie we need, so if we
|
|
||||||
// detect an empty response with cookies set that didn't have any errors,
|
|
||||||
// re-try the request while sending their cookies back to them.
|
|
||||||
if err == nil && response.ContentLength <= 0 && len(response.Cookies()) > 0 {
|
|
||||||
b, err = r.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rawRequestCookies(r.URL, b, response.Cookies())
|
// clientRequest can be used for building clients' Request methods if they
|
||||||
}
|
// require fairly standard behavior
|
||||||
|
func clientRequest(c Client, r *Request) (*Response, error) {
|
||||||
return response, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request marshals a Request object into XML, makes an HTTP request against
|
|
||||||
// it's URL, and then unmarshals the response into a Response object.
|
|
||||||
//
|
|
||||||
// Before being marshaled, some of the the Request object's values are
|
|
||||||
// overwritten, namely those dictated by the BasicClient's configuration (Version,
|
|
||||||
// AppID, AppVer fields), and the client's current time (DtClient). These are
|
|
||||||
// updated in place in the supplied Request object so they may later be
|
|
||||||
// inspected by the caller.
|
|
||||||
func (c *BasicClient) Request(r *Request) (*Response, error) {
|
|
||||||
response, err := c.RequestNoParse(r)
|
response, err := c.RequestNoParse(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -126,12 +126,13 @@ func tryProfile(appID, appVer, version string, noindent bool) bool {
|
|||||||
fmt.Println("Error creating new OfxVersion enum:", err)
|
fmt.Println("Error creating new OfxVersion enum:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
var client = ofxgo.BasicClient{
|
var client = ofxgo.GetClient(serverURL,
|
||||||
|
&ofxgo.BasicClient{
|
||||||
AppID: appID,
|
AppID: appID,
|
||||||
AppVer: appVer,
|
AppVer: appVer,
|
||||||
SpecVersion: ver,
|
SpecVersion: ver,
|
||||||
NoIndent: noindent,
|
NoIndent: noindent,
|
||||||
}
|
})
|
||||||
|
|
||||||
var query ofxgo.Request
|
var query ofxgo.Request
|
||||||
query.URL = serverURL
|
query.URL = serverURL
|
||||||
|
@ -12,12 +12,13 @@ func newRequest() (ofxgo.Client, *ofxgo.Request) {
|
|||||||
fmt.Println("Error creating new OfxVersion enum:", err)
|
fmt.Println("Error creating new OfxVersion enum:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
var client = ofxgo.BasicClient{
|
var client = ofxgo.GetClient(serverURL,
|
||||||
|
&ofxgo.BasicClient{
|
||||||
AppID: appID,
|
AppID: appID,
|
||||||
AppVer: appVer,
|
AppVer: appVer,
|
||||||
SpecVersion: ver,
|
SpecVersion: ver,
|
||||||
NoIndent: noIndentRequests,
|
NoIndent: noIndentRequests,
|
||||||
}
|
})
|
||||||
|
|
||||||
var query ofxgo.Request
|
var query ofxgo.Request
|
||||||
query.URL = serverURL
|
query.URL = serverURL
|
||||||
@ -27,5 +28,5 @@ func newRequest() (ofxgo.Client, *ofxgo.Request) {
|
|||||||
query.Signon.Org = ofxgo.String(org)
|
query.Signon.Org = ofxgo.String(org)
|
||||||
query.Signon.Fid = ofxgo.String(fid)
|
query.Signon.Fid = ofxgo.String(fid)
|
||||||
|
|
||||||
return &client, &query
|
return client, &query
|
||||||
}
|
}
|
||||||
|
79
vanguard_client.go
Normal file
79
vanguard_client.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package ofxgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VanguardClient provides a Client implementation which handles Vanguard's
|
||||||
|
// cookie-passing requirements. VanguardClient uses default, non-zero settings,
|
||||||
|
// if its fields are not initialized.
|
||||||
|
type VanguardClient struct {
|
||||||
|
*BasicClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVanguardClient returns a Client interface configured to handle Vanguard's
|
||||||
|
// brand of idiosyncracy
|
||||||
|
func NewVanguardClient(bc *BasicClient) Client {
|
||||||
|
return &VanguardClient{bc}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rawRequestCookies is RawRequest with the added feature of sending cookies
|
||||||
|
func rawRequestCookies(URL string, r io.Reader, cookies []*http.Cookie) (*http.Response, error) {
|
||||||
|
if !strings.HasPrefix(URL, "https://") {
|
||||||
|
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequest("POST", URL, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
request.Header.Set("Content-Type", "application/x-ofx")
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
request.AddCookie(cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := http.DefaultClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode != 200 {
|
||||||
|
return nil, errors.New("OFXQuery request status: " + response.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VanguardClient) RequestNoParse(r *Request) (*http.Response, error) {
|
||||||
|
r.SetClientFields(c)
|
||||||
|
|
||||||
|
b, err := r.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := c.RawRequest(r.URL, b)
|
||||||
|
|
||||||
|
// Some financial institutions (cough, Vanguard, cough), require a cookie
|
||||||
|
// to be set on the http request, or they return empty responses.
|
||||||
|
// Fortunately, the initial response contains the cookie we need, so if we
|
||||||
|
// detect an empty response with cookies set that didn't have any errors,
|
||||||
|
// re-try the request while sending their cookies back to them.
|
||||||
|
if err == nil && response.ContentLength <= 0 && len(response.Cookies()) > 0 {
|
||||||
|
b, err = r.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawRequestCookies(r.URL, b, response.Cookies())
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VanguardClient) Request(r *Request) (*Response, error) {
|
||||||
|
return clientRequest(c, r)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user