Compare commits
2 Commits
v4.1.8
...
dee7bc9c38
Author | SHA1 | Date | |
---|---|---|---|
dee7bc9c38 | |||
053fe2a69d |
@@ -25,7 +25,7 @@ jobs:
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache-dependency-path: "**/*.sum"
|
||||
go-version: 'stable'
|
||||
go-version: 'stable'
|
||||
|
||||
- name: test coverage
|
||||
run: |
|
||||
@@ -42,8 +42,8 @@ jobs:
|
||||
name: autocommit
|
||||
with:
|
||||
commit_message: Apply Code Coverage Badge
|
||||
skip_fetch: false
|
||||
skip_checkout: false
|
||||
skip_fetch: true
|
||||
skip_checkout: true
|
||||
file_pattern: ./README.md
|
||||
|
||||
- name: push
|
||||
@@ -51,4 +51,4 @@ jobs:
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ github.token }}
|
||||
branch: ${{ github.ref }}
|
||||
branch: ${{ github.ref }}
|
@@ -24,6 +24,6 @@ jobs:
|
||||
- name: setup deps
|
||||
run: go get -v ./...
|
||||
- name: run lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
uses: https://github.com/golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: 'latest'
|
@@ -1,57 +1,54 @@
|
||||
name: sync
|
||||
name: syncpull
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/5 * * * *'
|
||||
push:
|
||||
branches: [ master, v3, v4 ]
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- '.gitea/**'
|
||||
- cron: '* * * * *'
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
if: github.server_url != 'https://github.com'
|
||||
pull:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: init
|
||||
run: |
|
||||
git config --global user.email "vtolstov <vtolstov@users.noreply.github.com>"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
echo "machine git.unistack.org login vtolstov password ${{ secrets.TOKEN_GITEA }}" >> /root/.netrc
|
||||
echo "machine github.com login vtolstov password ${{ secrets.TOKEN_GITHUB }}" >> /root/.netrc
|
||||
echo "machine git.unistack.org login vtolstov password ${{ secrets.TOKEN_GITEA }}" | tee -a /root/.netrc
|
||||
echo "machine github.com login vtolstov password ${{ secrets.TOKEN_GITHUB }}" | tee -a /root/.netrc
|
||||
|
||||
- name: sync master
|
||||
- name: track master
|
||||
run: |
|
||||
git clone --filter=blob:none --filter=tree:0 --branch master --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
|
||||
git clone --depth=10 --branch master --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
|
||||
cd repo
|
||||
git remote add --no-tags --track master upstream https://github.com/${GITHUB_REPOSITORY}
|
||||
git remote add --no-tags --fetch --track master upstream https://github.com/${GITHUB_REPOSITORY}
|
||||
git pull --rebase upstream master
|
||||
git push upstream master --progress
|
||||
git merge --allow-unrelated-histories "upstream/master"
|
||||
git push origin master --progress
|
||||
cd ../
|
||||
rm -rf repo
|
||||
|
||||
- name: sync v3
|
||||
- name: track v3
|
||||
run: |
|
||||
git clone --filter=blob:none --filter=tree:0 --branch v3 --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
|
||||
git clone --depth=10 --branch v3 --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
|
||||
cd repo
|
||||
git remote add --no-tags --fetch --track v3 upstream https://github.com/${GITHUB_REPOSITORY}
|
||||
git pull --rebase upstream v3
|
||||
git push upstream v3
|
||||
git merge --allow-unrelated-histories "upstream/v3"
|
||||
git push origin v3 --progress
|
||||
cd ../
|
||||
rm -rf repo
|
||||
|
||||
- name: sync v4
|
||||
- name: track v4
|
||||
run: |
|
||||
git clone --filter=blob:none --filter=tree:0 --branch v4 --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
|
||||
git clone --depth=10 --branch v4 --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
|
||||
cd repo
|
||||
git remote add --no-tags --fetch --track v4 upstream https://github.com/${GITHUB_REPOSITORY}
|
||||
git pull --rebase upstream v4
|
||||
git push upstream v4
|
||||
git merge --allow-unrelated-histories "upstream/v4"
|
||||
git push origin v4 --progress
|
||||
cd ../
|
||||
rm -rf repo
|
@@ -32,19 +32,19 @@ jobs:
|
||||
go-version: 'stable'
|
||||
- name: setup go work
|
||||
env:
|
||||
GOWORK: ${{ github.workspace }}/go.work
|
||||
GOWORK: /workspace/${{ github.repository_owner }}/go.work
|
||||
run: |
|
||||
go work init
|
||||
go work use .
|
||||
go work use micro-tests
|
||||
- name: setup deps
|
||||
env:
|
||||
GOWORK: ${{ github.workspace }}/go.work
|
||||
GOWORK: /workspace/${{ github.repository_owner }}/go.work
|
||||
run: go get -v ./...
|
||||
- name: run tests
|
||||
env:
|
||||
INTEGRATION_TESTS: yes
|
||||
GOWORK: ${{ github.workspace }}/go.work
|
||||
GOWORK: /workspace/${{ github.repository_owner }}/go.work
|
||||
run: |
|
||||
cd micro-tests
|
||||
go test -mod readonly -v ./... || true
|
@@ -1,5 +1,5 @@
|
||||
run:
|
||||
concurrency: 8
|
||||
timeout: 5m
|
||||
deadline: 5m
|
||||
issues-exit-code: 1
|
||||
tests: true
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# Micro
|
||||

|
||||

|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://pkg.go.dev/go.unistack.org/micro/v4?tab=overview)
|
||||
[](https://git.unistack.org/unistack-org/micro/actions?query=workflow%3Abuild+branch%3Av4+event%3Apush)
|
||||
|
2
go.mod
2
go.mod
@@ -12,7 +12,6 @@ require (
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5
|
||||
github.com/spf13/cast v1.7.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go.unistack.org/micro-proto/v4 v4.1.0
|
||||
@@ -27,6 +26,7 @@ require (
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 // indirect
|
||||
|
@@ -36,7 +36,7 @@ func NewClientCallWrapper(keys ...string) client.CallWrapper {
|
||||
omd = metadata.New(len(imd))
|
||||
}
|
||||
for _, k := range keys {
|
||||
if v := imd.Get(k); v != nil {
|
||||
if v, ok := imd.Get(k); ok {
|
||||
omd.Add(k, v...)
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func (w *wrapper) Call(ctx context.Context, req client.Request, rsp interface{},
|
||||
omd = metadata.New(len(imd))
|
||||
}
|
||||
for _, k := range w.keys {
|
||||
if v := imd.Get(k); v != nil {
|
||||
if v, ok := imd.Get(k); ok {
|
||||
omd.Add(k, v...)
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ func (w *wrapper) Stream(ctx context.Context, req client.Request, opts ...client
|
||||
omd = metadata.New(len(imd))
|
||||
}
|
||||
for _, k := range w.keys {
|
||||
if v := imd.Get(k); v != nil {
|
||||
if v, ok := imd.Get(k); ok {
|
||||
omd.Add(k, v...)
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func NewServerHandlerWrapper(keys ...string) server.HandlerWrapper {
|
||||
omd = metadata.New(len(imd))
|
||||
}
|
||||
for _, k := range keys {
|
||||
if v := imd.Get(k); v != nil {
|
||||
if v, ok := imd.Get(k); ok {
|
||||
omd.Add(k, v...)
|
||||
}
|
||||
}
|
||||
|
@@ -38,16 +38,18 @@ var DefaultMetadataFunc = func(ctx context.Context) (context.Context, error) {
|
||||
|
||||
if xid == "" {
|
||||
var ids []string
|
||||
|
||||
for i := range imd.Get(DefaultMetadataKey) {
|
||||
if ids[i] != "" {
|
||||
xid = ids[i]
|
||||
if ids, iok = imd.Get(DefaultMetadataKey); iok {
|
||||
for i := range ids {
|
||||
if ids[i] != "" {
|
||||
xid = ids[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range omd.Get(DefaultMetadataKey) {
|
||||
if ids[i] != "" {
|
||||
xid = ids[i]
|
||||
if ids, ook = omd.Get(DefaultMetadataKey); ook {
|
||||
for i := range ids {
|
||||
if ids[i] != "" {
|
||||
xid = ids[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ package requestid
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"go.unistack.org/micro/v4/metadata"
|
||||
@@ -25,10 +24,10 @@ func TestDefaultMetadataFunc(t *testing.T) {
|
||||
t.Fatalf("md missing in outgoing context")
|
||||
}
|
||||
|
||||
iv := imd.Get(DefaultMetadataKey)
|
||||
ov := omd.Get(DefaultMetadataKey)
|
||||
_, iok := imd.Get(DefaultMetadataKey)
|
||||
_, ook := omd.Get(DefaultMetadataKey)
|
||||
|
||||
if !slices.Equal(iv, ov) {
|
||||
t.Fatalf("missing metadata key value %v != %v", iv, ov)
|
||||
if !iok || !ook {
|
||||
t.Fatalf("missing metadata key value")
|
||||
}
|
||||
}
|
||||
|
@@ -406,7 +406,7 @@ func TestLogger(t *testing.T) {
|
||||
func Test_WithContextAttrFunc(t *testing.T) {
|
||||
loggerContextAttrFuncs := []logger.ContextAttrFunc{
|
||||
func(ctx context.Context) []interface{} {
|
||||
md, ok := metadata.FromOutgoingContext(ctx)
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -425,7 +425,7 @@ func Test_WithContextAttrFunc(t *testing.T) {
|
||||
logger.DefaultContextAttrFuncs = append(logger.DefaultContextAttrFuncs, loggerContextAttrFuncs...)
|
||||
|
||||
ctx := context.TODO()
|
||||
ctx = metadata.AppendOutgoingContext(ctx, "X-Request-Id", uuid.New().String(),
|
||||
ctx = metadata.AppendIncomingContext(ctx, "X-Request-Id", uuid.New().String(),
|
||||
"Source-Service", "Test-System")
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
@@ -445,9 +445,9 @@ func Test_WithContextAttrFunc(t *testing.T) {
|
||||
t.Fatalf("logger info, buf %s", buf.Bytes())
|
||||
}
|
||||
buf.Reset()
|
||||
omd, _ := metadata.FromOutgoingContext(ctx)
|
||||
imd, _ := metadata.FromIncomingContext(ctx)
|
||||
l.Info(ctx, "test message1")
|
||||
omd.Set("Source-Service", "Test-System2")
|
||||
imd.Set("Source-Service", "Test-System2")
|
||||
l.Info(ctx, "test message2")
|
||||
|
||||
// t.Logf("xxx %s", buf.Bytes())
|
||||
|
@@ -106,7 +106,16 @@ func (md Metadata) CopyTo(out Metadata) {
|
||||
}
|
||||
|
||||
// Get obtains the values for a given key.
|
||||
func (md Metadata) Get(k string) []string {
|
||||
func (md Metadata) MustGet(k string) []string {
|
||||
v, ok := md.Get(k)
|
||||
if !ok {
|
||||
panic("missing metadata key")
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Get obtains the values for a given key.
|
||||
func (md Metadata) Get(k string) ([]string, bool) {
|
||||
v, ok := md[k]
|
||||
if !ok {
|
||||
v, ok = md[strings.ToLower(k)]
|
||||
@@ -114,13 +123,27 @@ func (md Metadata) Get(k string) []string {
|
||||
if !ok {
|
||||
v, ok = md[textproto.CanonicalMIMEHeaderKey(k)]
|
||||
}
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// MustGetJoined obtains the values for a given key
|
||||
// with joined values with "," symbol
|
||||
func (md Metadata) MustGetJoined(k string) string {
|
||||
v, ok := md.GetJoined(k)
|
||||
if !ok {
|
||||
panic("missing metadata key")
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// GetJoined obtains the values for a given key
|
||||
// with joined values with "," symbol
|
||||
func (md Metadata) GetJoined(k string) string {
|
||||
return strings.Join(md.Get(k), ",")
|
||||
func (md Metadata) GetJoined(k string) (string, bool) {
|
||||
v, ok := md.Get(k)
|
||||
if !ok {
|
||||
return "", ok
|
||||
}
|
||||
return strings.Join(v, ","), true
|
||||
}
|
||||
|
||||
// Set sets the value of a given key with a slice of values.
|
||||
@@ -218,6 +241,24 @@ func AppendContext(ctx context.Context, kv ...string) context.Context {
|
||||
return context.WithValue(ctx, metadataCurrentKey{}, rawMetadata{md: md.md, added: added})
|
||||
}
|
||||
|
||||
// AppendIncomingContext returns a new context with the provided kv merged
|
||||
// with any existing metadata in the context. Please refer to the documentation
|
||||
// of Pairs for a description of kv.
|
||||
func AppendIncomingContext(ctx context.Context, kv ...string) context.Context {
|
||||
if len(kv)%2 == 1 {
|
||||
panic(fmt.Sprintf("metadata: AppendIncomingContext got an odd number of input pairs for metadata: %d", len(kv)))
|
||||
}
|
||||
md, _ := ctx.Value(metadataIncomingKey{}).(rawMetadata)
|
||||
added := make([][]string, len(md.added)+1)
|
||||
copy(added, md.added)
|
||||
kvCopy := make([]string, 0, len(kv))
|
||||
for i := 0; i < len(kv); i += 2 {
|
||||
kvCopy = append(kvCopy, strings.ToLower(kv[i]), kv[i+1])
|
||||
}
|
||||
added[len(added)-1] = kvCopy
|
||||
return context.WithValue(ctx, metadataIncomingKey{}, rawMetadata{md: md.md, added: added})
|
||||
}
|
||||
|
||||
// AppendOutgoingContext returns a new context with the provided kv merged
|
||||
// with any existing metadata in the context. Please refer to the documentation
|
||||
// of Pairs for a description of kv.
|
||||
|
@@ -19,8 +19,8 @@ func TestAppendOutgoingContextModify(t *testing.T) {
|
||||
func TestLowercase(t *testing.T) {
|
||||
md := New(1)
|
||||
md["x-request-id"] = []string{"12345"}
|
||||
v := md.GetJoined("X-Request-Id")
|
||||
if v == "" {
|
||||
v, ok := md.GetJoined("X-Request-Id")
|
||||
if !ok || v == "" {
|
||||
t.Fatalf("metadata invalid %#+v", md)
|
||||
}
|
||||
}
|
||||
@@ -51,17 +51,29 @@ func TestMetadataSetMultiple(t *testing.T) {
|
||||
md := New(4)
|
||||
md.Set("key1", "val1", "key2", "val2")
|
||||
|
||||
if v := md.GetJoined("key1"); v != "val1" {
|
||||
if v, ok := md.GetJoined("key1"); !ok || v != "val1" {
|
||||
t.Fatalf("invalid kv %#+v", md)
|
||||
}
|
||||
if v := md.GetJoined("key2"); v != "val2" {
|
||||
if v, ok := md.GetJoined("key2"); !ok || v != "val2" {
|
||||
t.Fatalf("invalid kv %#+v", md)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppend(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx = AppendIncomingContext(ctx, "key1", "val1", "key2", "val2")
|
||||
md, ok := FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
t.Fatal("metadata empty")
|
||||
}
|
||||
if _, ok := md.Get("key1"); !ok {
|
||||
t.Fatal("key1 not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPairs(t *testing.T) {
|
||||
md := Pairs("key1", "val1", "key2", "val2")
|
||||
if v := md.Get("key1"); v == nil {
|
||||
if _, ok := md.Get("key1"); !ok {
|
||||
t.Fatal("key1 not found")
|
||||
}
|
||||
}
|
||||
@@ -85,7 +97,7 @@ func TestPassing(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("missing metadata from outgoing context")
|
||||
}
|
||||
if v := md.Get("Key1"); v == nil || v[0] != "Val1" {
|
||||
if v, ok := md.Get("Key1"); !ok || v[0] != "Val1" {
|
||||
t.Fatalf("invalid metadata value %#+v", md)
|
||||
}
|
||||
}
|
||||
@@ -115,21 +127,21 @@ func TestIterator(t *testing.T) {
|
||||
func TestMedataCanonicalKey(t *testing.T) {
|
||||
md := New(1)
|
||||
md.Set("x-request-id", "12345")
|
||||
v := md.GetJoined("x-request-id")
|
||||
if v == "" {
|
||||
v, ok := md.GetJoined("x-request-id")
|
||||
if !ok {
|
||||
t.Fatalf("failed to get x-request-id")
|
||||
} else if v != "12345" {
|
||||
t.Fatalf("invalid metadata value: %s != %s", "12345", v)
|
||||
}
|
||||
|
||||
v = md.GetJoined("X-Request-Id")
|
||||
if v == "" {
|
||||
v, ok = md.GetJoined("X-Request-Id")
|
||||
if !ok {
|
||||
t.Fatalf("failed to get x-request-id")
|
||||
} else if v != "12345" {
|
||||
t.Fatalf("invalid metadata value: %s != %s", "12345", v)
|
||||
}
|
||||
v = md.GetJoined("X-Request-ID")
|
||||
if v == "" {
|
||||
v, ok = md.GetJoined("X-Request-ID")
|
||||
if !ok {
|
||||
t.Fatalf("failed to get x-request-id")
|
||||
} else if v != "12345" {
|
||||
t.Fatalf("invalid metadata value: %s != %s", "12345", v)
|
||||
@@ -141,8 +153,8 @@ func TestMetadataSet(t *testing.T) {
|
||||
|
||||
md.Set("Key", "val")
|
||||
|
||||
val := md.GetJoined("Key")
|
||||
if val == "" {
|
||||
val, ok := md.GetJoined("Key")
|
||||
if !ok {
|
||||
t.Fatal("key Key not found")
|
||||
}
|
||||
if val != "val" {
|
||||
@@ -157,8 +169,8 @@ func TestMetadataDelete(t *testing.T) {
|
||||
}
|
||||
|
||||
md.Del("Baz")
|
||||
v := md.Get("Baz")
|
||||
if v != nil {
|
||||
_, ok := md.Get("Baz")
|
||||
if ok {
|
||||
t.Fatal("key Baz not deleted")
|
||||
}
|
||||
}
|
||||
@@ -257,6 +269,20 @@ func TestNewOutgoingContext(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendIncomingContext(t *testing.T) {
|
||||
md := New(1)
|
||||
md.Set("key1", "val1")
|
||||
ctx := AppendIncomingContext(context.TODO(), "key2", "val2")
|
||||
|
||||
nmd, ok := FromIncomingContext(ctx)
|
||||
if nmd == nil || !ok {
|
||||
t.Fatal("AppendIncomingContext not works")
|
||||
}
|
||||
if v, ok := nmd.GetJoined("key2"); !ok || v != "val2" {
|
||||
t.Fatal("AppendIncomingContext not works")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendOutgoingContext(t *testing.T) {
|
||||
md := New(1)
|
||||
md.Set("key1", "val1")
|
||||
@@ -266,7 +292,7 @@ func TestAppendOutgoingContext(t *testing.T) {
|
||||
if nmd == nil || !ok {
|
||||
t.Fatal("AppendOutgoingContext not works")
|
||||
}
|
||||
if v := nmd.GetJoined("key2"); v != "val2" {
|
||||
if v, ok := nmd.GetJoined("key2"); !ok || v != "val2" {
|
||||
t.Fatal("AppendOutgoingContext not works")
|
||||
}
|
||||
}
|
||||
|
@@ -489,74 +489,35 @@ func URLMap(query string) (map[string]interface{}, error) {
|
||||
return mp.(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
// FlattenMap flattens a nested map into a single-level map using dot notation for nested keys.
|
||||
// In case of key conflicts, all nested levels will be discarded in favor of the first-level key.
|
||||
//
|
||||
// Example #1:
|
||||
//
|
||||
// Input:
|
||||
// {
|
||||
// "user.name": "alex",
|
||||
// "user.document.id": "document_id"
|
||||
// "user.document.number": "document_number"
|
||||
// }
|
||||
// Output:
|
||||
// {
|
||||
// "user": {
|
||||
// "name": "alex",
|
||||
// "document": {
|
||||
// "id": "document_id"
|
||||
// "number": "document_number"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Example #2 (with conflicts):
|
||||
//
|
||||
// Input:
|
||||
// {
|
||||
// "user": "alex",
|
||||
// "user.document.id": "document_id"
|
||||
// "user.document.number": "document_number"
|
||||
// }
|
||||
// Output:
|
||||
// {
|
||||
// "user": "alex"
|
||||
// }
|
||||
func FlattenMap(input map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for k, v := range input {
|
||||
parts := strings.Split(k, ".")
|
||||
|
||||
if len(parts) == 1 {
|
||||
result[k] = v
|
||||
// FlattenMap expand key.subkey to nested map
|
||||
func FlattenMap(a map[string]interface{}) map[string]interface{} {
|
||||
// preprocess map
|
||||
nb := make(map[string]interface{}, len(a))
|
||||
for k, v := range a {
|
||||
ps := strings.Split(k, ".")
|
||||
if len(ps) == 1 {
|
||||
nb[k] = v
|
||||
continue
|
||||
}
|
||||
|
||||
current := result
|
||||
|
||||
for i, part := range parts {
|
||||
// last element in the path
|
||||
if i == len(parts)-1 {
|
||||
current[part] = v
|
||||
break
|
||||
}
|
||||
|
||||
// initialize map for current level if not exist
|
||||
if _, ok := current[part]; !ok {
|
||||
current[part] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if nested, ok := current[part].(map[string]interface{}); ok {
|
||||
current = nested // continue to the nested map
|
||||
} else {
|
||||
break // if current element is not a map, ignore it
|
||||
em := make(map[string]interface{})
|
||||
em[ps[len(ps)-1]] = v
|
||||
for i := len(ps) - 2; i > 0; i-- {
|
||||
nm := make(map[string]interface{})
|
||||
nm[ps[i]] = em
|
||||
em = nm
|
||||
}
|
||||
if vm, ok := nb[ps[0]]; ok {
|
||||
// nested map
|
||||
nm := vm.(map[string]interface{})
|
||||
for vk, vv := range em {
|
||||
nm[vk] = vv
|
||||
}
|
||||
nb[ps[0]] = nm
|
||||
} else {
|
||||
nb[ps[0]] = em
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
return nb
|
||||
}
|
||||
|
||||
/*
|
||||
|
@@ -6,7 +6,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
rutil "go.unistack.org/micro/v4/util/reflect"
|
||||
)
|
||||
|
||||
@@ -320,140 +319,3 @@ func TestIsZero(t *testing.T) {
|
||||
|
||||
// t.Logf("XX %#+v\n", ok)
|
||||
}
|
||||
|
||||
func TestFlattenMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input map[string]interface{}
|
||||
expected map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "empty map",
|
||||
input: map[string]interface{}{},
|
||||
expected: map[string]interface{}{},
|
||||
},
|
||||
{
|
||||
name: "nil map",
|
||||
input: nil,
|
||||
expected: map[string]interface{}{},
|
||||
},
|
||||
{
|
||||
name: "single level",
|
||||
input: map[string]interface{}{
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "two level",
|
||||
input: map[string]interface{}{
|
||||
"order_id": "order_id",
|
||||
"user.name": "username",
|
||||
"user.password": "password",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"order_id": "order_id",
|
||||
"user": map[string]interface{}{
|
||||
"name": "username",
|
||||
"password": "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "three level",
|
||||
input: map[string]interface{}{
|
||||
"order_id": "order_id",
|
||||
"user.name": "username",
|
||||
"user.password": "password",
|
||||
"user.document.id": "document_id",
|
||||
"user.document.number": "document_number",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"order_id": "order_id",
|
||||
"user": map[string]interface{}{
|
||||
"name": "username",
|
||||
"password": "password",
|
||||
"document": map[string]interface{}{
|
||||
"id": "document_id",
|
||||
"number": "document_number",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "four level",
|
||||
input: map[string]interface{}{
|
||||
"order_id": "order_id",
|
||||
"user.name": "username",
|
||||
"user.password": "password",
|
||||
"user.document.id": "document_id",
|
||||
"user.document.number": "document_number",
|
||||
"user.info.permissions.read": "available",
|
||||
"user.info.permissions.write": "available",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"order_id": "order_id",
|
||||
"user": map[string]interface{}{
|
||||
"name": "username",
|
||||
"password": "password",
|
||||
"document": map[string]interface{}{
|
||||
"id": "document_id",
|
||||
"number": "document_number",
|
||||
},
|
||||
"info": map[string]interface{}{
|
||||
"permissions": map[string]interface{}{
|
||||
"read": "available",
|
||||
"write": "available",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "key conflicts",
|
||||
input: map[string]interface{}{
|
||||
"user": "user",
|
||||
"user.name": "username",
|
||||
"user.password": "password",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"user": "user",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "overwriting conflicts",
|
||||
input: map[string]interface{}{
|
||||
"order_id": "order_id",
|
||||
"user.document.id": "document_id",
|
||||
"user.document.number": "document_number",
|
||||
"user.info.address": "address",
|
||||
"user.info.phone": "phone",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"order_id": "order_id",
|
||||
"user": map[string]interface{}{
|
||||
"document": map[string]interface{}{
|
||||
"id": "document_id",
|
||||
"number": "document_number",
|
||||
},
|
||||
"info": map[string]interface{}{
|
||||
"address": "address",
|
||||
"phone": "phone",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for range 100 { // need to exclude the impact of key order in the map on the test.
|
||||
require.Equal(t, tt.expected, rutil.FlattenMap(tt.input))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user