From c4fca88ad964472e778271dc181637202de0b43f Mon Sep 17 00:00:00 2001 From: Lucas Rouckhout Date: Wed, 13 Mar 2024 13:49:10 +0100 Subject: [PATCH] Initial changes to Client constructor to make it more end-user friendly --- README.md | 111 +++++++++++++++++++++++++++++++++++----------- example/simple.go | 38 ++++++++++++++++ session.go | 4 +- session_test.go | 2 +- twikey.go | 86 +++++++++++++++++++++++++++++++---- twikey_test.go | 35 +++++++++++++++ 6 files changed, 239 insertions(+), 37 deletions(-) create mode 100644 example/simple.go diff --git a/README.md b/README.md index 4cf3e3b..dfc6c6a 100644 --- a/README.md +++ b/README.md @@ -52,51 +52,110 @@ Initializing the Twikey API client by configuring your API key which you can fin the [Twikey merchant interface](https://www.twikey.com). ```go +package example + +import "github.com/twikey/twikey-api-go" + +func main() { + client := twikey.NewClient("YOU_API_KEY") +} + +``` + +It's possible to further configure the API client if so desired using functional options. +For example providing a custom HTTP client with a different timeout and a Custom logger +implementation. + +```go +package example + import ( - twikey "github.com/twikey/twikey-api-go" + "github.com/twikey/twikey-api-go" + "log" + "net/http" ) -var twikeyClient = twikey.NewClient(os.Getenv("TWIKEY_API_KEY")) +func main() { + client := twikey.NewClient("YOUR_API_KEY", + twikey.WithHTTPClient(&http.Client{Timeout: time.Minute}), + twikey.WithLogger(log.Default()), + ) +} + +``` + +Another example, it's possible to disable logging by setting the value of the logger to `NullLogger`. +By default, the Twikey client will print logs using the default standard library logger. -var twikeyClient = &twikey.TwikeyClient{ - ApiKey: os.Getenv("TWIKEY_API_KEY"), - //Debug: log.Default(), - HTTPClient: &http.Client{ - Timeout: time.Minute, - }, +```go +package example + +import ( + "github.com/twikey/twikey-api-go" + "log" + "net/http" +) + +func main() { + client := twikey.NewClient("YOUR_API_KEY", + twikey.WithLogger(twikey.NullLogger), + ) } ``` ## Documents -Invite a customer to sign a SEPA mandate using a specific behaviour template (ct) that allows you to configure -the behaviour or flow that the customer will experience. This 'ct' can be found in the template section of the settings. +Invite a customer to sign a SEPA mandate using a specific behaviour template (Template) that allows you to configure +the behaviour or flow that the customer will experience. This can be found in the template section of the settings. The extra can be used to pass in extra attributes linked to the mandate. ```go -invite, err := twikeyClient.DocumentInvite(context.Background(), &InviteRequest{ - ct: yourct, - customerNumber: "123", - email: "john@doe.com", - firstname: "John", - lastname: "Doe", - l: "en", - address: "Abbey road", - city: "Liverpool", - zip: "1526", - country: "BE", -}, nil) -if err != nil { - t.Fatal(err) +package example + +import ( + "context" + "fmt" + "github.com/twikey/twikey-api-go" + "log" + "os" +) + +func main() { + client := twikey.NewClient(os.Getenv("TWIKEY_API_KEY")) + + ctx := context.Background() + invite, err := client.DocumentSign(ctx, &twikey.InviteRequest{ + Template: "YOUR_TEMPLATE_ID", + CustomerNumber: "123", + Email: "john@doe.com", + Language: "en", + Lastname: "Doe", + Firstname: "John", + Address: "Abbey Road", + City: "Liverpool", + Zip: "1562", + Country: "EN", + Iban: "GB32BARC20040198915359", + Bic: "GEBEBEB", + Method: "sms", + Extra: map[string]string{ + "SomeKey": "VALUE", + }, + }) + if err != nil { + log.Fatal(err) + } + + fmt.Println(invite.Url) } -fmt.println(invite.Url) + ``` _After creation, the link available in invite.Url can be used to redirect the customer into the signing flow or even send him a link through any other mechanism. Ideally you store the mandatenumber for future usage (eg. sending transactions)._ -The DocumentSign function has a similar syntax only that it requires a method and is mosly used for interactive sessions +The DocumentSign function has a similar syntax only that it requires a method and is mostly used for interactive sessions where no screens are involved. See [the documentation](https://api.twikey.com) for more info. ### Feed diff --git a/example/simple.go b/example/simple.go new file mode 100644 index 0000000..882ab1e --- /dev/null +++ b/example/simple.go @@ -0,0 +1,38 @@ +package main + +import ( + "context" + "fmt" + "github.com/twikey/twikey-api-go" + "log" + "os" +) + +func main() { + client := twikey.NewClient(os.Getenv("TWIKEY_API_KEY")) + + ctx := context.Background() + invite, err := client.DocumentSign(ctx, &twikey.InviteRequest{ + Template: "YOUR_TEMPLATE_ID", + CustomerNumber: "123", + Email: "john@doe.com", + Language: "en", + Lastname: "Doe", + Firstname: "John", + Address: "Abbey Road", + City: "Liverpool", + Zip: "1562", + Country: "EN", + Iban: "GB32BARC20040198915359", + Bic: "GEBEBEB", + Method: "sms", + Extra: map[string]string{ + "SomeKey": "VALUE", + }, + }) + if err != nil { + log.Fatal(err) + } + + fmt.Println(invite.Url) +} diff --git a/session.go b/session.go index f333172..fbe3865 100644 --- a/session.go +++ b/session.go @@ -48,7 +48,7 @@ func generateOtp(_salt string, _privKey string) (int, error) { func (c *Client) refreshTokenIfRequired() error { - if c.timeProvider.Now().Sub(c.lastLogin).Hours() < 23 { + if c.TimeProvider.Now().Sub(c.lastLogin).Hours() < 23 { return nil } @@ -71,7 +71,7 @@ func (c *Client) refreshTokenIfRequired() error { if resp.StatusCode == 200 && token != nil { c.Debug.Println("Connected to", c.BaseURL, "with token", token[0]) c.apiToken = token[0] - c.lastLogin = c.timeProvider.Now() + c.lastLogin = c.TimeProvider.Now() return nil } else if resp.StatusCode > 500 { c.Debug.Println("General error", resp.StatusCode, resp.Status) diff --git a/session_test.go b/session_test.go index 32d20a0..e162609 100644 --- a/session_test.go +++ b/session_test.go @@ -35,7 +35,7 @@ func TestClient_refreshTokenIfRequired(t *testing.T) { HTTPClient: &http.Client{ Timeout: time.Minute, }, - timeProvider: &ttp, + TimeProvider: &ttp, Debug: log.Default(), } c.BaseURL = getEnv("TWIKEY_URL", "https://api.beta.twikey.com") diff --git a/twikey.go b/twikey.go index 0f950fd..1569fb5 100644 --- a/twikey.go +++ b/twikey.go @@ -33,6 +33,20 @@ func (tp DefaultTimeProvider) Now() time.Time { return time.Now() } +type Logger interface { + Println(v ...interface{}) + Printf(format string, v ...interface{}) +} + +// nullWriter is an io.Writer that discards all writes. +type nullWriter struct{} + +func (n *nullWriter) Write(p []byte) (int, error) { + return len(p), nil +} + +var NullLogger = log.New(&nullWriter{}, "", log.LstdFlags) + // Client is the base class, please use a dedicated UserAgent so we can notify the emergency contact // if weird behaviour is perceived. type Client struct { @@ -42,17 +56,67 @@ type Client struct { Salt string UserAgent string HTTPClient HTTPClient - Debug *log.Logger + Debug Logger apiToken string lastLogin time.Time - timeProvider TimeProvider + TimeProvider TimeProvider +} + +type ClientOption = func(*Client) + +// WithLogger sets the Logger for the Client. If you don't want +// the Client to log anything you can pass in the NullLogger +func WithLogger(logger Logger) ClientOption { + return func(client *Client) { + client.Debug = logger + } +} + +// WithBaseURL will set the base URL of the API used when making requests. +// +// In production, you will probably want to use the default but if you want +// to make request to some mock API in a test environment you can use this +// to make the Client make requests to a different host. +// +// The default: https://api.twikey.com +func WithBaseURL(baseURL string) ClientOption { + return func(client *Client) { + client.BaseURL = baseURL + } +} + +// WithHTTPClient configures the underlying HTTP client used to make HTTP requests. +func WithHTTPClient(httpClient HTTPClient) ClientOption { + return func(client *Client) { + client.HTTPClient = httpClient + } +} + +// WithTimeProvider sets the TimeProvider for this Client. +func WithTimeProvider(provider TimeProvider) ClientOption { + return func(client *Client) { + client.TimeProvider = provider + } +} + +// WithSalt sets the salt used in generating one-time-passwords +func WithSalt(salt string) ClientOption { + return func(client *Client) { + client.Salt = salt + } +} + +// WithUserAgent will configure the value that is passed on in the HTTP User-Agent header +// for all requests to the Twikey API made with this Client +func WithUserAgent(userAgent string) ClientOption { + return func(client *Client) { + client.UserAgent = userAgent + } } // NewClient is a convenience method to hit the ground running with the Twikey Rest API -func NewClient(apiKey string) *Client { - logger := log.Default() - logger.SetOutput(ioutil.Discard) - return &Client{ +func NewClient(apiKey string, opts ...ClientOption) *Client { + c := &Client{ BaseURL: baseURLV1, APIKey: apiKey, Salt: "own", @@ -60,9 +124,15 @@ func NewClient(apiKey string) *Client { HTTPClient: &http.Client{ Timeout: time.Minute, }, - Debug: logger, - timeProvider: DefaultTimeProvider{}, + Debug: log.Default(), + TimeProvider: DefaultTimeProvider{}, } + + for _, opt := range opts { + opt(c) + } + + return c } type errorResponse struct { diff --git a/twikey_test.go b/twikey_test.go index ff7c1ab..7a42e7e 100644 --- a/twikey_test.go +++ b/twikey_test.go @@ -1,6 +1,7 @@ package twikey import ( + "log" "os" "testing" ) @@ -11,6 +12,40 @@ func newTestClient() *Client { return c } +func TestClientWithBasicFunctionalOptions(t *testing.T) { + salt := "salty" + baseUrl := "http://localtest.me" + userAgent := "someUserAgent" + apikey := "123" + + c := NewClient(apikey, + WithBaseURL(baseUrl), + WithSalt(salt), + WithUserAgent(userAgent), + WithLogger(log.Default()), + ) + + if c.APIKey != apikey { + t.Fatalf("Expected client ApiKey to be %s but was %s", apikey, c.APIKey) + } + + if c.BaseURL != baseUrl { + t.Fatalf("Expected client BaseURL to be %s but was %s", baseUrl, c.BaseURL) + } + + if c.Salt != salt { + t.Fatalf("Expected client Salt to be %s but was %s", salt, c.Salt) + } + + if c.UserAgent != userAgent { + t.Fatalf("Expected client UserAgent to be %s but was %s", userAgent, c.UserAgent) + } + + if c.Debug != log.Default() { + t.Fatalf("Expected the default logger from log to be used") + } +} + func TestTwikeyClient_verifyWebhook(t *testing.T) { c := NewClient("1234") err := c.VerifyWebhook("55261CBC12BF62000DE1371412EF78C874DBC46F513B078FB9FF8643B2FD4FC2", "abc=123&name=abc")