mirror of
https://github.com/aclindsa/ofxgo.git
synced 2024-11-22 11:30:05 -05:00
Merge pull request #11 from aclindsa/make_client_interface
Make Client an interface instead of a struct
This commit is contained in:
commit
ac09538ec3
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