Merge pull request #11 from aclindsa/make_client_interface

Make Client an interface instead of a struct
This commit is contained in:
Aaron Lindsay 2018-10-03 10:23:40 -04:00 committed by GitHub
commit ac09538ec3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 230 additions and 149 deletions

80
basic_client.go Normal file
View 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)
}

192
client.go
View File

@ -1,7 +1,6 @@
package ofxgo
import (
"errors"
"io"
"net/http"
"strings"
@ -18,122 +17,65 @@ type Client interface {
Version() String
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)
// 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)
// RawRequest is little more than a thin wrapper around http.Post
//
// 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 yourself (perhaps for downloading an OFX file for use by an
// external program, or debugging server behavior), or have a handcrafted
// request you'd like to try.
//
// Caveats: RawRequest does *not* take client settings into account as
// 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 Response.Body (see the http module's documentation for more
// information)
RawRequest(URL string, r io.Reader) (*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
type clientCreationFunc func(*BasicClient) Client
// Don't insert newlines or indentation when marshalling to SGML/XML
NoIndent bool
// 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
}
// 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
//
// 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
// yourself (perhaps for downloading an OFX file for use by an external
// program, or debugging server behavior), or have a handcrafted request you'd
// like to try.
//
// Caveats: RawRequest does *not* take client settings into account as
// 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
// Response.Body (see the http module's documentation for more information)
func 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
}
// 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) {
// clientRequestNoParse can be used for building clients' RequestNoParse
// methods if they require fairly standard behavior
func clientRequestNoParse(c Client, r *Request) (*http.Response, error) {
r.SetClientFields(c)
b, err := r.Marshal()
@ -141,34 +83,12 @@ func (c *BasicClient) RequestNoParse(r *Request) (*http.Response, error) {
return nil, err
}
response, err := 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
return c.RawRequest(r.URL, b)
}
// 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) {
// clientRequest can be used for building clients' Request methods if they
// require fairly standard behavior
func clientRequest(c Client, r *Request) (*Response, error) {
response, err := c.RequestNoParse(r)
if err != nil {
return nil, err

View File

@ -126,12 +126,13 @@ func tryProfile(appID, appVer, version string, noindent bool) bool {
fmt.Println("Error creating new OfxVersion enum:", err)
os.Exit(1)
}
var client = ofxgo.BasicClient{
AppID: appID,
AppVer: appVer,
SpecVersion: ver,
NoIndent: noindent,
}
var client = ofxgo.GetClient(serverURL,
&ofxgo.BasicClient{
AppID: appID,
AppVer: appVer,
SpecVersion: ver,
NoIndent: noindent,
})
var query ofxgo.Request
query.URL = serverURL

View File

@ -12,12 +12,13 @@ func newRequest() (ofxgo.Client, *ofxgo.Request) {
fmt.Println("Error creating new OfxVersion enum:", err)
os.Exit(1)
}
var client = ofxgo.BasicClient{
AppID: appID,
AppVer: appVer,
SpecVersion: ver,
NoIndent: noIndentRequests,
}
var client = ofxgo.GetClient(serverURL,
&ofxgo.BasicClient{
AppID: appID,
AppVer: appVer,
SpecVersion: ver,
NoIndent: noIndentRequests,
})
var query ofxgo.Request
query.URL = serverURL
@ -27,5 +28,5 @@ func newRequest() (ofxgo.Client, *ofxgo.Request) {
query.Signon.Org = ofxgo.String(org)
query.Signon.Fid = ofxgo.String(fid)
return &client, &query
return client, &query
}

79
vanguard_client.go Normal file
View 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)
}