06/12/2020 - GO
In this example we are going to study two strategy pattern implementations but both of them achieve the same outcome in a slightly different way. They simply create token for different banks.
Both strategy models are valid in real-world scenarios so the "duplicated solution" doesn't necessarily mean it is a bad design at all. It is just how you can handle such business case. Also both models have their strengths and weaknesses.
Both examples rely on the ProviderName
parameter in request objects. Based on the given value:
├── main.go
└── obie
├── access_token.go
├── barclays
│ ├── config.go
│ └── provider.go
├── natwest
│ ├── config.go
│ └── provider.go
└── provider.go
package main
import (
"context"
"net/http"
"github.com/you/obie"
"github.com/you/obie/barclays"
"github.com/you/obie/natwest"
)
func main() {
barcPrv := barclays.NewProvider(barclays.Config{
TokenEndpoint: "https://barclays/token/endpoint",
ClientID: "barclays-client",
ClientSecret: "barclays-secret",
}, http.DefaultTransport)
natwPrv := natwest.NewProvider(natwest.Config{
TokenEndpoint: "https://natwest/token/endpoint",
ClientID: "natwest-client",
ClientSecret: "natwest-secret",
Scope: "natwest scope",
}, http.DefaultTransport)
provider := obie.NewProviderStrategy()
provider.Add(barclays.Name, barcPrv)
provider.Add(natwest.Name, natwPrv)
ctx := context.Background()
_, _ = accessToken(ctx, provider, obie.AccessTokenRequest{ProviderName: barclays.Name})
_, _ = accessToken(ctx, provider, obie.AccessTokenRequest{ProviderName: natwest.Name})
}
func accessToken(ctx context.Context, prv obie.Provider, req obie.AccessTokenRequest) (obie.AccessToken, error) {
return prv.AccessToken(ctx, req)
}
package obie
type AccessTokenRequest struct {
ProviderName Name
}
type AccessToken struct {
AccessToken string
RefreshToken string
TokenType string
Scope string
ExpiresIn int
}
package obie
import (
"context"
"fmt"
)
type Name string
type Provider interface {
AccessToken(context.Context, AccessTokenRequest) (AccessToken, error)
}
type ProviderStrategy struct {
providers map[Name]Provider
}
func NewProviderStrategy() *ProviderStrategy {
return &ProviderStrategy{
providers: make(map[Name]Provider),
}
}
func (p *ProviderStrategy) Add(name Name, prv Provider) {
p.providers[name] = prv
}
func (p *ProviderStrategy) AccessToken(ctx context.Context, req AccessTokenRequest) (AccessToken, error) {
if _, ok := p.providers[req.ProviderName]; !ok {
return AccessToken{}, fmt.Errorf("access token: unknown provider: %s", req.ProviderName)
}
return p.providers[req.ProviderName].AccessToken(ctx, req)
}
package barclays
type Config struct{
TokenEndpoint string
ClientID string
ClientSecret string
}
package barclays
import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"github.com/you/obie"
)
var Name obie.Name = "Barclays"
type Provider struct {
config Config
transport http.RoundTripper
}
func NewProvider(conf Config, trans http.RoundTripper) Provider {
return Provider{
config: conf,
transport: trans,
}
}
func (p Provider) AccessToken(ctx context.Context, req obie.AccessTokenRequest) (obie.AccessToken, error) {
data := url.Values{}
data.Set("grant_type", "client_credentials")
data.Set("client_id", p.config.ClientID)
data.Set("client_secret", p.config.ClientSecret)
httpReq, _ := http.NewRequestWithContext(
ctx,
http.MethodPost,
p.config.TokenEndpoint,
bytes.NewReader([]byte(data.Encode())),
)
body, _ := httputil.DumpRequest(httpReq, true)
fmt.Println(string(body))
// Send the request and handle the response
// b.transport.RoundTrip(httpReq)
return obie.AccessToken{}, nil
}
package natwest
type Config struct{
TokenEndpoint string
ClientID string
ClientSecret string
Scope string
}
package natwest
import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"github.com/you/obie"
)
var Name obie.Name = "Natwest"
type Provider struct {
config Config
transport http.RoundTripper
}
func NewProvider(conf Config, trans http.RoundTripper) Provider {
return Provider{
config: conf,
transport: trans,
}
}
func (p Provider) AccessToken(ctx context.Context, req obie.AccessTokenRequest) (obie.AccessToken, error) {
data := url.Values{}
data.Set("grant_type", "client_credentials")
data.Set("client_id", p.config.ClientID)
data.Set("client_secret", p.config.ClientSecret)
data.Set("scope", p.config.Scope)
httpReq, _ := http.NewRequestWithContext(
ctx,
http.MethodPost,
p.config.TokenEndpoint,
bytes.NewReader([]byte(data.Encode())),
)
body, _ := httputil.DumpRequest(httpReq, true)
fmt.Println(string(body))
// Send the request and handle the response
// b.transport.RoundTrip(httpReq)
return obie.AccessToken{}, nil
}
├── main.go
└── obie
├── access_token.go
├── client.go
├── config.go
└── refresh_token.go
package main
import (
"context"
"net/http"
"github.com/you/obie"
)
func main() {
barclays := obie.ProviderConfig{
TokenEndpoint: "https://barclays/token/endpoint",
ClientID: "barclays-client",
ClientSecret: "barclays-secret",
}
natwest := obie.ProviderConfig{
TokenEndpoint: "https://natwest/token/endpoint",
ClientID: "natwest-client",
ClientSecret: "natwest-secret",
}
config := &obie.Config{
Providers: map[obie.ProviderName]obie.ProviderConfig{
obie.ProviderName("barclays"): barclays,
obie.ProviderName("natwest"): natwest,
},
}
ctx := context.Background()
client := obie.NewClient(config, http.DefaultTransport)
_, _ = client.AccessToken(ctx, obie.AccessTokenRequest{ProviderName: "barclays"})
_, _ = client.RefreshToken(ctx, obie.RefreshTokenRequest{ProviderName: "natwest", RefreshToken: "ref-tok"})
}
package obie
type ProviderName string
type Config struct {
Providers map[ProviderName]ProviderConfig
}
type ProviderConfig struct {
TokenEndpoint string
ClientID string
ClientSecret string
}
package obie
import (
"context"
"net/http"
)
type Client interface {
AccessToken(context.Context, AccessTokenRequest) (AccessToken, error)
RefreshToken(context.Context, RefreshTokenRequest) (AccessToken, error)
}
type ClientStrategy struct {
config *Config
transport http.RoundTripper
}
func NewClient(config *Config, transport http.RoundTripper) ClientStrategy {
return ClientStrategy{
config: config,
transport: transport,
}
}
package obie
import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
)
type AccessTokenRequest struct {
ProviderName ProviderName
}
type AccessToken struct {
AccessToken string
RefreshToken string
TokenType string
Scope string
ExpiresIn int
}
func (c ClientStrategy) AccessToken(ctx context.Context, req AccessTokenRequest) (AccessToken, error) {
config := c.config.Providers[req.ProviderName]
data := url.Values{}
data.Set("grant_type", "client_credentials")
data.Set("client_id", config.ClientID)
data.Set("client_secret", config.ClientSecret)
httpReq, _ := http.NewRequestWithContext(
ctx,
http.MethodPost,
config.TokenEndpoint,
bytes.NewReader([]byte(data.Encode())),
)
httpReq.Header.Set("Cache-Control", "no-store")
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
httpReq.Header.Set("Pragma", "no-cache")
body, _ := httputil.DumpRequest(httpReq, true)
fmt.Println(string(body))
// Send the request and handle the response
// c.transport.RoundTrip(httpReq)
return AccessToken{}, nil
}
package obie
import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
)
type RefreshTokenRequest struct {
ProviderName ProviderName
RefreshToken string
}
func (c ClientStrategy) RefreshToken(ctx context.Context, req RefreshTokenRequest) (AccessToken, error) {
config := c.config.Providers[req.ProviderName]
data := url.Values{}
data.Set("grant_type", "refresh_token")
data.Set("client_id", config.ClientID)
data.Set("client_secret", config.ClientSecret)
data.Set("refresh_token", req.RefreshToken)
httpReq, _ := http.NewRequestWithContext(
ctx,
http.MethodPost,
config.TokenEndpoint,
bytes.NewReader([]byte(data.Encode())),
)
httpReq.Header.Set("Cache-Control", "no-store")
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
httpReq.Header.Set("Pragma", "no-cache")
body, _ := httputil.DumpRequest(httpReq, true)
fmt.Println(string(body))
// Send the request and handle the response
// c.transport.RoundTrip(httpReq)
return AccessToken{}, nil
}