Skip to content
Snippets Groups Projects
Commit 9da70a3e authored by Lyuben Penkovski's avatar Lyuben Penkovski
Browse files

Merge branch 'optional-namespace-scope' into 'main'

Make the namespace and scope fields optional when setting and getting values from cache

See merge request !4
parents 60e23039 c38b1219
No related branches found
No related tags found
1 merge request!4Make the namespace and scope fields optional when setting and getting values from cache
Pipeline #50595 passed with stage
in 43 seconds
Showing
with 511 additions and 81 deletions
[![pipeline status](https://code.vereign.com/gaiax/tsa/cache/badges/main/pipeline.svg)](https://code.vereign.com/gaiax/tsa/cache/-/commits/main)
[![coverage report](https://code.vereign.com/gaiax/tsa/cache/badges/main/coverage.svg)](https://code.vereign.com/gaiax/tsa/cache/-/commits/main)
# Cache service
Cache service exposes HTTP interface for working with Redis.
### Basic Architecture
```mermaid
flowchart LR
A[Client] -- request --> B[Cache Service] --> C[(Redis)]
```mermaid
flowchart LR
A[Client] -- request --> B[HTTP API]
subgraph cache
B --> C[(Redis)]
end
```
### API Documentation
The API Documentation is accessible at `/swagger-ui` path in OAS 3.0 format.
Example: `http://localhost:8080/swagger-ui`
The API Documentation is accessible at `/swagger-ui` path in OAS 3.0 format. If you
use the docker-compose environment, it's exposed at `http://localhost:8083/swagger-ui`
### Dependencies
There must be a running instance of [Redis](https://redis.io/) visible to the service.
The address, username and password of the Redis in-memory store instance must be provided as environment variables.
The address, username and password of Redis must be provided as environment variables.
Example:
```
......@@ -28,11 +34,12 @@ REDIS_PASS="pass"
### Development
This service uses [Goa framework](https://goa.design/) v3 as a backbone. [This](https://goa.design/learn/getting-started/) is a good starting point for learning to use the framework.
This service uses [Goa framework](https://goa.design/) v3 as a backbone.
[This](https://goa.design/learn/getting-started/) is a good starting point for learning to use the framework.
### Dependencies and Vendor
The project uses Go modules for managing dependencies and we commit the `vendor` directory.
The project uses Go modules for managing dependencies, and we commit the `vendor` directory.
When you add/change dependencies, be sure to clean and update the `vendor` directory before
submitting your Merge Request for review.
```shell
......
......@@ -7,7 +7,7 @@ var CacheGetRequest = Type("CacheGetRequest", func() {
Field(1, "key", String)
Field(2, "namespace", String)
Field(3, "scope", String) // Initial implementation with a single scope
Required("key", "namespace", "scope")
Required("key")
})
var CacheSetRequest = Type("CacheSetRequest", func() {
......@@ -15,5 +15,5 @@ var CacheSetRequest = Type("CacheSetRequest", func() {
Field(2, "key", String)
Field(3, "namespace", String)
Field(4, "scope", String) // Initial implementation with a single scope
Required("data", "key", "namespace", "scope")
Required("data", "key")
})
......@@ -32,14 +32,14 @@ var MethodNames = [2]string{"Get", "Set"}
// CacheGetRequest is the payload type of the cache service Get method.
type CacheGetRequest struct {
Key string
Namespace string
Scope string
Namespace *string
Scope *string
}
// CacheSetRequest is the payload type of the cache service Set method.
type CacheSetRequest struct {
Data interface{}
Key string
Namespace string
Scope string
Namespace *string
Scope *string
}
......@@ -20,13 +20,17 @@ func BuildGetPayload(cacheGetKey string, cacheGetNamespace string, cacheGetScope
{
key = cacheGetKey
}
var namespace string
var namespace *string
{
namespace = cacheGetNamespace
if cacheGetNamespace != "" {
namespace = &cacheGetNamespace
}
}
var scope string
var scope *string
{
scope = cacheGetScope
if cacheGetScope != "" {
scope = &cacheGetScope
}
}
v := &cache.CacheGetRequest{}
v.Key = key
......@@ -50,13 +54,17 @@ func BuildSetPayload(cacheSetBody string, cacheSetKey string, cacheSetNamespace
{
key = cacheSetKey
}
var namespace string
var namespace *string
{
namespace = cacheSetNamespace
if cacheSetNamespace != "" {
namespace = &cacheSetNamespace
}
}
var scope string
var scope *string
{
scope = cacheSetScope
if cacheSetScope != "" {
scope = &cacheSetScope
}
}
v := body
res := &cache.CacheSetRequest{
......
......@@ -45,12 +45,12 @@ func EncodeGetRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Re
head := p.Key
req.Header.Set("x-cache-key", head)
}
{
head := p.Namespace
if p.Namespace != nil {
head := *p.Namespace
req.Header.Set("x-cache-namespace", head)
}
{
head := p.Scope
if p.Scope != nil {
head := *p.Scope
req.Header.Set("x-cache-scope", head)
}
return nil
......@@ -119,12 +119,12 @@ func EncodeSetRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Re
head := p.Key
req.Header.Set("x-cache-key", head)
}
{
head := p.Namespace
if p.Namespace != nil {
head := *p.Namespace
req.Header.Set("x-cache-namespace", head)
}
{
head := p.Scope
if p.Scope != nil {
head := *p.Scope
req.Header.Set("x-cache-scope", head)
}
body := p.Data
......
......@@ -35,21 +35,21 @@ func DecodeGetRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Dec
return func(r *http.Request) (interface{}, error) {
var (
key string
namespace string
scope string
namespace *string
scope *string
err error
)
key = r.Header.Get("x-cache-key")
if key == "" {
err = goa.MergeErrors(err, goa.MissingFieldError("x-cache-key", "header"))
}
namespace = r.Header.Get("x-cache-namespace")
if namespace == "" {
err = goa.MergeErrors(err, goa.MissingFieldError("x-cache-namespace", "header"))
namespaceRaw := r.Header.Get("x-cache-namespace")
if namespaceRaw != "" {
namespace = &namespaceRaw
}
scope = r.Header.Get("x-cache-scope")
if scope == "" {
err = goa.MergeErrors(err, goa.MissingFieldError("x-cache-scope", "header"))
scopeRaw := r.Header.Get("x-cache-scope")
if scopeRaw != "" {
scope = &scopeRaw
}
if err != nil {
return nil, err
......@@ -87,20 +87,20 @@ func DecodeSetRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Dec
var (
key string
namespace string
scope string
namespace *string
scope *string
)
key = r.Header.Get("x-cache-key")
if key == "" {
err = goa.MergeErrors(err, goa.MissingFieldError("x-cache-key", "header"))
}
namespace = r.Header.Get("x-cache-namespace")
if namespace == "" {
err = goa.MergeErrors(err, goa.MissingFieldError("x-cache-namespace", "header"))
namespaceRaw := r.Header.Get("x-cache-namespace")
if namespaceRaw != "" {
namespace = &namespaceRaw
}
scope = r.Header.Get("x-cache-scope")
if scope == "" {
err = goa.MergeErrors(err, goa.MissingFieldError("x-cache-scope", "header"))
scopeRaw := r.Header.Get("x-cache-scope")
if scopeRaw != "" {
scope = &scopeRaw
}
if err != nil {
return nil, err
......
......@@ -12,7 +12,7 @@ import (
)
// NewGetCacheGetRequest builds a cache service Get endpoint payload.
func NewGetCacheGetRequest(key string, namespace string, scope string) *cache.CacheGetRequest {
func NewGetCacheGetRequest(key string, namespace *string, scope *string) *cache.CacheGetRequest {
v := &cache.CacheGetRequest{}
v.Key = key
v.Namespace = namespace
......@@ -22,7 +22,7 @@ func NewGetCacheGetRequest(key string, namespace string, scope string) *cache.Ca
}
// NewSetCacheSetRequest builds a cache service Set endpoint payload.
func NewSetCacheSetRequest(body interface{}, key string, namespace string, scope string) *cache.CacheSetRequest {
func NewSetCacheSetRequest(body interface{}, key string, namespace *string, scope *string) *cache.CacheSetRequest {
v := body
res := &cache.CacheSetRequest{
Data: v,
......
......@@ -56,14 +56,14 @@ func ParseEndpoint(
cacheGetFlags = flag.NewFlagSet("get", flag.ExitOnError)
cacheGetKeyFlag = cacheGetFlags.String("key", "REQUIRED", "")
cacheGetNamespaceFlag = cacheGetFlags.String("namespace", "REQUIRED", "")
cacheGetScopeFlag = cacheGetFlags.String("scope", "REQUIRED", "")
cacheGetNamespaceFlag = cacheGetFlags.String("namespace", "", "")
cacheGetScopeFlag = cacheGetFlags.String("scope", "", "")
cacheSetFlags = flag.NewFlagSet("set", flag.ExitOnError)
cacheSetBodyFlag = cacheSetFlags.String("body", "REQUIRED", "")
cacheSetKeyFlag = cacheSetFlags.String("key", "REQUIRED", "")
cacheSetNamespaceFlag = cacheSetFlags.String("namespace", "REQUIRED", "")
cacheSetScopeFlag = cacheSetFlags.String("scope", "REQUIRED", "")
cacheSetNamespaceFlag = cacheSetFlags.String("namespace", "", "")
cacheSetScopeFlag = cacheSetFlags.String("scope", "", "")
)
healthFlags.Usage = healthUsage
healthLivenessFlags.Usage = healthLivenessUsage
......
{"swagger":"2.0","info":{"title":"Cache Service","description":"The cache service exposes interface for working with Redis.","version":""},"host":"localhost:8083","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/liveness":{"get":{"tags":["health"],"summary":"Liveness health","operationId":"health#Liveness","responses":{"200":{"description":"OK response."}},"schemes":["http"]}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response."}},"schemes":["http"]}},"/v1/cache":{"get":{"tags":["cache"],"summary":"Get cache","description":"Get JSON value from the cache.","operationId":"cache#Get","produces":["application/json"],"parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","required":true,"type":"string"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","required":true,"type":"string"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","required":true,"type":"string"}],"responses":{"200":{"description":"OK response.","schema":{"type":"string","format":"binary"}}},"schemes":["http"]},"post":{"tags":["cache"],"summary":"Set cache","description":"Set a JSON value in the cache.","operationId":"cache#Set","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","required":true,"type":"string"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","required":true,"type":"string"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","required":true,"type":"string"},{"name":"any","in":"body","required":true,"schema":{"type":"string","format":"binary"}}],"responses":{"201":{"description":"Created response."}},"schemes":["http"]}}}}
\ No newline at end of file
{"swagger":"2.0","info":{"title":"Cache Service","description":"The cache service exposes interface for working with Redis.","version":""},"host":"localhost:8083","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/liveness":{"get":{"tags":["health"],"summary":"Liveness health","operationId":"health#Liveness","responses":{"200":{"description":"OK response."}},"schemes":["http"]}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response."}},"schemes":["http"]}},"/v1/cache":{"get":{"tags":["cache"],"summary":"Get cache","description":"Get JSON value from the cache.","operationId":"cache#Get","produces":["application/json"],"parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","required":true,"type":"string"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","required":false,"type":"string"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","required":false,"type":"string"}],"responses":{"200":{"description":"OK response.","schema":{"type":"string","format":"binary"}}},"schemes":["http"]},"post":{"tags":["cache"],"summary":"Set cache","description":"Set a JSON value in the cache.","operationId":"cache#Set","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","required":true,"type":"string"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","required":false,"type":"string"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","required":false,"type":"string"},{"name":"any","in":"body","required":true,"schema":{"type":"string","format":"binary"}}],"responses":{"201":{"description":"Created response."}},"schemes":["http"]}}}}
\ No newline at end of file
......@@ -53,12 +53,12 @@ paths:
- name: x-cache-namespace
in: header
description: Cache entry namespace
required: true
required: false
type: string
- name: x-cache-scope
in: header
description: Cache entry scope
required: true
required: false
type: string
responses:
"200":
......@@ -83,12 +83,12 @@ paths:
- name: x-cache-namespace
in: header
description: Cache entry namespace
required: true
required: false
type: string
- name: x-cache-scope
in: header
description: Cache entry scope
required: true
required: false
type: string
- name: any
in: body
......
{"openapi":"3.0.3","info":{"title":"Cache Service","description":"The cache service exposes interface for working with Redis.","version":"1.0"},"servers":[{"url":"http://localhost:8083","description":"Cache Server"}],"paths":{"/liveness":{"get":{"tags":["health"],"summary":"Liveness health","operationId":"health#Liveness","responses":{"200":{"description":"OK response."}}}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response."}}}},"/v1/cache":{"get":{"tags":["cache"],"summary":"Get cache","description":"Get JSON value from the cache.","operationId":"cache#Get","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Cache entry key","example":"did:web:example.com"},"example":"did:web:example.com"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Cache entry namespace","example":"Login"},"example":"Login"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Cache entry scope","example":"administration"},"example":"administration"}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"type":"string","example":"Qui iusto enim est dolores dolorem et.","format":"binary"},"example":"Quisquam ab dolores distinctio quis."}}}}},"post":{"tags":["cache"],"summary":"Set cache","description":"Set a JSON value in the cache.","operationId":"cache#Set","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Cache entry key","example":"did:web:example.com"},"example":"did:web:example.com"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Cache entry namespace","example":"Login"},"example":"Login"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Cache entry scope","example":"administration"},"example":"administration"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"string","example":"Et illum fugiat ut.","format":"binary"},"example":"Optio aliquam error nam."}}},"responses":{"201":{"description":"Created response."}}}}},"components":{},"tags":[{"name":"health","description":"Health service provides health check endpoints."},{"name":"cache","description":"Cache service allows storing and retrieving data from distributed cache."}]}
\ No newline at end of file
{"openapi":"3.0.3","info":{"title":"Cache Service","description":"The cache service exposes interface for working with Redis.","version":"1.0"},"servers":[{"url":"http://localhost:8083","description":"Cache Server"}],"paths":{"/liveness":{"get":{"tags":["health"],"summary":"Liveness health","operationId":"health#Liveness","responses":{"200":{"description":"OK response."}}}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response."}}}},"/v1/cache":{"get":{"tags":["cache"],"summary":"Get cache","description":"Get JSON value from the cache.","operationId":"cache#Get","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Cache entry key","example":"did:web:example.com"},"example":"did:web:example.com"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry namespace","example":"Login"},"example":"Login"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry scope","example":"administration"},"example":"administration"}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"type":"string","example":"Qui iusto enim est dolores dolorem et.","format":"binary"},"example":"Quisquam ab dolores distinctio quis."}}}}},"post":{"tags":["cache"],"summary":"Set cache","description":"Set a JSON value in the cache.","operationId":"cache#Set","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Cache entry key","example":"did:web:example.com"},"example":"did:web:example.com"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry namespace","example":"Login"},"example":"Login"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry scope","example":"administration"},"example":"administration"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"string","example":"Et illum fugiat ut.","format":"binary"},"example":"Optio aliquam error nam."}}},"responses":{"201":{"description":"Created response."}}}}},"components":{},"tags":[{"name":"health","description":"Health service provides health check endpoints."},{"name":"cache","description":"Cache service allows storing and retrieving data from distributed cache."}]}
\ No newline at end of file
......@@ -47,7 +47,6 @@ paths:
in: header
description: Cache entry namespace
allowEmptyValue: true
required: true
schema:
type: string
description: Cache entry namespace
......@@ -57,7 +56,6 @@ paths:
in: header
description: Cache entry scope
allowEmptyValue: true
required: true
schema:
type: string
description: Cache entry scope
......@@ -94,7 +92,6 @@ paths:
in: header
description: Cache entry namespace
allowEmptyValue: true
required: true
schema:
type: string
description: Cache entry namespace
......@@ -104,7 +101,6 @@ paths:
in: header
description: Cache entry scope
allowEmptyValue: true
required: true
schema:
type: string
description: Cache entry scope
......
......@@ -6,6 +6,7 @@ require (
code.vereign.com/gaiax/tsa/golib v0.0.0-20220321093827-5fdf8f34aad9
github.com/go-redis/redis/v8 v8.11.5
github.com/kelseyhightower/envconfig v1.4.0
github.com/stretchr/testify v1.7.0
go.uber.org/zap v1.21.0
goa.design/goa/v3 v3.7.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
......@@ -13,6 +14,7 @@ require (
require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 // indirect
github.com/dimfeld/httptreemux/v5 v5.4.0 // indirect
......@@ -21,6 +23,7 @@ require (
github.com/gorilla/websocket v1.5.0 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/smartystreets/assertions v1.2.1 // indirect
github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea // indirect
......@@ -31,4 +34,5 @@ require (
golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
// Code generated by counterfeiter. DO NOT EDIT.
package cachefakes
import (
"context"
"sync"
"time"
"code.vereign.com/gaiax/tsa/cache/internal/service/cache"
)
type FakeCache struct {
GetStub func(context.Context, string) ([]byte, error)
getMutex sync.RWMutex
getArgsForCall []struct {
arg1 context.Context
arg2 string
}
getReturns struct {
result1 []byte
result2 error
}
getReturnsOnCall map[int]struct {
result1 []byte
result2 error
}
SetStub func(context.Context, string, []byte, time.Duration) error
setMutex sync.RWMutex
setArgsForCall []struct {
arg1 context.Context
arg2 string
arg3 []byte
arg4 time.Duration
}
setReturns struct {
result1 error
}
setReturnsOnCall map[int]struct {
result1 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeCache) Get(arg1 context.Context, arg2 string) ([]byte, error) {
fake.getMutex.Lock()
ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)]
fake.getArgsForCall = append(fake.getArgsForCall, struct {
arg1 context.Context
arg2 string
}{arg1, arg2})
stub := fake.GetStub
fakeReturns := fake.getReturns
fake.recordInvocation("Get", []interface{}{arg1, arg2})
fake.getMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeCache) GetCallCount() int {
fake.getMutex.RLock()
defer fake.getMutex.RUnlock()
return len(fake.getArgsForCall)
}
func (fake *FakeCache) GetCalls(stub func(context.Context, string) ([]byte, error)) {
fake.getMutex.Lock()
defer fake.getMutex.Unlock()
fake.GetStub = stub
}
func (fake *FakeCache) GetArgsForCall(i int) (context.Context, string) {
fake.getMutex.RLock()
defer fake.getMutex.RUnlock()
argsForCall := fake.getArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeCache) GetReturns(result1 []byte, result2 error) {
fake.getMutex.Lock()
defer fake.getMutex.Unlock()
fake.GetStub = nil
fake.getReturns = struct {
result1 []byte
result2 error
}{result1, result2}
}
func (fake *FakeCache) GetReturnsOnCall(i int, result1 []byte, result2 error) {
fake.getMutex.Lock()
defer fake.getMutex.Unlock()
fake.GetStub = nil
if fake.getReturnsOnCall == nil {
fake.getReturnsOnCall = make(map[int]struct {
result1 []byte
result2 error
})
}
fake.getReturnsOnCall[i] = struct {
result1 []byte
result2 error
}{result1, result2}
}
func (fake *FakeCache) Set(arg1 context.Context, arg2 string, arg3 []byte, arg4 time.Duration) error {
var arg3Copy []byte
if arg3 != nil {
arg3Copy = make([]byte, len(arg3))
copy(arg3Copy, arg3)
}
fake.setMutex.Lock()
ret, specificReturn := fake.setReturnsOnCall[len(fake.setArgsForCall)]
fake.setArgsForCall = append(fake.setArgsForCall, struct {
arg1 context.Context
arg2 string
arg3 []byte
arg4 time.Duration
}{arg1, arg2, arg3Copy, arg4})
stub := fake.SetStub
fakeReturns := fake.setReturns
fake.recordInvocation("Set", []interface{}{arg1, arg2, arg3Copy, arg4})
fake.setMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3, arg4)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeCache) SetCallCount() int {
fake.setMutex.RLock()
defer fake.setMutex.RUnlock()
return len(fake.setArgsForCall)
}
func (fake *FakeCache) SetCalls(stub func(context.Context, string, []byte, time.Duration) error) {
fake.setMutex.Lock()
defer fake.setMutex.Unlock()
fake.SetStub = stub
}
func (fake *FakeCache) SetArgsForCall(i int) (context.Context, string, []byte, time.Duration) {
fake.setMutex.RLock()
defer fake.setMutex.RUnlock()
argsForCall := fake.setArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4
}
func (fake *FakeCache) SetReturns(result1 error) {
fake.setMutex.Lock()
defer fake.setMutex.Unlock()
fake.SetStub = nil
fake.setReturns = struct {
result1 error
}{result1}
}
func (fake *FakeCache) SetReturnsOnCall(i int, result1 error) {
fake.setMutex.Lock()
defer fake.setMutex.Unlock()
fake.SetStub = nil
if fake.setReturnsOnCall == nil {
fake.setReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.setReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeCache) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.getMutex.RLock()
defer fake.getMutex.RUnlock()
fake.setMutex.RLock()
defer fake.setMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeCache) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ cache.Cache = new(FakeCache)
......@@ -3,7 +3,6 @@ package cache
import (
"context"
"encoding/json"
"fmt"
"time"
"go.uber.org/zap"
......@@ -12,6 +11,8 @@ import (
"code.vereign.com/gaiax/tsa/golib/errors"
)
//go:generate counterfeiter . Cache
type Cache interface {
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
......@@ -30,17 +31,26 @@ func New(cache Cache, logger *zap.Logger) *Service {
}
func (s *Service) Get(ctx context.Context, req *cache.CacheGetRequest) (interface{}, error) {
var operation = zap.String("operation", "get")
if req.Key == "" || req.Namespace == "" || req.Scope == "" {
s.logger.Error("bad request: missing key or namespace or scopes", operation)
return nil, errors.New(errors.BadRequest, "bad request")
logger := s.logger.With(zap.String("operation", "get"))
if req.Key == "" {
logger.Error("bad request: missing key")
return nil, errors.New(errors.BadRequest, "missing key")
}
var namespace, scope string
if req.Namespace != nil {
namespace = *req.Namespace
}
if req.Scope != nil {
scope = *req.Scope
}
// create key from the input fields
key := makeCacheKey(req.Namespace, req.Scope, req.Key)
key := makeCacheKey(req.Key, namespace, scope)
data, err := s.cache.Get(ctx, key)
if err != nil {
s.logger.Error("error getting value from cache", zap.String("key", key), zap.Error(err))
logger.Error("error getting value from cache", zap.String("key", key), zap.Error(err))
if errors.Is(errors.NotFound, err) {
return nil, errors.New("key not found in cache", err)
}
......@@ -49,7 +59,7 @@ func (s *Service) Get(ctx context.Context, req *cache.CacheGetRequest) (interfac
var decodedValue interface{}
if err := json.Unmarshal(data, &decodedValue); err != nil {
s.logger.Error("cannot decode json value from cache", zap.Error(err))
logger.Error("cannot decode json value from cache", zap.Error(err))
return nil, errors.New("cannot decode json value from cache", err)
}
......@@ -57,32 +67,47 @@ func (s *Service) Get(ctx context.Context, req *cache.CacheGetRequest) (interfac
}
func (s *Service) Set(ctx context.Context, req *cache.CacheSetRequest) error {
var operation = zap.String("operation", "set")
logger := s.logger.With(zap.String("operation", "set"))
if req.Key == "" || req.Namespace == "" || req.Scope == "" {
s.logger.Error("bad request: missing key or namespace or scope or data", operation)
return errors.New(errors.BadRequest, "bad request")
if req.Key == "" {
logger.Error("bad request: missing key")
return errors.New(errors.BadRequest, "missing key")
}
var namespace, scope string
if req.Namespace != nil {
namespace = *req.Namespace
}
if req.Scope != nil {
scope = *req.Scope
}
// TODO(kinkov): issue #3 - evaluate key metadata (key, namespace and scope) and set TTL over a policy execution
// create key from the input fields
key := makeCacheKey(req.Namespace, req.Scope, req.Key)
key := makeCacheKey(req.Key, namespace, scope)
// encode payload to json bytes for storing in cache
value, err := json.Marshal(req.Data)
if err != nil {
s.logger.Error("error encode payload to json", zap.Error(err), operation)
return errors.New("error encode payload to json", err)
logger.Error("error encode payload to json", zap.Error(err))
return errors.New(errors.BadRequest, "cannot encode payload to json", err)
}
if err := s.cache.Set(ctx, key, value, 0); err != nil {
s.logger.Error("error setting value in cache", zap.Error(err), operation)
return errors.New("error setting value in cache", err)
logger.Error("error storing value in cache", zap.Error(err))
return errors.New("error storing value in cache", err)
}
return nil
}
func makeCacheKey(namespace, scope, key string) string {
return fmt.Sprintf("%s,%s,%s", namespace, scope, key)
func makeCacheKey(key, namespace, scope string) string {
k := key
if namespace != "" {
k += "," + namespace
}
if scope != "" {
k += "," + scope
}
return k
}
package cache_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
goacache "code.vereign.com/gaiax/tsa/cache/gen/cache"
"code.vereign.com/gaiax/tsa/cache/internal/service/cache"
"code.vereign.com/gaiax/tsa/cache/internal/service/cache/cachefakes"
"code.vereign.com/gaiax/tsa/golib/errors"
"code.vereign.com/gaiax/tsa/golib/ptr"
)
func TestNew(t *testing.T) {
svc := cache.New(nil, zap.NewNop())
assert.Implements(t, (*goacache.Service)(nil), svc)
}
func TestService_Get(t *testing.T) {
tests := []struct {
name string
cache *cachefakes.FakeCache
req *goacache.CacheGetRequest
res interface{}
errkind errors.Kind
errtext string
}{
{
name: "missing cache key",
req: &goacache.CacheGetRequest{},
errkind: errors.BadRequest,
errtext: "missing key",
},
{
name: "error getting value from cache",
req: &goacache.CacheGetRequest{
Key: "key",
Namespace: ptr.String("namespace"),
Scope: ptr.String("scope"),
},
cache: &cachefakes.FakeCache{
GetStub: func(ctx context.Context, key string) ([]byte, error) {
return nil, errors.New("some error")
},
},
errkind: errors.Unknown,
errtext: "some error",
},
{
name: "key not found in cache",
req: &goacache.CacheGetRequest{
Key: "key",
Namespace: ptr.String("namespace"),
Scope: ptr.String("scope"),
},
cache: &cachefakes.FakeCache{
GetStub: func(ctx context.Context, key string) ([]byte, error) {
return nil, errors.New(errors.NotFound)
},
},
errkind: errors.NotFound,
errtext: "key not found in cache",
},
{
name: "value returned from cache is not json",
req: &goacache.CacheGetRequest{
Key: "key",
Namespace: ptr.String("namespace"),
Scope: ptr.String("scope"),
},
cache: &cachefakes.FakeCache{
GetStub: func(ctx context.Context, key string) ([]byte, error) {
return []byte("boom"), nil
},
},
errkind: errors.Unknown,
errtext: "cannot decode json value from cache",
},
{
name: "json value is successfully returned from cache",
req: &goacache.CacheGetRequest{
Key: "key",
Namespace: ptr.String("namespace"),
Scope: ptr.String("scope"),
},
cache: &cachefakes.FakeCache{
GetStub: func(ctx context.Context, key string) ([]byte, error) {
return []byte(`{"test":"value"}`), nil
},
},
res: map[string]interface{}{"test": "value"},
errtext: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
svc := cache.New(test.cache, zap.NewNop())
res, err := svc.Get(context.Background(), test.req)
if err == nil {
assert.Empty(t, test.errtext)
assert.Equal(t, test.res, res)
} else {
assert.Nil(t, res)
assert.Error(t, err)
e, ok := err.(*errors.Error)
assert.True(t, ok)
assert.Equal(t, test.errkind, e.Kind)
assert.Contains(t, e.Error(), test.errtext)
}
})
}
}
func TestService_Set(t *testing.T) {
tests := []struct {
name string
cache *cachefakes.FakeCache
req *goacache.CacheSetRequest
res interface{}
errkind errors.Kind
errtext string
}{
{
name: "missing cache key",
req: &goacache.CacheSetRequest{},
errkind: errors.BadRequest,
errtext: "missing key",
},
{
name: "error setting value in cache",
req: &goacache.CacheSetRequest{
Key: "key",
Namespace: ptr.String("namespace"),
Scope: ptr.String("scope"),
Data: map[string]interface{}{"test": "value"},
},
cache: &cachefakes.FakeCache{
SetStub: func(ctx context.Context, key string, value []byte, ttl time.Duration) error {
return errors.New(errors.Timeout, "some error")
},
},
errkind: errors.Timeout,
errtext: "some error",
},
{
name: "successfully set value in cache",
req: &goacache.CacheSetRequest{
Key: "key",
Namespace: ptr.String("namespace"),
Scope: ptr.String("scope"),
Data: map[string]interface{}{"test": "value"},
},
cache: &cachefakes.FakeCache{
SetStub: func(ctx context.Context, key string, value []byte, ttl time.Duration) error {
return nil
},
},
errtext: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
svc := cache.New(test.cache, zap.NewNop())
err := svc.Set(context.Background(), test.req)
if err == nil {
assert.Empty(t, test.errtext)
} else {
assert.Error(t, err)
e, ok := err.(*errors.Error)
assert.True(t, ok)
assert.Equal(t, test.errkind, e.Kind)
assert.Contains(t, e.Error(), test.errtext)
}
})
}
}
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment