Cache calls to SecurityTypeByISIN #18

Merged
natercio merged 2 commits from cache-figi-responses into main 2025-11-25 13:52:05 +00:00
2 changed files with 56 additions and 1 deletions
Showing only changes of commit 57ee768006 - Show all commits

View File

@@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"sync"
"time" "time"
"github.com/biter777/countries" "github.com/biter777/countries"
@@ -16,16 +17,28 @@ import (
type OpenFIGI struct { type OpenFIGI struct {
client *http.Client client *http.Client
mappingLimiter *rate.Limiter mappingLimiter *rate.Limiter
mu sync.RWMutex
securityTypeCache map[string]string
} }
func NewOpenFIGI(c *http.Client) *OpenFIGI { func NewOpenFIGI(c *http.Client) *OpenFIGI {
return &OpenFIGI{ return &OpenFIGI{
client: c, client: c,
mappingLimiter: rate.NewLimiter(rate.Every(time.Minute), 25), // https://www.openfigi.com/api/documentation#rate-limits mappingLimiter: rate.NewLimiter(rate.Every(time.Minute), 25), // https://www.openfigi.com/api/documentation#rate-limits
securityTypeCache: make(map[string]string),
} }
} }
func (of *OpenFIGI) SecurityTypeByISIN(ctx context.Context, isin string) (string, error) { func (of *OpenFIGI) SecurityTypeByISIN(ctx context.Context, isin string) (string, error) {
of.mu.RLock()
if secType, ok := of.securityTypeCache[isin]; ok {
of.mu.RUnlock()
return secType, nil
}
of.mu.RUnlock()
if len(isin) != 12 || countries.ByName(isin[:2]) == countries.Unknown { if len(isin) != 12 || countries.ByName(isin[:2]) == countries.Unknown {
return "", fmt.Errorf("invalid ISIN: %s", isin) return "", fmt.Errorf("invalid ISIN: %s", isin)
} }
@@ -76,7 +89,13 @@ func (of *OpenFIGI) SecurityTypeByISIN(ctx context.Context, isin string) (string
// It is not possible that an isin is assign to diferent security types, therefore we can assume // It is not possible that an isin is assign to diferent security types, therefore we can assume
// all entries have the same securityType value. // all entries have the same securityType value.
return resBody[0].Data[0].SecurityType, nil secType := resBody[0].Data[0].SecurityType
of.mu.Lock()
of.securityTypeCache[isin] = secType
of.mu.Unlock()
return secType, nil
} }
type mappingRequestBody struct { type mappingRequestBody struct {

View File

@@ -118,6 +118,42 @@ func TestOpenFIGI_SecurityTypeByISIN(t *testing.T) {
} }
} }
func TestOpenFIGI_SecurityTypeByISIN_Cache(t *testing.T) {
var alreadyCalled bool
c := NewTestClient(t, func(req *http.Request) (*http.Response, error) {
if alreadyCalled {
t.Fatalf("want requests to be cached")
}
alreadyCalled = true
return &http.Response{
Status: http.StatusText(http.StatusOK),
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(`[{"data":[{"securityType":"Common Stock"}]}]`)),
}, nil
})
of := internal.NewOpenFIGI(c)
got, gotErr := of.SecurityTypeByISIN(t.Context(), "NL0000235190")
if gotErr != nil {
t.Fatalf("want 1st success call but got error: %v", gotErr)
}
if got != "Common Stock" {
t.Fatalf("want 1st securityType to be %q but got %q", "Common Stock", got)
}
got, gotErr = of.SecurityTypeByISIN(t.Context(), "NL0000235190")
if gotErr != nil {
t.Fatalf("want 2nd success call but got error: %v", gotErr)
}
if got != "Common Stock" {
t.Fatalf("want 2nd securityType to be %q but got %q", "Common Stock", got)
}
}
type RoundTripFunc func(req *http.Request) (*http.Response, error) type RoundTripFunc func(req *http.Request) (*http.Response, error)
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {