diff --git a/README.md b/README.md index be574f982993bc63ce2f5848ca2b536fbabdb77e..c128eb832b764806b929ed71930a42a8aaf3ee47 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,53 @@ -# Cache +# Cache service -Cache exposes HTTP interface for working with Redis. \ No newline at end of file +Cache service exposes HTTP interface for working with Redis. + +### Basic Architecture + +```mermaid +flowchart LR + A[Client] -- request --> B[Cache Service] --> C[(Redis)] +``` + +### API Documentation + +The API Documentation is accessible at `/swagger-ui` path in OAS 3.0 format. +Example: `http://localhost:8080/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. + +Example: +``` +REDIS_ADDR="localhost:6379" +REDIS_USER="user" +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. + +### Dependencies and Vendor + +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 +go mod tidy +go mod vendor +``` + +### Tests and Linters + +To execute the units tests for the service go to the root project directory and run: +```go +go test -race ./... +``` + +To run the linters go to the root project directory and run: +```go +golangci-lint run +``` diff --git a/design/design.go b/design/design.go index 0617832a38b93fa17411bdfbb53bd60df6dd43c0..df4c8c84acb64044fe38b34e5487573297d43302 100644 --- a/design/design.go +++ b/design/design.go @@ -41,10 +41,10 @@ var _ = Service("cache", func() { Description("Cache service allows storing and retrieving data from distributed cache.") Method("Get", func() { - Description("Get value from the cache. The result is a sequence of bytes which the client must decode.") + Description("Get JSON value from the cache.") Payload(CacheGetRequest) - Result(Bytes) + Result(Any) HTTP(func() { GET("/v1/cache") @@ -59,7 +59,33 @@ var _ = Service("cache", func() { Example("administration") }) - Response(StatusOK) + Response(StatusOK, func() { + ContentType("application/json") + }) + }) + }) + + Method("Set", func() { + Description("Set a JSON value in the cache.") + + Payload(CacheSetRequest) + Result(Empty) + + HTTP(func() { + POST("/v1/cache") + + Header("key:x-cache-key", String, "Cache entry key", func() { + Example("did:web:example.com") + }) + Header("namespace:x-cache-namespace", String, "Cache entry namespace", func() { + Example("Login") + }) + Header("scope:x-cache-scope", String, "Cache entry scope", func() { + Example("administration") + }) + Body("data") + + Response(StatusCreated) }) }) }) diff --git a/design/types.go b/design/types.go index 57789a4b12ab9bf0bfb4dd467668d950e784e2a6..3ea641e275ca9fed03207bd7d3e052865ae005c3 100644 --- a/design/types.go +++ b/design/types.go @@ -9,3 +9,11 @@ var CacheGetRequest = Type("CacheGetRequest", func() { Field(3, "scope", String) // Initial implementation with a single scope Required("key", "namespace", "scope") }) + +var CacheSetRequest = Type("CacheSetRequest", func() { + Field(1, "data", Any) + Field(2, "key", String) + Field(3, "namespace", String) + Field(4, "scope", String) // Initial implementation with a single scope + Required("data", "key", "namespace", "scope") +}) diff --git a/gen/cache/client.go b/gen/cache/client.go index ec6218557a4ad5ef34f1a76161238b1105677603..d076c91f9ce19b9aa45fb2e4abeaa45e808603c3 100644 --- a/gen/cache/client.go +++ b/gen/cache/client.go @@ -16,21 +16,29 @@ import ( // Client is the "cache" service client. type Client struct { GetEndpoint goa.Endpoint + SetEndpoint goa.Endpoint } // NewClient initializes a "cache" service client given the endpoints. -func NewClient(get goa.Endpoint) *Client { +func NewClient(get, set goa.Endpoint) *Client { return &Client{ GetEndpoint: get, + SetEndpoint: set, } } // Get calls the "Get" endpoint of the "cache" service. -func (c *Client) Get(ctx context.Context, p *CacheGetRequest) (res []byte, err error) { +func (c *Client) Get(ctx context.Context, p *CacheGetRequest) (res interface{}, err error) { var ires interface{} ires, err = c.GetEndpoint(ctx, p) if err != nil { return } - return ires.([]byte), nil + return ires.(interface{}), nil +} + +// Set calls the "Set" endpoint of the "cache" service. +func (c *Client) Set(ctx context.Context, p *CacheSetRequest) (err error) { + _, err = c.SetEndpoint(ctx, p) + return } diff --git a/gen/cache/endpoints.go b/gen/cache/endpoints.go index 4dfb7f41650a6bc82b6317a2aea05fba34d967b2..c3d44cb790448204680ba0f060701a6bfffede4c 100644 --- a/gen/cache/endpoints.go +++ b/gen/cache/endpoints.go @@ -16,18 +16,21 @@ import ( // Endpoints wraps the "cache" service endpoints. type Endpoints struct { Get goa.Endpoint + Set goa.Endpoint } // NewEndpoints wraps the methods of the "cache" service with endpoints. func NewEndpoints(s Service) *Endpoints { return &Endpoints{ Get: NewGetEndpoint(s), + Set: NewSetEndpoint(s), } } // Use applies the given middleware to all the "cache" service endpoints. func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) { e.Get = m(e.Get) + e.Set = m(e.Set) } // NewGetEndpoint returns an endpoint function that calls the method "Get" of @@ -38,3 +41,12 @@ func NewGetEndpoint(s Service) goa.Endpoint { return s.Get(ctx, p) } } + +// NewSetEndpoint returns an endpoint function that calls the method "Set" of +// service "cache". +func NewSetEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req interface{}) (interface{}, error) { + p := req.(*CacheSetRequest) + return nil, s.Set(ctx, p) + } +} diff --git a/gen/cache/service.go b/gen/cache/service.go index 089bd94d2623488893a2a0a9a7c2763957b84a0b..ec2b3af1d8b783c03c9ab4e4416a0d1eccac9b28 100644 --- a/gen/cache/service.go +++ b/gen/cache/service.go @@ -13,9 +13,10 @@ import ( // Cache service allows storing and retrieving data from distributed cache. type Service interface { - // Get value from the cache. The result is a sequence of bytes which the client - // must decode. - Get(context.Context, *CacheGetRequest) (res []byte, err error) + // Get JSON value from the cache. + Get(context.Context, *CacheGetRequest) (res interface{}, err error) + // Set a JSON value in the cache. + Set(context.Context, *CacheSetRequest) (err error) } // ServiceName is the name of the service as defined in the design. This is the @@ -26,7 +27,7 @@ const ServiceName = "cache" // MethodNames lists the service method names as defined in the design. These // are the same values that are set in the endpoint request contexts under the // MethodKey key. -var MethodNames = [1]string{"Get"} +var MethodNames = [2]string{"Get", "Set"} // CacheGetRequest is the payload type of the cache service Get method. type CacheGetRequest struct { @@ -34,3 +35,11 @@ type CacheGetRequest struct { 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 +} diff --git a/gen/http/cache/client/cli.go b/gen/http/cache/client/cli.go index 102424b710f74365e6125b64830218de30980a04..6502a89ae3ee866ac1389db01de6fa73d87081c5 100644 --- a/gen/http/cache/client/cli.go +++ b/gen/http/cache/client/cli.go @@ -8,6 +8,9 @@ package client import ( + "encoding/json" + "fmt" + cache "code.vereign.com/gaiax/tsa/cache/gen/cache" ) @@ -32,3 +35,36 @@ func BuildGetPayload(cacheGetKey string, cacheGetNamespace string, cacheGetScope return v, nil } + +// BuildSetPayload builds the payload for the cache Set endpoint from CLI flags. +func BuildSetPayload(cacheSetBody string, cacheSetKey string, cacheSetNamespace string, cacheSetScope string) (*cache.CacheSetRequest, error) { + var err error + var body interface{} + { + err = json.Unmarshal([]byte(cacheSetBody), &body) + if err != nil { + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "\"Quasi ut.\"") + } + } + var key string + { + key = cacheSetKey + } + var namespace string + { + namespace = cacheSetNamespace + } + var scope string + { + scope = cacheSetScope + } + v := body + res := &cache.CacheSetRequest{ + Data: v, + } + res.Key = key + res.Namespace = namespace + res.Scope = scope + + return res, nil +} diff --git a/gen/http/cache/client/client.go b/gen/http/cache/client/client.go index a909cf4774e21ad7b49f24881b0b1e2d4321f37b..f813e4412a506fd9efed09bd9b534d490a5b8576 100644 --- a/gen/http/cache/client/client.go +++ b/gen/http/cache/client/client.go @@ -20,6 +20,9 @@ type Client struct { // Get Doer is the HTTP client used to make requests to the Get endpoint. GetDoer goahttp.Doer + // Set Doer is the HTTP client used to make requests to the Set endpoint. + SetDoer goahttp.Doer + // RestoreResponseBody controls whether the response bodies are reset after // decoding so they can be read again. RestoreResponseBody bool @@ -41,6 +44,7 @@ func NewClient( ) *Client { return &Client{ GetDoer: doer, + SetDoer: doer, RestoreResponseBody: restoreBody, scheme: scheme, host: host, @@ -72,3 +76,27 @@ func (c *Client) Get() goa.Endpoint { return decodeResponse(resp) } } + +// Set returns an endpoint that makes HTTP requests to the cache service Set +// server. +func (c *Client) Set() goa.Endpoint { + var ( + encodeRequest = EncodeSetRequest(c.encoder) + decodeResponse = DecodeSetResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v interface{}) (interface{}, error) { + req, err := c.BuildSetRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.SetDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("cache", "Set", err) + } + return decodeResponse(resp) + } +} diff --git a/gen/http/cache/client/encode_decode.go b/gen/http/cache/client/encode_decode.go index dd4550d9b4b97277842a3b9a182c872796d6ed01..be26cb474f49755658db16e76b03237c882009ed 100644 --- a/gen/http/cache/client/encode_decode.go +++ b/gen/http/cache/client/encode_decode.go @@ -77,7 +77,7 @@ func DecodeGetResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody switch resp.StatusCode { case http.StatusOK: var ( - body []byte + body interface{} err error ) err = decoder(resp).Decode(&body) @@ -91,3 +91,73 @@ func DecodeGetResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody } } } + +// BuildSetRequest instantiates a HTTP request object with method and path set +// to call the "cache" service "Set" endpoint +func (c *Client) BuildSetRequest(ctx context.Context, v interface{}) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: SetCachePath()} + req, err := http.NewRequest("POST", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("cache", "Set", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeSetRequest returns an encoder for requests sent to the cache Set +// server. +func EncodeSetRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, interface{}) error { + return func(req *http.Request, v interface{}) error { + p, ok := v.(*cache.CacheSetRequest) + if !ok { + return goahttp.ErrInvalidType("cache", "Set", "*cache.CacheSetRequest", v) + } + { + head := p.Key + req.Header.Set("x-cache-key", head) + } + { + head := p.Namespace + req.Header.Set("x-cache-namespace", head) + } + { + head := p.Scope + req.Header.Set("x-cache-scope", head) + } + body := p.Data + if err := encoder(req).Encode(&body); err != nil { + return goahttp.ErrEncodingError("cache", "Set", err) + } + return nil + } +} + +// DecodeSetResponse returns a decoder for responses returned by the cache Set +// endpoint. restoreBody controls whether the response body should be restored +// after having been read. +func DecodeSetResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (interface{}, error) { + return func(resp *http.Response) (interface{}, error) { + if restoreBody { + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = ioutil.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = ioutil.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusCreated: + return nil, nil + default: + body, _ := ioutil.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("cache", "Set", resp.StatusCode, string(body)) + } + } +} diff --git a/gen/http/cache/client/paths.go b/gen/http/cache/client/paths.go index d4e85ee6fa5552f06361e65105ef5bea446e5753..90588e4bae0c42a9b125238089f4519ae1b13a5f 100644 --- a/gen/http/cache/client/paths.go +++ b/gen/http/cache/client/paths.go @@ -11,3 +11,8 @@ package client func GetCachePath() string { return "/v1/cache" } + +// SetCachePath returns the URL path to the cache service Set HTTP endpoint. +func SetCachePath() string { + return "/v1/cache" +} diff --git a/gen/http/cache/server/encode_decode.go b/gen/http/cache/server/encode_decode.go index 6e8629d2e7494d5e077d13533ad55a83d966660f..b6587e807c913d0cd21fc1001df3baddd7353f34 100644 --- a/gen/http/cache/server/encode_decode.go +++ b/gen/http/cache/server/encode_decode.go @@ -9,6 +9,7 @@ package server import ( "context" + "io" "net/http" goahttp "goa.design/goa/v3/http" @@ -19,7 +20,8 @@ import ( // endpoint. func EncodeGetResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, interface{}) error { return func(ctx context.Context, w http.ResponseWriter, v interface{}) error { - res, _ := v.([]byte) + res, _ := v.(interface{}) + ctx = context.WithValue(ctx, goahttp.ContentTypeKey, "application/json") enc := encoder(ctx, w) body := res w.WriteHeader(http.StatusOK) @@ -57,3 +59,54 @@ func DecodeGetRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Dec return payload, nil } } + +// EncodeSetResponse returns an encoder for responses returned by the cache Set +// endpoint. +func EncodeSetResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, interface{}) error { + return func(ctx context.Context, w http.ResponseWriter, v interface{}) error { + w.WriteHeader(http.StatusCreated) + return nil + } +} + +// DecodeSetRequest returns a decoder for requests sent to the cache Set +// endpoint. +func DecodeSetRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (interface{}, error) { + return func(r *http.Request) (interface{}, error) { + var ( + body interface{} + err error + ) + err = decoder(r).Decode(&body) + if err != nil { + if err == io.EOF { + return nil, goa.MissingPayloadError() + } + return nil, goa.DecodePayloadError(err.Error()) + } + + var ( + key 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")) + } + scope = r.Header.Get("x-cache-scope") + if scope == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("x-cache-scope", "header")) + } + if err != nil { + return nil, err + } + payload := NewSetCacheSetRequest(body, key, namespace, scope) + + return payload, nil + } +} diff --git a/gen/http/cache/server/paths.go b/gen/http/cache/server/paths.go index 656155750cbb8e8932366cf94bde88f3d5e97d6f..31e15617209f7986fb54b5cf611ad1ed57c8d457 100644 --- a/gen/http/cache/server/paths.go +++ b/gen/http/cache/server/paths.go @@ -11,3 +11,8 @@ package server func GetCachePath() string { return "/v1/cache" } + +// SetCachePath returns the URL path to the cache service Set HTTP endpoint. +func SetCachePath() string { + return "/v1/cache" +} diff --git a/gen/http/cache/server/server.go b/gen/http/cache/server/server.go index 63e8e6f0139c65f080770c08f884366d149a90b3..dd65640c9087f7a8309277bd5933ee1e75f42f4f 100644 --- a/gen/http/cache/server/server.go +++ b/gen/http/cache/server/server.go @@ -20,6 +20,7 @@ import ( type Server struct { Mounts []*MountPoint Get http.Handler + Set http.Handler } // ErrorNamer is an interface implemented by generated error structs that @@ -56,8 +57,10 @@ func New( return &Server{ Mounts: []*MountPoint{ {"Get", "GET", "/v1/cache"}, + {"Set", "POST", "/v1/cache"}, }, Get: NewGetHandler(e.Get, mux, decoder, encoder, errhandler, formatter), + Set: NewSetHandler(e.Set, mux, decoder, encoder, errhandler, formatter), } } @@ -67,11 +70,13 @@ func (s *Server) Service() string { return "cache" } // Use wraps the server handlers with the given middleware. func (s *Server) Use(m func(http.Handler) http.Handler) { s.Get = m(s.Get) + s.Set = m(s.Set) } // Mount configures the mux to serve the cache endpoints. func Mount(mux goahttp.Muxer, h *Server) { MountGetHandler(mux, h.Get) + MountSetHandler(mux, h.Set) } // Mount configures the mux to serve the cache endpoints. @@ -129,3 +134,54 @@ func NewGetHandler( } }) } + +// MountSetHandler configures the mux to serve the "cache" service "Set" +// endpoint. +func MountSetHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("POST", "/v1/cache", f) +} + +// NewSetHandler creates a HTTP handler which loads the HTTP request and calls +// the "cache" service "Set" endpoint. +func NewSetHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeSetRequest(mux, decoder) + encodeResponse = EncodeSetResponse(encoder) + encodeError = goahttp.ErrorEncoder(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "Set") + ctx = context.WithValue(ctx, goa.ServiceKey, "cache") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} diff --git a/gen/http/cache/server/types.go b/gen/http/cache/server/types.go index 4585d80628f45910bd241d0caacce974fb9766fd..c9d87f134992134625b260b82a3ceee77e0a0ee6 100644 --- a/gen/http/cache/server/types.go +++ b/gen/http/cache/server/types.go @@ -20,3 +20,16 @@ func NewGetCacheGetRequest(key string, namespace string, scope string) *cache.Ca return v } + +// NewSetCacheSetRequest builds a cache service Set endpoint payload. +func NewSetCacheSetRequest(body interface{}, key string, namespace string, scope string) *cache.CacheSetRequest { + v := body + res := &cache.CacheSetRequest{ + Data: v, + } + res.Key = key + res.Namespace = namespace + res.Scope = scope + + return res +} diff --git a/gen/http/cli/cache/cli.go b/gen/http/cli/cache/cli.go index 2bfecddeacdbcbb0bd07454c0527db1669dc680a..007ace38ca77de84e7e57b6d6a23f783ef89adbe 100644 --- a/gen/http/cli/cache/cli.go +++ b/gen/http/cli/cache/cli.go @@ -25,14 +25,14 @@ import ( // func UsageCommands() string { return `health (liveness|readiness) -cache get +cache (get|set) ` } // UsageExamples produces an example of a valid invocation of the CLI tool. func UsageExamples() string { return os.Args[0] + ` health liveness` + "\n" + - os.Args[0] + ` cache get --key "Officiis fuga architecto cum." --namespace "Sint adipisci." --scope "Debitis id reprehenderit incidunt necessitatibus assumenda."` + "\n" + + os.Args[0] + ` cache get --key "Suscipit sed consequatur rerum occaecati in veritatis." --namespace "Repudiandae eos consequatur sint dolorum occaecati." --scope "Voluptatum modi tenetur tempore quia est ratione."` + "\n" + "" } @@ -58,6 +58,12 @@ func ParseEndpoint( cacheGetKeyFlag = cacheGetFlags.String("key", "REQUIRED", "") cacheGetNamespaceFlag = cacheGetFlags.String("namespace", "REQUIRED", "") cacheGetScopeFlag = cacheGetFlags.String("scope", "REQUIRED", "") + + 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", "") ) healthFlags.Usage = healthUsage healthLivenessFlags.Usage = healthLivenessUsage @@ -65,6 +71,7 @@ func ParseEndpoint( cacheFlags.Usage = cacheUsage cacheGetFlags.Usage = cacheGetUsage + cacheSetFlags.Usage = cacheSetUsage if err := flag.CommandLine.Parse(os.Args[1:]); err != nil { return nil, nil, err @@ -115,6 +122,9 @@ func ParseEndpoint( case "get": epf = cacheGetFlags + case "set": + epf = cacheSetFlags + } } @@ -153,6 +163,9 @@ func ParseEndpoint( case "get": endpoint = c.Get() data, err = cachec.BuildGetPayload(*cacheGetKeyFlag, *cacheGetNamespaceFlag, *cacheGetScopeFlag) + case "set": + endpoint = c.Set() + data, err = cachec.BuildSetPayload(*cacheSetBodyFlag, *cacheSetKeyFlag, *cacheSetNamespaceFlag, *cacheSetScopeFlag) } } } @@ -204,7 +217,8 @@ Usage: %[1]s [globalflags] cache COMMAND [flags] COMMAND: - get: Get value from the cache. The result is a sequence of bytes which the client must decode. + get: Get JSON value from the cache. + set: Set a JSON value in the cache. Additional help: %[1]s cache COMMAND --help @@ -213,12 +227,26 @@ Additional help: func cacheGetUsage() { fmt.Fprintf(os.Stderr, `%[1]s [flags] cache get -key STRING -namespace STRING -scope STRING -Get value from the cache. The result is a sequence of bytes which the client must decode. +Get JSON value from the cache. + -key STRING: + -namespace STRING: + -scope STRING: + +Example: + %[1]s cache get --key "Suscipit sed consequatur rerum occaecati in veritatis." --namespace "Repudiandae eos consequatur sint dolorum occaecati." --scope "Voluptatum modi tenetur tempore quia est ratione." +`, os.Args[0]) +} + +func cacheSetUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] cache set -body JSON -key STRING -namespace STRING -scope STRING + +Set a JSON value in the cache. + -body JSON: -key STRING: -namespace STRING: -scope STRING: Example: - %[1]s cache get --key "Officiis fuga architecto cum." --namespace "Sint adipisci." --scope "Debitis id reprehenderit incidunt necessitatibus assumenda." + %[1]s cache set --body "Quasi ut." --key "Quasi perspiciatis." --namespace "Accusantium animi non alias." --scope "Esse inventore ullam placeat aut." `, os.Args[0]) } diff --git a/gen/http/openapi.json b/gen/http/openapi.json index b77a470aa4c05f88e90ef116416482555f7a5c90..b9d430311b5a1b01b8f6f061e08c090c3e191545 100644 --- a/gen/http/openapi.json +++ b/gen/http/openapi.json @@ -1 +1 @@ -{"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 value from the cache. The result is a sequence of bytes which the client must decode.","operationId":"cache#Get","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":"byte"}}},"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":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 diff --git a/gen/http/openapi.yaml b/gen/http/openapi.yaml index 5043265f7fa571f22f8a8f96b303296e161d0cdd..b1bac2cdadd957be492a9618414d12516397b6b2 100644 --- a/gen/http/openapi.yaml +++ b/gen/http/openapi.yaml @@ -40,9 +40,10 @@ paths: tags: - cache summary: Get cache - description: Get value from the cache. The result is a sequence of bytes which - the client must decode. + description: Get JSON value from the cache. operationId: cache#Get + produces: + - application/json parameters: - name: x-cache-key in: header @@ -64,6 +65,39 @@ paths: description: OK response. schema: type: string - format: byte + 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 diff --git a/gen/http/openapi3.json b/gen/http/openapi3.json index be3f9a4d1f1018196f1bb8c204da2f91a9434473..0ce517d7b5de2092ed45e6bb357aa14c8556a7c9 100644 --- a/gen/http/openapi3.json +++ b/gen/http/openapi3.json @@ -1 +1 @@ -{"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 value from the cache. The result is a sequence of bytes which the client must decode.","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":"UmVwdWRpYW5kYWUgZW9zIGNvbnNlcXVhdHVyIHNpbnQgZG9sb3J1bSBvY2NhZWNhdGku","format":"binary"},"example":"Vm9sdXB0YXR1bSBtb2RpIHRlbmV0dXIgdGVtcG9yZSBxdWlhIGVzdCByYXRpb25lLg=="}}}}}}},"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,"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 diff --git a/gen/http/openapi3.yaml b/gen/http/openapi3.yaml index 7fa181f0598cfd836cc793396662672f5d9f5776..b936940cde419698a7133a062edd0436c1650930 100644 --- a/gen/http/openapi3.yaml +++ b/gen/http/openapi3.yaml @@ -30,8 +30,7 @@ paths: tags: - cache summary: Get cache - description: Get value from the cache. The result is a sequence of bytes which - the client must decode. + description: Get JSON value from the cache. operationId: cache#Get parameters: - name: x-cache-key @@ -71,109 +70,58 @@ paths: application/json: schema: type: string - example: - - 82 - - 101 - - 112 - - 117 - - 100 - - 105 - - 97 - - 110 - - 100 - - 97 - - 101 - - 32 - - 101 - - 111 - - 115 - - 32 - - 99 - - 111 - - 110 - - 115 - - 101 - - 113 - - 117 - - 97 - - 116 - - 117 - - 114 - - 32 - - 115 - - 105 - - 110 - - 116 - - 32 - - 100 - - 111 - - 108 - - 111 - - 114 - - 117 - - 109 - - 32 - - 111 - - 99 - - 99 - - 97 - - 101 - - 99 - - 97 - - 116 - - 105 - - 46 + example: Qui iusto enim est dolores dolorem et. format: binary - example: - - 86 - - 111 - - 108 - - 117 - - 112 - - 116 - - 97 - - 116 - - 117 - - 109 - - 32 - - 109 - - 111 - - 100 - - 105 - - 32 - - 116 - - 101 - - 110 - - 101 - - 116 - - 117 - - 114 - - 32 - - 116 - - 101 - - 109 - - 112 - - 111 - - 114 - - 101 - - 32 - - 113 - - 117 - - 105 - - 97 - - 32 - - 101 - - 115 - - 116 - - 32 - - 114 - - 97 - - 116 - - 105 - - 111 - - 110 - - 101 - - 46 + 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 diff --git a/internal/clients/redis/client.go b/internal/clients/redis/client.go index 84d5106ebd417ffbf82945259c2174fb0bb6abdc..0b1f1c993de879d277aa217a2876cb5dd7571375 100644 --- a/internal/clients/redis/client.go +++ b/internal/clients/redis/client.go @@ -41,3 +41,11 @@ func (c *Client) Get(ctx context.Context, key string) ([]byte, error) { } return []byte(result.Val()), nil } + +func (c *Client) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error { + if ttl == 0 { + ttl = c.defaultTTL + } + + return c.rdb.Set(ctx, key, value, ttl).Err() +} diff --git a/internal/service/cache/service.go b/internal/service/cache/service.go index 0e3153a6a3ac0ed6a03720e9b617f22246b38847..4644cc015ed6f44f86ea7dc4b6ead5f73420f384 100644 --- a/internal/service/cache/service.go +++ b/internal/service/cache/service.go @@ -2,7 +2,9 @@ package cache import ( "context" + "encoding/json" "fmt" + "time" "go.uber.org/zap" @@ -12,6 +14,7 @@ import ( type Cache interface { Get(ctx context.Context, key string) ([]byte, error) + Set(ctx context.Context, key string, value []byte, ttl time.Duration) error } type Service struct { @@ -26,7 +29,7 @@ func New(cache Cache, logger *zap.Logger) *Service { } } -func (s *Service) Get(ctx context.Context, req *cache.CacheGetRequest) ([]byte, error) { +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) @@ -34,7 +37,7 @@ func (s *Service) Get(ctx context.Context, req *cache.CacheGetRequest) ([]byte, } // create key from the input fields - key := makeCacheKey(req.Key, req.Namespace, req.Scope) + key := makeCacheKey(req.Namespace, req.Scope, req.Key) 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)) @@ -44,7 +47,40 @@ func (s *Service) Get(ctx context.Context, req *cache.CacheGetRequest) ([]byte, return nil, errors.New("error getting value from cache", err) } - return data, nil + var decodedValue interface{} + if err := json.Unmarshal(data, &decodedValue); err != nil { + s.logger.Error("cannot decode json value from cache", zap.Error(err)) + return nil, errors.New("cannot decode json value from cache", err) + } + + return decodedValue, nil +} + +func (s *Service) Set(ctx context.Context, req *cache.CacheSetRequest) error { + var operation = 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") + } + + // 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) + // 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) + } + + 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) + } + + return nil } func makeCacheKey(namespace, scope, key string) string {