From 3412d6c75dc984db82f8be5b7acc9c3c2129aa48 Mon Sep 17 00:00:00 2001
From: Valentin Yordanov <valentin.yordanov@vereign.com>
Date: Mon, 11 Dec 2023 10:37:14 +0000
Subject: [PATCH] Get cache data with multiple scopes

---
 .gitlab-ci.yml                                |   4 +-
 deployment/ci/Dockerfile                      |   2 +-
 deployment/compose/Dockerfile                 |   2 +-
 design/design.go                              |   8 +-
 design/types.go                               |   3 +-
 gen/cache/service.go                          |   1 +
 gen/http/cache/client/cli.go                  |  13 +-
 gen/http/cache/client/encode_decode.go        |   4 +
 gen/http/cache/server/encode_decode.go        |   7 +-
 gen/http/cache/server/types.go                |   3 +-
 gen/http/cli/cache/cli.go                     |  14 +-
 gen/http/openapi.json                         |   2 +-
 gen/http/openapi.yaml                         |  29 ++--
 gen/http/openapi3.json                        |   2 +-
 gen/http/openapi3.yaml                        |  62 +++++---
 internal/service/cache/service.go             | 135 +++++++++++++++--
 internal/service/cache/service_test.go        | 138 +++++++++++++++++-
 .../zap/zaptest/observer/logged_entry.go      | Bin 0 -> 1596 bytes
 .../zap/zaptest/observer/observer.go          | Bin 0 -> 5725 bytes
 vendor/modules.txt                            | Bin 10911 -> 10944 bytes
 20 files changed, 362 insertions(+), 67 deletions(-)
 create mode 100644 vendor/go.uber.org/zap/zaptest/observer/logged_entry.go
 create mode 100644 vendor/go.uber.org/zap/zaptest/observer/observer.go

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7ba775a..c92e869 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 60f9b82..346b141 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 e9d3bae..b57e8bd 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 470b6a6..28fc20a 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 eee0825..bf8bd71 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 74f41d9..9d0f802 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 4f75bd4..31684f6 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 db80d15..793256a 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 44aee8d..f8d56c0 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 56803f1..1e45878 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 44fb882..518b211 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 febee9e..81d8d61 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 3648e0c..0e5f69c 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 605856b..c1681f1 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 61ccd2d..0c90f30 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 9fdb01c..22de0e1 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 530d9bf..1d87d4d 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
GIT binary patch
literal 1596
zcmdPbS8&cRs4U7%&nQvQNY+#^GB7k(2u(^YQV2;+&dAHp$xqKrE!I)+%uCke(%0wG
z*H;KgEy~R-F3!x)Q^+h<$Ve?pO{!E#FG|cSNlnpFNGnQBRme|MNX|$sN>9~MD9Kky
z%&Sx=NG&SP&r`@xDoM=D%gjqxNK{D9FQ^2Wm!GCkl95@gP@JDuQl40ps*sqMqL5fz
zoS&STSdyBekdmKVnwy$el2`(=GA%PFwOB!;BqLQpDHy6lNfT^ON@`*b$iU1z1&}mM
zR-rtzBqP7HM4>3PxTGjE8Dxx(LS|laPH76rB{1on%-l?<B_QqKFewI^SCX$#TAZo_
zcCd~@ZhlH;TBVLcZYtP+1*J(jnZ+483MrW&)00X|QgsxHL4wJtc_3Yhc`5q&MGD2K
zIbg>n=NDwA7K1_vWC_fTI$%$i<b%Q~H?ssp73b#_gF>%7BR?0_Kbc@xrIi-tWfo_o
zrhpWs<SP{CgYC^qO)de6fJ{%z&&kOz2l+8MKQAQ{<ndxFXe@?gq$(sP<(H*`y#@BH
zLSBAJW-=uF!7);Rh-s))aYkZJjzUr@H26|e6f*NbK?D+o`K$=!{o<0uypqhs9EF1X
zBCx&aK8B^H5D!;{V1KueaK|851<zoGfFS=cPZw7g1trH|1<zn59ffeu5D)**5QT8Z
zpdd%TkVpl8Hw8z(NCj_CKNlSZ*NA{1*Wh3U{~(Y%JbeRvJY8LM6g>T$eL`J4{oECt
zLPHe%{6iFcJbgVwTwN4G{1rfULQV5@4F;L#>l)<j;pi9Q=;Z0+84{@jGRw^~#1CY$
zn}3jkqe6gVP>82<sE=ciLO^IxfPb*7f}@{{f}g*ir=MGpr=Pp4ud82(o`R>Jf}g*F
zYnZEFh(fT3qmK{BHjr_Sp&=grK_G`JIQs`g26?)BgeZ9U`?$CU1uHnYD)@LhI{CON
zfNk@ORB-lj^z_wHaB=i?bO*UHNWnkE!xijFkV=R%6~aASL82gA9sLv>okKkR{XqV4
z_V)`3a&!*SQ3&x53Q_Qf>JIk|cGXdE4Dt*Hg^XK}zc1LQpwRVq1F7=#Q*ie8^K*q5
z1PXbO^AQo`9|Y!y2D^e(gI%QH;_B$*>E|A-;OPhQCn#`X5vs?<RgjpRotU1gke^hX
zT2z)=#Ko1F3rZ6TO6mD}rAetpdih1^`c;VqAe@|El&Zu9GQ}}Z!6!dGJvGHOucW9F
zRL~^mDWv8l=Yz7WZen^~esM`=vO-a6K~ZWkyl}`*Q%F?E$xm0vO)V}?Oiu+RG6lEH
z)SMKB#InT9oW!KeoXiriamo34C8-r93Mr`tsd*`hc_n&WC6xuKD7F@t6qP2IC{%NC
zRwWjI+@=S14Hu^~RC9DJOag2PS1s7>P+8x^0)?W~lG38QVueJ7+{6NfwEQ9kP)<n$
zxu{qnGfx4kRF5mIG%r~}1I2nxn5=JNfrci?fM`$&lb0TwnOBlpl$MyBT3rir95`4~
zixjME6=2TsOD*?JEbvbPl?JY$P)jY+(B$F-IUrs~Aq}LoC^0WRRUuUm>I9HMoN0QF
fDJdcO8c-88xj1XNI3a$31b%9f9@wYFT(w*PRaybU

literal 0
HcmV?d00001

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
GIT binary patch
literal 5725
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
Z4(fc>;>`uYm3he;nhJ2QL1Px85de+rY~TO@

literal 0
HcmV?d00001

diff --git a/vendor/modules.txt b/vendor/modules.txt
index ffa5dce85532a46fe8dc891429ca72e3eb6cbf3f..489d9cc44ec0130d660153efea671ef669dd4f01 100644
GIT binary patch
delta 45
ycmbOqdLVRzudG6PzFuikYLQ-kQM!IrVgU%3q!yRx=O-1X7L}zIZT683<^=$jIuSDf

delta 12
TcmX>QIzM!Suk7YL*&tp3ByI%3

-- 
GitLab