diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7ba775a534d64291955648e24da611f3c5a3cd99..c92e8694f8f984638f7769e9cf29d719ff1b6adf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,7 +14,7 @@ linters: - golangci-lint run unit tests: - image: golang:1.21.3 + image: golang:1.21.5 stage: test script: - go version @@ -23,7 +23,7 @@ unit tests: coverage: '/total:\s+\(statements\)\s+(\d+.\d+\%)/' govulncheck: - image: golang:1.21.3 + image: golang:1.21.5 stage: test script: - go version diff --git a/deployment/ci/Dockerfile b/deployment/ci/Dockerfile index 60f9b8210b56c843e3e73c46004f0ee0c57647ab..346b141a62289f11e1bcedf9da75f140ab43c7d5 100644 --- a/deployment/ci/Dockerfile +++ b/deployment/ci/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21.3-alpine3.17 as builder +FROM golang:1.21.5-alpine3.17 as builder RUN apk add git diff --git a/deployment/compose/Dockerfile b/deployment/compose/Dockerfile index e9d3baeaf73b41a653953d21efab82ca482ce244..b57e8bd82a58c31dd728367293b58ff2186d67f4 100644 --- a/deployment/compose/Dockerfile +++ b/deployment/compose/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21.3 +FROM golang:1.21.5 RUN go install github.com/ysmood/kit/cmd/guard@v0.25.11 diff --git a/design/design.go b/design/design.go index 470b6a6e27c714cd3524267766e900f15f042233..28fc20acae40eba9df4a35c6fab6a451a49aef92 100644 --- a/design/design.go +++ b/design/design.go @@ -56,7 +56,13 @@ var _ = Service("cache", func() { Example("Login") }) Header("scope:x-cache-scope", String, "Cache entry scope", func() { - Example("administration") + Example("multiple scopes", "administration,user") + Example("default", "administration") + }) + Header("strategy:x-cache-flatten-strategy", String, "Flatten strategy.", func() { + Example("first key value only", "first") + Example("last key value only", "last") + Example("default", "merge") }) Response(StatusOK, func() { diff --git a/design/types.go b/design/types.go index eee0825d85ed07a661c979e8a5d8ea51edcc2740..bf8bd71bf9c0baa88a2ee57bcb0bcd53718f0348 100644 --- a/design/types.go +++ b/design/types.go @@ -6,7 +6,8 @@ import . "goa.design/goa/v3/dsl" var CacheGetRequest = Type("CacheGetRequest", func() { Field(1, "key", String) Field(2, "namespace", String) - Field(3, "scope", String) // Initial implementation with a single scope + Field(3, "scope", String) + Field(4, "strategy", String) Required("key") }) diff --git a/gen/cache/service.go b/gen/cache/service.go index 74f41d961927e3fdc1c77b37a1df1f31fd4c37b7..9d0f802dd1dc90f4c44a0fc6db58b06083d0dcd7 100644 --- a/gen/cache/service.go +++ b/gen/cache/service.go @@ -36,6 +36,7 @@ type CacheGetRequest struct { Key string Namespace *string Scope *string + Strategy *string } // CacheSetRequest is the payload type of the cache service Set method. diff --git a/gen/http/cache/client/cli.go b/gen/http/cache/client/cli.go index 4f75bd46a479ad1a576bbd0fac478d8a3298f7ba..31684f60e89eea3f76b436fbd1791e0c66d5437b 100644 --- a/gen/http/cache/client/cli.go +++ b/gen/http/cache/client/cli.go @@ -16,7 +16,7 @@ import ( ) // BuildGetPayload builds the payload for the cache Get endpoint from CLI flags. -func BuildGetPayload(cacheGetKey string, cacheGetNamespace string, cacheGetScope string) (*cache.CacheGetRequest, error) { +func BuildGetPayload(cacheGetKey string, cacheGetNamespace string, cacheGetScope string, cacheGetStrategy string) (*cache.CacheGetRequest, error) { var key string { key = cacheGetKey @@ -33,10 +33,17 @@ func BuildGetPayload(cacheGetKey string, cacheGetNamespace string, cacheGetScope scope = &cacheGetScope } } + var strategy *string + { + if cacheGetStrategy != "" { + strategy = &cacheGetStrategy + } + } v := &cache.CacheGetRequest{} v.Key = key v.Namespace = namespace v.Scope = scope + v.Strategy = strategy return v, nil } @@ -48,7 +55,7 @@ func BuildSetPayload(cacheSetBody string, cacheSetKey string, cacheSetNamespace { 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, "\"Dolorem et quam et illum.\"") + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "\"Enim vel.\"") } } var key string @@ -99,7 +106,7 @@ func BuildSetExternalPayload(cacheSetExternalBody string, cacheSetExternalKey st { err = json.Unmarshal([]byte(cacheSetExternalBody), &body) if err != nil { - return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "\"Quis rerum velit sunt rerum dignissimos at.\"") + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "\"Sint ipsa fugiat et id rem.\"") } } var key string diff --git a/gen/http/cache/client/encode_decode.go b/gen/http/cache/client/encode_decode.go index db80d155308f960f911ccd572d55cac8666449a4..793256a4178a693a2515773d657f701e72d09827 100644 --- a/gen/http/cache/client/encode_decode.go +++ b/gen/http/cache/client/encode_decode.go @@ -54,6 +54,10 @@ func EncodeGetRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Re head := *p.Scope req.Header.Set("x-cache-scope", head) } + if p.Strategy != nil { + head := *p.Strategy + req.Header.Set("x-cache-flatten-strategy", head) + } return nil } } diff --git a/gen/http/cache/server/encode_decode.go b/gen/http/cache/server/encode_decode.go index 44aee8d8ef7da8d78fdaa596d533af1a57edd0d7..f8d56c06ec04bc970610f12da13fe9cbd177c404 100644 --- a/gen/http/cache/server/encode_decode.go +++ b/gen/http/cache/server/encode_decode.go @@ -38,6 +38,7 @@ func DecodeGetRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Dec key string namespace *string scope *string + strategy *string err error ) key = r.Header.Get("x-cache-key") @@ -52,10 +53,14 @@ func DecodeGetRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Dec if scopeRaw != "" { scope = &scopeRaw } + strategyRaw := r.Header.Get("x-cache-flatten-strategy") + if strategyRaw != "" { + strategy = &strategyRaw + } if err != nil { return nil, err } - payload := NewGetCacheGetRequest(key, namespace, scope) + payload := NewGetCacheGetRequest(key, namespace, scope, strategy) return payload, nil } diff --git a/gen/http/cache/server/types.go b/gen/http/cache/server/types.go index 56803f12967d47936b136a77a6500fc4cb48e5bf..1e458783ec6e1f57a977e0de4449c6849f8a0351 100644 --- a/gen/http/cache/server/types.go +++ b/gen/http/cache/server/types.go @@ -12,11 +12,12 @@ 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, strategy *string) *cache.CacheGetRequest { v := &cache.CacheGetRequest{} v.Key = key v.Namespace = namespace v.Scope = scope + v.Strategy = strategy return v } diff --git a/gen/http/cli/cache/cli.go b/gen/http/cli/cache/cli.go index 44fb882cbf558eca3f57bf7e8d0ffe88a836ddaf..518b2114a59d7f17162845ee0c88a43fa3a7f585 100644 --- a/gen/http/cli/cache/cli.go +++ b/gen/http/cli/cache/cli.go @@ -30,7 +30,7 @@ health (liveness|readiness) // UsageExamples produces an example of a valid invocation of the CLI tool. func UsageExamples() string { - return os.Args[0] + ` cache get --key "Voluptatum modi tenetur tempore quia est ratione." --namespace "Corrupti sunt dolores." --scope "Repellat omnis id ex."` + "\n" + + return os.Args[0] + ` cache get --key "Iusto consequatur voluptatem eligendi et eligendi." --namespace "Optio natus." --scope "Ratione quasi perspiciatis qui." --strategy "Animi non alias occaecati esse."` + "\n" + os.Args[0] + ` health liveness` + "\n" + "" } @@ -51,6 +51,7 @@ func ParseEndpoint( cacheGetKeyFlag = cacheGetFlags.String("key", "REQUIRED", "") cacheGetNamespaceFlag = cacheGetFlags.String("namespace", "", "") cacheGetScopeFlag = cacheGetFlags.String("scope", "", "") + cacheGetStrategyFlag = cacheGetFlags.String("strategy", "", "") cacheSetFlags = flag.NewFlagSet("set", flag.ExitOnError) cacheSetBodyFlag = cacheSetFlags.String("body", "REQUIRED", "") @@ -163,7 +164,7 @@ func ParseEndpoint( switch epn { case "get": endpoint = c.Get() - data, err = cachec.BuildGetPayload(*cacheGetKeyFlag, *cacheGetNamespaceFlag, *cacheGetScopeFlag) + data, err = cachec.BuildGetPayload(*cacheGetKeyFlag, *cacheGetNamespaceFlag, *cacheGetScopeFlag, *cacheGetStrategyFlag) case "set": endpoint = c.Set() data, err = cachec.BuildSetPayload(*cacheSetBodyFlag, *cacheSetKeyFlag, *cacheSetNamespaceFlag, *cacheSetScopeFlag, *cacheSetTTLFlag) @@ -206,15 +207,16 @@ Additional help: `, os.Args[0]) } func cacheGetUsage() { - fmt.Fprintf(os.Stderr, `%[1]s [flags] cache get -key STRING -namespace STRING -scope STRING + fmt.Fprintf(os.Stderr, `%[1]s [flags] cache get -key STRING -namespace STRING -scope STRING -strategy STRING Get JSON value from the cache. -key STRING: -namespace STRING: -scope STRING: + -strategy STRING: Example: - %[1]s cache get --key "Voluptatum modi tenetur tempore quia est ratione." --namespace "Corrupti sunt dolores." --scope "Repellat omnis id ex." + %[1]s cache get --key "Iusto consequatur voluptatem eligendi et eligendi." --namespace "Optio natus." --scope "Ratione quasi perspiciatis qui." --strategy "Animi non alias occaecati esse." `, os.Args[0]) } @@ -229,7 +231,7 @@ Set a JSON value in the cache. -ttl INT: Example: - %[1]s cache set --body "Dolorem et quam et illum." --key "Esse inventore ullam placeat aut." --namespace "Omnis itaque." --scope "Quasi ut." --ttl 3100652887298323449 + %[1]s cache set --body "Enim vel." --key "Ut in." --namespace "Ab dolores distinctio quis." --scope "Optio aliquam error nam." --ttl 2227603043401673122 `, os.Args[0]) } @@ -244,7 +246,7 @@ Set an external JSON value in the cache and provide an event for the input. -ttl INT: Example: - %[1]s cache set-external --body "Quis rerum velit sunt rerum dignissimos at." --key "Optio aliquam error nam." --namespace "Recusandae illo." --scope "Placeat veniam veritatis doloribus." --ttl 5137048679846705449 + %[1]s cache set-external --body "Sint ipsa fugiat et id rem." --key "Molestiae minima." --namespace "Quia dolores rem." --scope "Est illum." --ttl 6207033275224297400 `, os.Args[0]) } diff --git a/gen/http/openapi.json b/gen/http/openapi.json index febee9e13843c7696b10226d473cbc0aeeb535e8..81d8d61561c393db3717cdf66bbcfad496db6da6 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.","schema":{"$ref":"#/definitions/HealthLivenessResponseBody","required":["service","status","version"]}}},"schemes":["http"]}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/HealthReadinessResponseBody","required":["service","status","version"]}}},"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":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","required":false,"type":"integer"},{"name":"any","in":"body","required":true,"schema":{"type":"string","format":"binary"}}],"responses":{"201":{"description":"Created response."}},"schemes":["http"]}},"/v1/external/cache":{"post":{"tags":["cache"],"summary":"SetExternal cache","description":"Set an external JSON value in the cache and provide an event for the input.","operationId":"cache#SetExternal","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":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","required":false,"type":"integer"},{"name":"any","in":"body","required":true,"schema":{"type":"string","format":"binary"}}],"responses":{"200":{"description":"OK response."}},"schemes":["http"]}}},"definitions":{"HealthLivenessResponseBody":{"title":"HealthLivenessResponseBody","type":"object","properties":{"service":{"type":"string","description":"Service name.","example":"Laborum reprehenderit rerum est et ut dolores."},"status":{"type":"string","description":"Status message.","example":"Consequatur porro qui est dolor a."},"version":{"type":"string","description":"Service runtime version.","example":"Ad dolor."}},"example":{"service":"Fugiat voluptatem vel et.","status":"Sint tempore est nam iusto.","version":"Ipsam quidem aut velit vitae est."},"required":["service","status","version"]},"HealthReadinessResponseBody":{"title":"HealthReadinessResponseBody","type":"object","properties":{"service":{"type":"string","description":"Service name.","example":"Repellat qui totam et recusandae."},"status":{"type":"string","description":"Status message.","example":"Dolores at qui aliquam ullam."},"version":{"type":"string","description":"Service runtime version.","example":"Omnis ex fugit corporis."}},"example":{"service":"Minima sed et.","status":"Veniam optio qui.","version":"Suscipit velit aliquid et."},"required":["service","status","version"]}}} \ 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.","schema":{"$ref":"#/definitions/HealthLivenessResponseBody","required":["service","status","version"]}}},"schemes":["http"]}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/HealthReadinessResponseBody","required":["service","status","version"]}}},"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"},{"name":"x-cache-flatten-strategy","in":"header","description":"Flatten strategy.","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":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","required":false,"type":"integer"},{"name":"any","in":"body","required":true,"schema":{"type":"string","format":"binary"}}],"responses":{"201":{"description":"Created response."}},"schemes":["http"]}},"/v1/external/cache":{"post":{"tags":["cache"],"summary":"SetExternal cache","description":"Set an external JSON value in the cache and provide an event for the input.","operationId":"cache#SetExternal","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":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","required":false,"type":"integer"},{"name":"any","in":"body","required":true,"schema":{"type":"string","format":"binary"}}],"responses":{"200":{"description":"OK response."}},"schemes":["http"]}}},"definitions":{"HealthLivenessResponseBody":{"title":"HealthLivenessResponseBody","type":"object","properties":{"service":{"type":"string","description":"Service name.","example":"Repellat qui totam et recusandae."},"status":{"type":"string","description":"Status message.","example":"Dolores at qui aliquam ullam."},"version":{"type":"string","description":"Service runtime version.","example":"Omnis ex fugit corporis."}},"example":{"service":"Minima sed et.","status":"Veniam optio qui.","version":"Suscipit velit aliquid et."},"required":["service","status","version"]},"HealthReadinessResponseBody":{"title":"HealthReadinessResponseBody","type":"object","properties":{"service":{"type":"string","description":"Service name.","example":"Rerum et qui alias qui."},"status":{"type":"string","description":"Status message.","example":"Beatae commodi."},"version":{"type":"string","description":"Service runtime version.","example":"Fuga voluptas explicabo et libero."}},"example":{"service":"Illum nam ratione nisi.","status":"Labore vel.","version":"Aut illum."},"required":["service","status","version"]}}} \ No newline at end of file diff --git a/gen/http/openapi.yaml b/gen/http/openapi.yaml index 3648e0c82368173c51c7c1ff87e89388b3fb8042..0e5f69ccd6b890c8890e333e74e95dcbfc35654d 100644 --- a/gen/http/openapi.yaml +++ b/gen/http/openapi.yaml @@ -72,6 +72,11 @@ paths: description: Cache entry scope required: false type: string + - name: x-cache-flatten-strategy + in: header + description: Flatten strategy. + required: false + type: string responses: "200": description: OK response. @@ -165,19 +170,19 @@ definitions: service: type: string description: Service name. - example: Laborum reprehenderit rerum est et ut dolores. + example: Repellat qui totam et recusandae. status: type: string description: Status message. - example: Consequatur porro qui est dolor a. + example: Dolores at qui aliquam ullam. version: type: string description: Service runtime version. - example: Ad dolor. + example: Omnis ex fugit corporis. example: - service: Fugiat voluptatem vel et. - status: Sint tempore est nam iusto. - version: Ipsam quidem aut velit vitae est. + service: Minima sed et. + status: Veniam optio qui. + version: Suscipit velit aliquid et. required: - service - status @@ -189,19 +194,19 @@ definitions: service: type: string description: Service name. - example: Repellat qui totam et recusandae. + example: Rerum et qui alias qui. status: type: string description: Status message. - example: Dolores at qui aliquam ullam. + example: Beatae commodi. version: type: string description: Service runtime version. - example: Omnis ex fugit corporis. + example: Fuga voluptas explicabo et libero. example: - service: Minima sed et. - status: Veniam optio qui. - version: Suscipit velit aliquid et. + service: Illum nam ratione nisi. + status: Labore vel. + version: Aut illum. required: - service - status diff --git a/gen/http/openapi3.json b/gen/http/openapi3.json index 605856bfa27850eb6f1bf81f1b1101d5742ed5ec..c1681f1cf9ebc5972cbe0c9343c8bd787eb0e75a 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"},"example":{"service":"Molestiae minima.","status":"Quia dolores rem.","version":"Est illum."}}}}}}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"},"example":{"service":"Repellendus quo.","status":"Cumque aut.","version":"Sit sint ipsa fugiat et id rem."}}}}}}},"/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":"Rerum et qui alias qui.","format":"binary"},"example":"Accusamus ratione voluptatibus."}}}}},"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"},{"name":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","allowEmptyValue":true,"schema":{"type":"integer","description":"Cache entry TTL in seconds","example":60,"format":"int64"},"example":60}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"string","example":"Beatae commodi.","format":"binary"},"example":"Quae minus maiores nulla deleniti ipsa."}}},"responses":{"201":{"description":"Created response."}}}},"/v1/external/cache":{"post":{"tags":["cache"],"summary":"SetExternal cache","description":"Set an external JSON value in the cache and provide an event for the input.","operationId":"cache#SetExternal","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"},{"name":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","allowEmptyValue":true,"schema":{"type":"integer","description":"Cache entry TTL in seconds","example":60,"format":"int64"},"example":60}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"string","example":"Fuga voluptas explicabo et libero.","format":"binary"},"example":"Quidem nihil quis tempore."}}},"responses":{"200":{"description":"OK response."}}}}},"components":{"schemas":{"HealthResponse":{"type":"object","properties":{"service":{"type":"string","description":"Service name.","example":"Illum nam ratione nisi."},"status":{"type":"string","description":"Status message.","example":"Labore vel."},"version":{"type":"string","description":"Service runtime version.","example":"Aut illum."}},"example":{"service":"Itaque vel.","status":"Itaque enim aut consequatur beatae ut.","version":"Et atque impedit nostrum perspiciatis ipsum."},"required":["service","status","version"]}}},"tags":[{"name":"cache","description":"Cache service allows storing and retrieving data from distributed cache."},{"name":"health","description":"Health service provides health check endpoints."}]} \ 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"},"example":{"service":"Laborum reprehenderit rerum est et ut dolores.","status":"Consequatur porro qui est dolor a.","version":"Ad dolor."}}}}}}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"},"example":{"service":"Fugiat voluptatem vel et.","status":"Sint tempore est nam iusto.","version":"Ipsam quidem aut velit vitae est."}}}}}}},"/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"},"examples":{"default":{"summary":"default","value":"administration"},"multiple scopes":{"summary":"multiple scopes","value":"administration,user"}}},{"name":"x-cache-flatten-strategy","in":"header","description":"Flatten strategy.","allowEmptyValue":true,"schema":{"type":"string","description":"Flatten strategy.","example":"merge"},"examples":{"default":{"summary":"default","value":"merge"},"first key value only":{"summary":"first key value only","value":"first"},"last key value only":{"summary":"last key value only","value":"last"}}}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"type":"string","example":"Itaque vel.","format":"binary"},"example":"Amet atque cupiditate."}}}}},"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"},{"name":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","allowEmptyValue":true,"schema":{"type":"integer","description":"Cache entry TTL in seconds","example":60,"format":"int64"},"example":60}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"string","example":"Itaque enim aut consequatur beatae ut.","format":"binary"},"example":"Minus nostrum eaque."}}},"responses":{"201":{"description":"Created response."}}}},"/v1/external/cache":{"post":{"tags":["cache"],"summary":"SetExternal cache","description":"Set an external JSON value in the cache and provide an event for the input.","operationId":"cache#SetExternal","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"},{"name":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","allowEmptyValue":true,"schema":{"type":"integer","description":"Cache entry TTL in seconds","example":60,"format":"int64"},"example":60}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"string","example":"Et atque impedit nostrum perspiciatis ipsum.","format":"binary"},"example":"Eligendi magni qui porro beatae porro."}}},"responses":{"200":{"description":"OK response."}}}}},"components":{"schemas":{"HealthResponse":{"type":"object","properties":{"service":{"type":"string","description":"Service name.","example":"Accusamus ratione voluptatibus."},"status":{"type":"string","description":"Status message.","example":"Quae minus maiores nulla deleniti ipsa."},"version":{"type":"string","description":"Service runtime version.","example":"Quidem nihil quis tempore."}},"example":{"service":"Vel quis doloremque iure eius reiciendis.","status":"Perferendis porro laborum autem dolorem aut nesciunt.","version":"Sed eius qui placeat sed."},"required":["service","status","version"]}}},"tags":[{"name":"cache","description":"Cache service allows storing and retrieving data from distributed cache."},{"name":"health","description":"Health service provides health check endpoints."}]} \ No newline at end of file diff --git a/gen/http/openapi3.yaml b/gen/http/openapi3.yaml index 61ccd2d10543d218a16fb87c715c17cafe1d589b..0c90f30cff57af6c76a7c5fd2d97f938e245c63d 100644 --- a/gen/http/openapi3.yaml +++ b/gen/http/openapi3.yaml @@ -21,9 +21,9 @@ paths: schema: $ref: '#/components/schemas/HealthResponse' example: - service: Molestiae minima. - status: Quia dolores rem. - version: Est illum. + service: Laborum reprehenderit rerum est et ut dolores. + status: Consequatur porro qui est dolor a. + version: Ad dolor. /readiness: get: tags: @@ -38,9 +38,9 @@ paths: schema: $ref: '#/components/schemas/HealthResponse' example: - service: Repellendus quo. - status: Cumque aut. - version: Sit sint ipsa fugiat et id rem. + service: Fugiat voluptatem vel et. + status: Sint tempore est nam iusto. + version: Ipsam quidem aut velit vitae est. /v1/cache: get: tags: @@ -76,7 +76,31 @@ paths: type: string description: Cache entry scope example: administration - example: administration + examples: + default: + summary: default + value: administration + multiple scopes: + summary: multiple scopes + value: administration,user + - name: x-cache-flatten-strategy + in: header + description: Flatten strategy. + allowEmptyValue: true + schema: + type: string + description: Flatten strategy. + example: merge + examples: + default: + summary: default + value: merge + first key value only: + summary: first key value only + value: first + last key value only: + summary: last key value only + value: last responses: "200": description: OK response. @@ -84,9 +108,9 @@ paths: application/json: schema: type: string - example: Rerum et qui alias qui. + example: Itaque vel. format: binary - example: Accusamus ratione voluptatibus. + example: Amet atque cupiditate. post: tags: - cache @@ -138,9 +162,9 @@ paths: application/json: schema: type: string - example: Beatae commodi. + example: Itaque enim aut consequatur beatae ut. format: binary - example: Quae minus maiores nulla deleniti ipsa. + example: Minus nostrum eaque. responses: "201": description: Created response. @@ -196,9 +220,9 @@ paths: application/json: schema: type: string - example: Fuga voluptas explicabo et libero. + example: Et atque impedit nostrum perspiciatis ipsum. format: binary - example: Quidem nihil quis tempore. + example: Eligendi magni qui porro beatae porro. responses: "200": description: OK response. @@ -210,19 +234,19 @@ components: service: type: string description: Service name. - example: Illum nam ratione nisi. + example: Accusamus ratione voluptatibus. status: type: string description: Status message. - example: Labore vel. + example: Quae minus maiores nulla deleniti ipsa. version: type: string description: Service runtime version. - example: Aut illum. + example: Quidem nihil quis tempore. example: - service: Itaque vel. - status: Itaque enim aut consequatur beatae ut. - version: Et atque impedit nostrum perspiciatis ipsum. + service: Vel quis doloremque iure eius reiciendis. + status: Perferendis porro laborum autem dolorem aut nesciunt. + version: Sed eius qui placeat sed. required: - service - status diff --git a/internal/service/cache/service.go b/internal/service/cache/service.go index 9fdb01cc111e18143c9e1f968c3bdf0a1b4b9662..22de0e16543250a5a3c129c28dfa8ef4a849721e 100644 --- a/internal/service/cache/service.go +++ b/internal/service/cache/service.go @@ -3,6 +3,8 @@ package cache import ( "context" "encoding/json" + "fmt" + "strings" "time" "go.uber.org/zap" @@ -45,21 +47,19 @@ func (s *Service) Get(ctx context.Context, req *cache.CacheGetRequest) (interfac return nil, errors.New(errors.BadRequest, "missing key") } - // create key from the input fields - key := makeCacheKey(req.Key, req.Namespace, req.Scope) - data, err := s.cache.Get(ctx, key) - if err != nil { - 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) - } - return nil, errors.New("error getting value from cache", err) + var scopes []string + if req.Scope != nil { + scopes = strings.Split(*req.Scope, ",") } - var decodedValue interface{} - if err := json.Unmarshal(data, &decodedValue); err != nil { - logger.Error("cannot decode json value from cache", zap.Error(err)) - return nil, errors.New("cannot decode json value from cache", err) + if len(scopes) > 1 { + return s.getWithMultipleScopes(ctx, req, scopes) + } + + decodedValue, err := s.get(ctx, req.Key, req.Namespace, req.Scope) + if err != nil { + logger.Error("error getting value from cache", zap.Error(err)) + return nil, err } return decodedValue, nil @@ -128,3 +128,112 @@ func makeCacheKey(key string, namespace, scope *string) string { } return k } + +func (s *Service) getWithMultipleScopes(ctx context.Context, req *cache.CacheGetRequest, scopes []string) (map[string]interface{}, error) { + keyValues := map[string][]interface{}{} + result := map[string]interface{}{} + + for _, scope := range scopes { + scope := strings.TrimSpace(scope) + decodedValue, err := s.get(ctx, req.Key, req.Namespace, &scope) + if err != nil { + if errors.Is(errors.NotFound, err) { + s.logger.Warn(err.Error()) + continue + } + return nil, err + } + + switch d := decodedValue.(type) { + case map[string]interface{}: + addValue(d, keyValues) + case []map[string]interface{}: + for _, data := range d { + addValue(data, keyValues) + } + default: + s.logger.Warn("decode value is of unknown type") + continue + } + } + + switch { + case req.Strategy == nil || *req.Strategy == "merge": + result = mergeAll(keyValues) + + case *req.Strategy == "first": + for key, value := range keyValues { + result[key] = value[0] + } + + case *req.Strategy == "last": + for key, value := range keyValues { + result[key] = value[len(value)-1] + } + } + + return result, nil +} + +func addValue(decodedValue map[string]interface{}, des map[string][]interface{}) { + for k, v := range decodedValue { + if dataArr, contains := des[k]; contains { + dataArr = append(dataArr, v) + des[k] = dataArr + continue + } + + des[k] = []interface{}{v} + } +} + +// mergeAll merges all values for a key if more than one value is available. +func mergeAll(data map[string][]interface{}) map[string]interface{} { + result := map[string]interface{}{} + for key, value := range data { + if len(value) == 1 { + result[key] = value[0] + continue + } + + for i, v := range value { + index := fmt.Sprintf("%s_%d", key, i+1) + result[index] = v + } + } + return result +} + +func (s *Service) get(ctx context.Context, key string, namespace *string, scope *string) (interface{}, error) { + // create key from the input fields + cacheKey := makeCacheKey(key, namespace, scope) + data, err := s.cache.Get(ctx, cacheKey) + if err != nil { + if errors.Is(errors.NotFound, err) { + return nil, errors.New(errors.NotFound, "key not found in cache", err) + } + return nil, errors.New("error getting value from cache", err) + } + + decodedValue, err := unmarshalCacheData(data) + if err != nil { + return nil, errors.New("cannot decode json value from cache", err) + } + + return decodedValue, nil +} + +func unmarshalCacheData(data []byte) (interface{}, error) { + var keyValueArray []map[string]interface{} + var keyValue map[string]interface{} + + err := json.Unmarshal(data, &keyValue) + if err != nil { + err := json.Unmarshal(data, &keyValueArray) + if err != nil { + return nil, err + } + return keyValueArray, nil + } + return keyValue, nil +} diff --git a/internal/service/cache/service_test.go b/internal/service/cache/service_test.go index 530d9bfe6e7dd5a406baf37f12bda81784a77b60..1d87d4df67a3a304000aa22bbf6a06ec21df9274 100644 --- a/internal/service/cache/service_test.go +++ b/internal/service/cache/service_test.go @@ -2,11 +2,13 @@ package cache_test import ( "context" + "fmt" "testing" "time" "github.com/stretchr/testify/assert" "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" goacache "gitlab.eclipse.org/eclipse/xfsc/tsa/cache/gen/cache" "gitlab.eclipse.org/eclipse/xfsc/tsa/cache/internal/service/cache" @@ -21,14 +23,18 @@ func TestNew(t *testing.T) { } func TestService_Get(t *testing.T) { + const key1 = "key,namespace,scope" + const key2 = "key,namespace,scope2" + tests := []struct { name string cache *cachefakes.FakeCache req *goacache.CacheGetRequest - res interface{} - errkind errors.Kind - errtext string + res interface{} + errkind errors.Kind + errtext string + loggerText string }{ { name: "missing cache key", @@ -96,12 +102,136 @@ func TestService_Get(t *testing.T) { res: map[string]interface{}{"test": "value"}, errtext: "", }, + { + name: "multiple scope cache return error", + req: &goacache.CacheGetRequest{ + Key: "key", + Namespace: ptr.String("namespace"), + Scope: ptr.String("scope,scope2"), + Strategy: ptr.String("last"), + }, + cache: &cachefakes.FakeCache{ + GetStub: func(ctx context.Context, key string) ([]byte, error) { + if key == key1 { + return nil, fmt.Errorf("some error") + } + return []byte(`{"test":"value2"}`), nil + }, + }, + errtext: "error getting value from cache", + }, + { + name: "multiple scope with merge flatten strategy", + req: &goacache.CacheGetRequest{ + Key: "key", + Namespace: ptr.String("namespace"), + Scope: ptr.String("scope,scope2"), + Strategy: ptr.String("merge"), + }, + cache: &cachefakes.FakeCache{ + GetStub: func(ctx context.Context, key string) ([]byte, error) { + if key == key1 { + return []byte(`{"test":"value"}`), nil + } + return []byte(`{"test":"value2"}`), nil + }, + }, + res: map[string]interface{}{"test_1": "value", "test_2": "value2"}, + errtext: "", + }, + { + name: "multiple scope with first flatten strategy", + req: &goacache.CacheGetRequest{ + Key: "key", + Namespace: ptr.String("namespace"), + Scope: ptr.String("scope,scope2"), + Strategy: ptr.String("first"), + }, + cache: &cachefakes.FakeCache{ + GetStub: func(ctx context.Context, key string) ([]byte, error) { + if key == key1 { + return []byte(`{"test":"value"}`), nil + } + return []byte(`{"test":"value2"}`), nil + }, + }, + res: map[string]interface{}{"test": "value"}, + errtext: "", + }, + { + name: "multiple scope with last flatten strategy", + req: &goacache.CacheGetRequest{ + Key: "key", + Namespace: ptr.String("namespace"), + Scope: ptr.String("scope,not_existed_scope,scope2"), + Strategy: ptr.String("last"), + }, + cache: &cachefakes.FakeCache{ + GetStub: func(ctx context.Context, key string) ([]byte, error) { + if key == key1 { + return []byte(`{"test":"value"}`), nil + } + return []byte(`{"test":"value2"}`), nil + }, + }, + res: map[string]interface{}{"test": "value2"}, + errtext: "", + }, + { + name: "multiple scope with last flatten strategy", + req: &goacache.CacheGetRequest{ + Key: "key", + Namespace: ptr.String("namespace"), + Scope: ptr.String("scope,scope2"), + Strategy: ptr.String("last"), + }, + cache: &cachefakes.FakeCache{ + GetStub: func(ctx context.Context, key string) ([]byte, error) { + if key == key1 { + return []byte(`{"test":"value"}`), nil + } + return []byte(`{"test":"value2"}`), nil + }, + }, + res: map[string]interface{}{"test": "value2"}, + errtext: "", + }, + { + name: "multiple scope return warn if the key doesn't exist", + req: &goacache.CacheGetRequest{ + Key: "key", + Namespace: ptr.String("namespace"), + Scope: ptr.String("scope,scope2"), + Strategy: ptr.String("last"), + }, + cache: &cachefakes.FakeCache{ + GetStub: func(ctx context.Context, key string) ([]byte, error) { + if key == key1 { + return []byte(`{"test":"value"}`), nil + } + if key == key2 { + return []byte(`{"test":"value2"}`), nil + } + return nil, errors.New(errors.NotFound, "some error") + }, + }, + res: map[string]interface{}{"test": "value2"}, + loggerText: "key not found in cache", + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - svc := cache.New(test.cache, nil, zap.NewNop()) + core, logs := observer.New(zap.WarnLevel) + logger := zap.New(core) + + svc := cache.New(test.cache, nil, logger) res, err := svc.Get(context.Background(), test.req) + + if test.loggerText != "" && logs.Len() >= 1 { + assert.Contains(t, logs.All()[0].Message, test.loggerText) + } + if err == nil { assert.Empty(t, test.errtext) assert.Equal(t, test.res, res) diff --git a/vendor/go.uber.org/zap/zaptest/observer/logged_entry.go b/vendor/go.uber.org/zap/zaptest/observer/logged_entry.go new file mode 100644 index 0000000000000000000000000000000000000000..a4ea7ec36c1e4162f9a56e17183d78be218c4147 Binary files /dev/null and b/vendor/go.uber.org/zap/zaptest/observer/logged_entry.go differ diff --git a/vendor/go.uber.org/zap/zaptest/observer/observer.go b/vendor/go.uber.org/zap/zaptest/observer/observer.go new file mode 100644 index 0000000000000000000000000000000000000000..f77f1308baf29fa3699df5d3cb0eb07763f50d9b Binary files /dev/null and b/vendor/go.uber.org/zap/zaptest/observer/observer.go differ diff --git a/vendor/modules.txt b/vendor/modules.txt index ffa5dce85532a46fe8dc891429ca72e3eb6cbf3f..489d9cc44ec0130d660153efea671ef669dd4f01 100644 Binary files a/vendor/modules.txt and b/vendor/modules.txt differ