diff --git a/template/builder.go b/template/builder.go index 64530be62337de21dc5b93956df261ce5211a302..9944aab4f93631d849539fa74b1424f30a5aaa46 100644 --- a/template/builder.go +++ b/template/builder.go @@ -3,7 +3,10 @@ package template import ( "bytes" "context" - "text/template" + html "html/template" + "io" + "maps" + text "text/template" "git.perx.ru/perxis/perxis-go/pkg/collections" "git.perx.ru/perxis/perxis-go/pkg/content" @@ -11,38 +14,74 @@ import ( "git.perx.ru/perxis/perxis-go/pkg/spaces" ) +type Template interface { + Execute(w io.Writer, data any) error +} + type Builder struct { ctx context.Context cnt *content.Content SpaceID string EnvID string CollID string - data map[string]interface{} + data map[string]any // Для кеширования запросов space *spaces.Space environment *environments.Environment collection *collections.Collection + + template func(templ string) (Template, error) +} + +func NewBuilder(cnt *content.Content, space, env, coll string) *Builder { + b := &Builder{ + ctx: context.Background(), + cnt: cnt, + SpaceID: space, + EnvID: env, + CollID: coll, + } + b.template = func(templ string) (Template, error) { + t, err := text.New("main"). + Funcs(b.getFuncs()). + Parse(templ) + if err != nil { + return nil, err + } + return t, nil + } + return b } -func NewBuilder(cnt *content.Content, space, env, col string) *Builder { - return &Builder{ +func NewHTMLBuilder(cnt *content.Content, space, env, coll string) *Builder { + b := &Builder{ ctx: context.Background(), cnt: cnt, SpaceID: space, EnvID: env, - CollID: col, + CollID: coll, } + b.template = func(templ string) (Template, error) { + t, err := html.New("main"). + Funcs(b.getFuncs()). + Parse(templ) + if err != nil { + return nil, err + } + return t, nil + } + return b } -func (b *Builder) getFuncs() template.FuncMap { - return template.FuncMap{ +func (b *Builder) getFuncs() map[string]any { + return map[string]any{ "lookup": getLookup(b), "system": getSystem(b), } } -func (b *Builder) WithData(data map[string]interface{}) *Builder { +func (b *Builder) WithData(data map[string]any) *Builder { bld := *b bld.data = data return &bld @@ -51,7 +90,8 @@ func (b *Builder) WithData(data map[string]interface{}) *Builder { func (b *Builder) WithKV(kv ...any) *Builder { bld := *b if bld.data == nil { - bld.data = make(map[string]interface{}, 10) + //nolint:mnd // Количество аргументов делится на 2, так как они представлены в виде пар ключ-значение. + bld.data = make(map[string]any, len(kv)/2) } for i := 0; i < len(kv)-1; i += 2 { k, _ := kv[i].(string) @@ -63,7 +103,7 @@ func (b *Builder) WithKV(kv ...any) *Builder { return &bld } -func (b *Builder) GetData() map[string]interface{} { +func (b *Builder) GetData() map[string]any { return b.data } @@ -84,69 +124,68 @@ func (b *Builder) Context() context.Context { return b.ctx } -func (b *Builder) Template() *template.Template { - return template.New("main").Funcs(b.getFuncs()) +func (b *Builder) Template(text string) (Template, error) { + return b.template(text) } -func (b *Builder) Execute(str string, data ...any) (string, error) { - t := b.Template() - buf := new(bytes.Buffer) - t, err := t.Parse(str) +func (b *Builder) Execute(templ string, data ...any) (string, error) { + t, err := b.Template(templ) if err != nil { return "", err } + buf := new(bytes.Buffer) if err = t.Execute(buf, b.getData(data...)); err != nil { return "", err } return buf.String(), nil } -func (b *Builder) ExecuteList(str []string, data ...any) ([]string, error) { - t := b.Template() - result := make([]string, len(str)) - buffer := new(bytes.Buffer) - for i, tmpl := range str { - if tmpl == "" { - continue - } - t, err := t.Parse(tmpl) +func (b *Builder) ExecuteList(templs []string, data ...any) ([]string, error) { + result := make([]string, len(templs)) + buf := new(bytes.Buffer) + d := b.getData(data...) + + for i, templ := range templs { + t, err := b.Template(templ) if err != nil { - return []string{}, err + return nil, err } - if err = t.Execute(buffer, b.getData(data...)); err != nil { - return []string{}, err + + buf.Reset() + err = t.Execute(buf, d) + if err != nil { + return nil, err } - result[i] = buffer.String() - buffer.Reset() + + result[i] = buf.String() } + return result, nil } -func (b *Builder) ExecuteMap(str map[string]interface{}, data ...any) (map[string]interface{}, error) { - result := make(map[string]interface{}, len(str)) - for k, v := range str { +func (b *Builder) ExecuteMap(templMap map[string]any, data ...any) (map[string]any, error) { + result := make(map[string]any, len(templMap)) + d := b.getData(data...) + + for k, v := range templMap { switch t := v.(type) { case string: - value, err := b.Execute(t, data...) + value, err := b.Execute(t, d) if err != nil { return nil, err } - v = value + result[k] = value case []string: - values, err := b.ExecuteList(append([]string{k}, t...), data...) + var err error + result[k], err = b.ExecuteList(t, d) if err != nil { return nil, err } - k = values[0] - vv := make([]interface{}, 0, len(t)) - for _, val := range values[1:] { - vv = append(vv, val) - } - v = vv + default: + result[k] = v } - - result[k] = v } + return result, nil } @@ -155,25 +194,19 @@ func (b *Builder) getData(data ...any) any { return b.data } - var res map[string]interface{} + res := maps.Clone(b.data) + if res == nil { + res = make(map[string]any) + } for _, v := range data { - if m, ok := v.(map[string]interface{}); ok && b.data != nil { - res = mergeMaps(b.data, m) + if m, ok := v.(map[string]any); ok { + maps.Copy(res, m) } } - if res != nil { - return res - } - return data[0] -} - -func mergeMaps(in ...map[string]interface{}) map[string]interface{} { - out := make(map[string]interface{}) - for _, i := range in { - for k, v := range i { - out[k] = v - } + if len(res) == 0 { + return data[0] } - return out + + return res } diff --git a/template/builder_test.go b/template/builder_test.go index cd6a0807567a8618005eb0acc37fbbef84864ee4..48fdb2cb0826c6d392eaed0cb725f7ee5a6651df 100644 --- a/template/builder_test.go +++ b/template/builder_test.go @@ -2,48 +2,89 @@ package template import ( "context" - "errors" "testing" - "git.perx.ru/perxis/perxis-go/pkg/collections" - colsmocks "git.perx.ru/perxis/perxis-go/pkg/collections/mocks" "git.perx.ru/perxis/perxis-go/pkg/content" - "git.perx.ru/perxis/perxis-go/pkg/environments" - envsmocks "git.perx.ru/perxis/perxis-go/pkg/environments/mocks" + "git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/items" mocksitems "git.perx.ru/perxis/perxis-go/pkg/items/mocks" - "git.perx.ru/perxis/perxis-go/pkg/spaces" - spsmocks "git.perx.ru/perxis/perxis-go/pkg/spaces/mocks" "github.com/stretchr/testify/assert" ) func TestBuilder_Execute(t *testing.T) { tests := []struct { - name string - SpaceID string - EnvID string - CollID string - str string - data any - want any - wantErr bool + name string + SpaceID string + EnvID string + CollID string + template string + data any + want any + htmlBuilder bool + wantErr bool getCnt func(t *testing.T) *content.Content }{ - {name: "error", str: "hello {{ .a }}", data: "world", want: "", wantErr: true}, - {name: "empty", str: "", data: "", want: "", wantErr: false}, - {name: "#1", str: "hello {{ . }}", data: "world", want: "hello world", wantErr: false}, - {name: "#2", str: "{{ . }}", data: "world", want: "world", wantErr: false}, - {name: "#3 ", str: "", data: "world", want: "", wantErr: false}, - {name: "#4 ", str: "hello", data: "world", want: "hello", wantErr: false}, { - name: "lookup", - SpaceID: "space", - EnvID: "env", - str: "hello, {{ lookup \"secrets.dev.key\" }}", - data: "", - want: "hello, Luk", - wantErr: false, + name: "invalid data", + template: "hello {{ .a }}", + data: "world", + want: "", + wantErr: true, + }, + { + name: "invalid template", + template: "hello {{ . ", + data: "world", + want: "", + wantErr: true, + }, + { + name: "empty template", + template: "", + data: "world", + want: "", + wantErr: false, + }, + { + name: "empty data", + template: "{{ . }}", + data: "", + want: "", + wantErr: false, + }, + { + name: "success", + template: "hello {{ . }}", + data: "world", + want: "hello world", + wantErr: false, + }, + { + name: "success with html builder", + template: "<span class=\"comment\">{{ . }}</span>", + data: "<script>alert(localStorage.getItem('secret'))</script>", + want: "<span class=\"comment\">" + + "<script>alert(localStorage.getItem('secret'))</script>" + + "</span>", + htmlBuilder: true, + wantErr: false, + }, + { + name: "template as plain text", + template: "hello", + data: "world", + want: "hello", + wantErr: false, + }, + { + name: "lookup function", + SpaceID: "space", + EnvID: "env", + template: "hello, {{ lookup \"secrets.dev.key\" }}", + data: "", + want: "hello, Luk", + wantErr: false, getCnt: func(t *testing.T) *content.Content { itemsSvc := mocksitems.NewItems(t) itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev"). @@ -52,7 +93,7 @@ func TestBuilder_Execute(t *testing.T) { SpaceID: "space", EnvID: "env", CollectionID: "secrets", - Data: map[string]interface{}{ + Data: map[string]any{ "id": "dev", "key": "Luk", }, @@ -61,13 +102,13 @@ func TestBuilder_Execute(t *testing.T) { }, }, { - name: "lookup with slice", - SpaceID: "space", - EnvID: "env", - str: "numbers {{ lookup \"secrets.dev.slice\" }}", - data: "", - want: "numbers [1 2 3]", - wantErr: false, + name: "lookup function with slice", + SpaceID: "space", + EnvID: "env", + template: "numbers {{ lookup \"secrets.dev.slice\" }}", + data: "", + want: "numbers [1 2 3]", + wantErr: false, getCnt: func(t *testing.T) *content.Content { itemsSvc := mocksitems.NewItems(t) itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev"). @@ -76,7 +117,7 @@ func TestBuilder_Execute(t *testing.T) { SpaceID: "space", EnvID: "env", CollectionID: "secrets", - Data: map[string]interface{}{ + Data: map[string]any{ "id": "dev", "slice": []int{1, 2, 3}, }, @@ -85,34 +126,13 @@ func TestBuilder_Execute(t *testing.T) { }, }, { - name: "lookup with empty Data", - SpaceID: "space", - EnvID: "env", - str: "numbers {{ lookup \"secrets.dev.slice\" }}", - data: "", - want: "numbers <no value>", - wantErr: false, - getCnt: func(t *testing.T) *content.Content { - itemsSvc := mocksitems.NewItems(t) - itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev"). - Return(&items.Item{ - ID: "dev", - SpaceID: "space", - EnvID: "env", - CollectionID: "secrets", - Data: map[string]interface{}{}, - }, nil).Once() - return &content.Content{Items: itemsSvc} - }, - }, - { - name: "lookup with incorrect field", - SpaceID: "space", - EnvID: "env", - str: "hello {{ lookup \"secrets.dev.incorrect\" }}", - data: "", - want: "hello <no value>", - wantErr: false, + name: "lookup function with unknown field", + SpaceID: "space", + EnvID: "env", + template: "numbers {{ lookup \"secrets.dev.slice\" }}", + data: "", + want: "numbers <no value>", + wantErr: false, getCnt: func(t *testing.T) *content.Content { itemsSvc := mocksitems.NewItems(t) itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev"). @@ -121,22 +141,19 @@ func TestBuilder_Execute(t *testing.T) { SpaceID: "space", EnvID: "env", CollectionID: "secrets", - Data: map[string]interface{}{ - "id": "dev", - "key": "1234", - }, + Data: map[string]any{}, }, nil).Once() return &content.Content{Items: itemsSvc} }, }, { - name: "lookup not found", - SpaceID: "space", - EnvID: "env", - str: "hello {{ lookup \"secrets.prod.pass\" }}", - data: "", - want: "", - wantErr: true, + name: "lookup function not found item", + SpaceID: "space", + EnvID: "env", + template: "hello {{ lookup \"secrets.prod.pass\" }}", + data: "", + want: "", + wantErr: true, getCnt: func(t *testing.T) *content.Content { itemsSvc := mocksitems.NewItems(t) itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "prod"). @@ -144,34 +161,14 @@ func TestBuilder_Execute(t *testing.T) { return &content.Content{Items: itemsSvc} }, }, - {name: "lookup without itemID", SpaceID: "space", EnvID: "env", str: "hello {{ lookup \"secrets.pass\" }}", data: "", want: "", wantErr: true}, - {name: "system#1", SpaceID: "space", str: "hello {{ system.SpaceID }}", data: "", want: "hello space", wantErr: false}, - {name: "system space", SpaceID: "space", getCnt: func(t *testing.T) *content.Content { - spsSvc := spsmocks.NewSpaces(t) - spsSvc.On("Get", context.Background(), "space").Return(&spaces.Space{Description: "description"}, nil).Once() - return &content.Content{Spaces: spsSvc} - }, str: "{{ system.Space.Description }}", want: "description", wantErr: false}, - {name: "system environment", SpaceID: "space", EnvID: "env", getCnt: func(t *testing.T) *content.Content { - envsSvc := envsmocks.NewEnvironments(t) - envsSvc.On("Get", context.Background(), "space", "env").Return(&environments.Environment{Aliases: []string{"master"}}, nil).Once() - return &content.Content{Environments: envsSvc} - }, str: "{{ system.Environment.Aliases }}", want: "[master]", wantErr: false}, { - name: "system collection", - SpaceID: "space", - EnvID: "env", - CollID: "col", - getCnt: func(t *testing.T) *content.Content { - collsSvc := colsmocks.NewCollections(t) - collsSvc.On("Get", context.Background(), "space", "env", "col"). - Return(&collections.Collection{Name: "cars"}, nil).Once() - return &content.Content{Collections: collsSvc} - }, - str: "{{ system.Collection.Name }}", - want: "cars", - wantErr: false, + name: "system function", + SpaceID: "space", + template: "hello {{ system.SpaceID }}", + data: "", + want: "hello space", + wantErr: false, }, - {name: "system without account", SpaceID: "space", str: "hello {{ system.Organization.Name }}", want: "", wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -180,16 +177,14 @@ func TestBuilder_Execute(t *testing.T) { cnt = tt.getCnt(t) } - b := &Builder{ - ctx: context.Background(), - cnt: cnt, - SpaceID: tt.SpaceID, - EnvID: tt.EnvID, - CollID: tt.CollID, + var b *Builder + if tt.htmlBuilder { + b = NewHTMLBuilder(cnt, tt.SpaceID, tt.EnvID, tt.CollID) + } else { + b = NewBuilder(cnt, tt.SpaceID, tt.EnvID, tt.CollID) } - - got, err := b.Execute(tt.str, tt.data) - if tt.wantErr == true { + got, err := b.Execute(tt.template, tt.data) + if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) @@ -201,67 +196,110 @@ func TestBuilder_Execute(t *testing.T) { func TestBuilder_ExecuteList(t *testing.T) { tests := []struct { - name string - SpaceID string - EnvID string - str []string - data any - want []string - wantErr bool + name string + templs []string + data any + want []string + htmlBuilder bool + assertError assert.ErrorAssertionFunc - itemsCall func(itemsSvc *mocksitems.Items) + getCnt func(t *testing.T) *content.Content }{ - {name: "error", SpaceID: "space", EnvID: "env", str: []string{"hello { . }}", "go {{ . }"}, data: "world", want: []string{}, wantErr: true}, - {name: "empty", SpaceID: "space", EnvID: "env", str: []string{""}, data: "world", want: []string{""}, wantErr: false}, - {name: "#1", SpaceID: "space", EnvID: "env", str: []string{"hello {{ . }}", "go {{ . }}"}, data: "world", want: []string{"hello world", "go world"}, wantErr: false}, - {name: "#2", SpaceID: "space", EnvID: "env", str: []string{"{{ . }}"}, data: "world", want: []string{"world"}, wantErr: false}, - {name: "#3 ", SpaceID: "space", EnvID: "env", str: []string{""}, data: "world", want: []string{""}, wantErr: false}, - {name: "#4 ", SpaceID: "space", EnvID: "env", str: []string{"hello"}, data: "world", want: []string{"hello"}, wantErr: false}, - {name: "lookup", SpaceID: "space", EnvID: "env", str: []string{"hello {{ lookup \"secrets.dev.key\" }}"}, data: "", want: []string{"hello 1234"}, wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) { - itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ - ID: "dev", - SpaceID: "space", - EnvID: "env", - CollectionID: "secrets", - Data: map[string]interface{}{ - "id": "dev", - "key": "1234", - }, - }, nil).Once() - }}, - {name: "lookup with incorrect field", SpaceID: "space", EnvID: "env", str: []string{"hello {{ lookup \"secrets.dev.incorrect\" }}"}, data: "", want: []string{"hello <no value>"}, wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) { - itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ - ID: "dev", - SpaceID: "space", - EnvID: "env", - CollectionID: "secrets", - Data: map[string]interface{}{ - "id": "dev", - "key": "1234", - }, - }, nil).Once() - }}, - {name: "system ", SpaceID: "space", EnvID: "env", str: []string{"hello {{ system.SpaceID }}"}, data: "", want: []string{"hello space"}, wantErr: false}, + { + name: "invalid data", + templs: []string{ + "hello {{ .a }}", + }, + data: "world", + want: nil, + assertError: assert.Error, + }, + { + name: "invalid template", + templs: []string{ + "hello {{ . ", + }, + data: "world", + want: nil, + assertError: assert.Error, + }, + { + name: "empty template", + templs: []string{ + "", + }, + data: "world", + want: []string{ + "", + }, + assertError: assert.NoError, + }, + { + name: "empty data", + templs: []string{ + "{{ . }}", + }, + data: "", + want: []string{ + "", + }, + assertError: assert.NoError, + }, + { + name: "success", + templs: []string{ + "hello {{ . }}", + "world {{ . }}", + }, + data: "world", + want: []string{ + "hello world", + "world world", + }, + assertError: assert.NoError, + }, + { + name: "success with html builder", + templs: []string{ + "<span class=\"comment\">{{ . }}</span>", + }, + data: "<script>alert(localStorage.getItem('secret'))</script>", + want: []string{ + "<span class=\"comment\">" + + "<script>alert(localStorage.getItem('secret'))</script>" + + "</span>", + }, + htmlBuilder: true, + assertError: assert.NoError, + }, + { + name: "template as plain text", + templs: []string{ + "hello", + }, + data: "world", + want: []string{ + "hello", + }, + assertError: assert.NoError, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - itemsSvc := mocksitems.NewItems(t) - if tt.itemsCall != nil { - tt.itemsCall(itemsSvc) - } - b := &Builder{ - ctx: context.Background(), - cnt: &content.Content{Items: itemsSvc}, - SpaceID: tt.SpaceID, - EnvID: tt.EnvID, + var cnt *content.Content + if tt.getCnt != nil { + cnt = tt.getCnt(t) } - got, err := b.ExecuteList(tt.str, tt.data) - if tt.wantErr == true { - assert.Error(t, err) + var b *Builder + if tt.htmlBuilder { + b = NewHTMLBuilder(cnt, "space_id", "env_id", "coll_id") } else { - assert.NoError(t, err) + b = NewBuilder(cnt, "space_id", "env_id", "coll_id") } + + got, err := b.ExecuteList(tt.templs, tt.data) + tt.assertError(t, err) assert.Equal(t, tt.want, got) }) } @@ -269,68 +307,276 @@ func TestBuilder_ExecuteList(t *testing.T) { func TestBuilder_ExecuteMap(t *testing.T) { tests := []struct { - name string - SpaceID string - EnvID string - str map[string]interface{} - data any - want map[string]interface{} - wantErr bool + name string + SpaceID string + EnvID string + templateMap map[string]any + data any + want map[string]any + wantErr bool itemsCall func(itemsSvc *mocksitems.Items) }{ - {name: "error", SpaceID: "space", EnvID: "env", str: map[string]interface{}{"hello": "{{ . }"}, data: "world", want: nil, wantErr: true}, - {name: "empty", SpaceID: "space", EnvID: "env", str: map[string]interface{}{}, data: "", want: map[string]interface{}{}, wantErr: false}, - {name: "#1", SpaceID: "space", EnvID: "env", str: map[string]interface{}{"hello": "{{ . }}"}, data: "world", want: map[string]interface{}{"hello": "world"}, wantErr: false}, - {name: "#2", SpaceID: "space", EnvID: "env", str: map[string]interface{}{"hello": "{{ . }}", "go": "{{ . }}"}, data: "world", want: map[string]interface{}{"hello": "world", "go": "world"}, wantErr: false}, - {name: "#3 ", SpaceID: "space", EnvID: "env", str: map[string]interface{}{}, data: "world", want: map[string]interface{}{}, wantErr: false}, - {name: "#4 ", SpaceID: "space", EnvID: "env", str: map[string]interface{}{"a": "b"}, data: "world", want: map[string]interface{}{"a": "b"}, wantErr: false}, - {name: "lookup ", SpaceID: "space", EnvID: "env", str: map[string]interface{}{"hello": "{{ lookup \"secrets.dev.key\" }}"}, data: "", want: map[string]interface{}{"hello": "1234"}, wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) { - itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ - ID: "dev", - SpaceID: "space", - EnvID: "env", - CollectionID: "secrets", - Data: map[string]interface{}{ - "id": "dev", - "key": "1234", + { + name: "invalid template", + SpaceID: "space", + EnvID: "env", + templateMap: map[string]any{"hello": "{{ . }"}, + data: "world", + want: nil, + wantErr: true, + }, + { + name: "empty data", + SpaceID: "space", + EnvID: "env", + templateMap: map[string]any{}, + data: "", + want: map[string]any{}, + wantErr: false, + }, + { + name: "one template", + SpaceID: "space", + EnvID: "env", + templateMap: map[string]any{ + "hello": "{{ . }}", + }, + data: "world", + want: map[string]any{ + "hello": "world", + }, + wantErr: false, + }, + { + name: "one template with many value templates", + SpaceID: "space", + EnvID: "env", + templateMap: map[string]any{ + "hello": []string{ + "{{ .foo }}", + "{{ .bar }}", + }, + }, + data: map[string]any{ + "foo": "hello", + "bar": "world", + }, + want: map[string]any{ + "hello": []string{ + "hello", + "world", + }, + }, + wantErr: false, + }, + { + name: "many template", + SpaceID: "space", + EnvID: "env", + templateMap: map[string]any{ + "hello": "{{ . }}", + "go": "{{ . }}", + }, + data: "world", + want: map[string]any{ + "hello": "world", + "go": "world", + }, + wantErr: false, + }, + { + name: "template in key with one value", + SpaceID: "space", + EnvID: "env", + templateMap: map[string]any{ + "{{ .key }}": "value", + }, + data: map[string]any{ + "key": "foo", + }, + want: map[string]any{ + "{{ .key }}": "value", + }, + wantErr: false, + }, + { + name: "template in key with many values", + SpaceID: "space", + EnvID: "env", + templateMap: map[string]any{ + "{{ .key }}": []string{ + "value", }, - }, nil).Once() - }}, - {name: "lookup with incorrect field", SpaceID: "space", EnvID: "env", str: map[string]interface{}{"hello": "{{ lookup \"secrets.dev.incorrect\" }}"}, data: "", want: map[string]interface{}{"hello": "<no value>"}, wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) { - itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ - ID: "dev", - SpaceID: "space", - EnvID: "env", - CollectionID: "secrets", - Data: map[string]interface{}{ - "id": "dev", - "key": "1234", + }, + data: map[string]any{ + "key": "foo", + }, + want: map[string]any{ + "{{ .key }}": []string{ + "value", }, - }, nil).Once() - }}, - {name: "system ", SpaceID: "space", EnvID: "env", str: map[string]interface{}{"hello": "{{ system.SpaceID }}"}, data: "", want: map[string]interface{}{"hello": "space"}, wantErr: false}, + }, + wantErr: false, + }, + { + name: "template as plain text", + SpaceID: "space", + EnvID: "env", + templateMap: map[string]any{ + "a": "b", + }, + data: "world", + want: map[string]any{ + "a": "b", + }, + wantErr: false, + }, + { + name: "template is not template", + SpaceID: "space", + EnvID: "env", + templateMap: map[string]any{ + "a": "b", + "b": []int{1, 2, 3}, + }, + data: "world", + want: map[string]any{ + "a": "b", + "b": []int{1, 2, 3}, + }, + wantErr: false, + }, + { + name: "lookup function", + SpaceID: "space", + EnvID: "env", + templateMap: map[string]any{ + "hello": "{{ lookup \"secrets.dev.key\" }}", + }, + want: map[string]any{ + "hello": "1234", + }, + wantErr: false, + itemsCall: func(itemsSvc *mocksitems.Items) { + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ + ID: "dev", + SpaceID: "space", + EnvID: "env", + CollectionID: "secrets", + Data: map[string]any{ + "id": "dev", + "key": "1234", + }, + }, nil).Once() + }, + }, + { + name: "lookup function with unknown field", + SpaceID: "space", + EnvID: "env", + templateMap: map[string]any{"hello": "{{ lookup \"secrets.dev.incorrect\" }}"}, + want: map[string]any{"hello": "<no value>"}, + wantErr: false, + itemsCall: func(itemsSvc *mocksitems.Items) { + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ + ID: "dev", + SpaceID: "space", + EnvID: "env", + CollectionID: "secrets", + Data: map[string]any{ + "id": "dev", + "key": "1234", + }, + }, nil).Once() + }, + }, + { + name: "system function", + SpaceID: "space", + EnvID: "env", + templateMap: map[string]any{ + "hello": "{{ system.SpaceID }}", + }, + want: map[string]any{ + "hello": "space", + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - itemsSvc := mocksitems.NewItems(t) + itemsSvc := &mocksitems.Items{} if tt.itemsCall != nil { tt.itemsCall(itemsSvc) } - b := &Builder{ - ctx: context.Background(), - cnt: &content.Content{Items: itemsSvc}, - SpaceID: tt.SpaceID, - EnvID: tt.EnvID, - } + b := NewBuilder(&content.Content{Items: itemsSvc}, tt.SpaceID, tt.EnvID, "") - got, err := b.ExecuteMap(tt.str, tt.data) + got, err := b.ExecuteMap(tt.templateMap, tt.data) if tt.wantErr == true { assert.Error(t, err) } else { assert.NoError(t, err) } assert.Equal(t, tt.want, got) + itemsSvc.AssertExpectations(t) }) } } + +func TestBuilder_getData(t *testing.T) { + t.Run("empty data", func(t *testing.T) { + builder := NewBuilder(nil, "space_id", "env_id", "coll_id") + assert.Equal(t, + map[string]any(nil), + builder.getData(), + ) + }) + t.Run("with data", func(t *testing.T) { + builder := NewBuilder(nil, "space_id", "env_id", "coll_id"). + WithData(map[string]any{ + "foo": "bar", + }) + assert.Equal(t, + map[string]any{ + "foo": "bar", + }, + builder.getData(), + ) + }) + t.Run("merge data", func(t *testing.T) { + builder := NewBuilder(nil, "space_id", "env_id", "coll_id") + assert.Equal(t, + map[string]any{ + "a": 1, + "b": 2, + }, + builder.getData( + map[string]any{}, + map[string]any{"a": 1}, + map[string]any{"b": 2}, + []string{"a", "b"}, + "string", + ), + ) + }) + t.Run("merge with original data", func(t *testing.T) { + builder := NewBuilder(nil, "space_id", "env_id", "coll_id"). + WithData(map[string]any{ + "foo": "bar", + "baz": "qux", + }) + assert.Equal(t, + map[string]any{ + "a": 1, + "b": 2, + "foo": "bar", + "baz": "qux", + }, + builder.getData( + map[string]any{"a": 1}, + map[string]any{"b": 2}, + ), + ) + }) +} diff --git a/template/funcs.go b/template/funcs.go index 0c320ad139e964f002b691ce097b24e70e6cfaf3..55eab67682f799ca671e1650ebc6a45546722c20 100644 --- a/template/funcs.go +++ b/template/funcs.go @@ -9,11 +9,11 @@ import ( // getLookup возвращает функцию для шаблонизатора для получения значений из записи коллекции // name указывается в виде "<collection id>.<item id>.<field>" // Использование в шаблонах: {{ lookup "secrets.key.value" }} -func getLookup(b *Builder) any { +func getLookup(b *Builder) func(string) (any, error) { return func(name string) (any, error) { parsedName := strings.Split(name, ".") if len(parsedName) < 3 { - return "", errors.Errorf("incorrect parameter \"%s\"", name) + return nil, errors.Errorf("incorrect parameter \"%s\"", name) } collectionID := parsedName[0] @@ -21,7 +21,7 @@ func getLookup(b *Builder) any { field := parsedName[2] item, err := b.cnt.Items.Get(b.Context(), b.SpaceID, b.EnvID, collectionID, itemID) if err != nil { - return "", errors.Wrapf(err, "failed to get \"%s\"") + return nil, errors.Wrapf(err, "failed to get \"%s\"") } if len(item.Data) > 0 { @@ -36,7 +36,7 @@ func getLookup(b *Builder) any { // getSys возвращает функцию получения System // Использование в шаблонах: {{ system.SpaceID }} -func getSystem(b *Builder) any { +func getSystem(b *Builder) func() *System { return func() *System { return &System{builder: b} } diff --git a/template/funcs_test.go b/template/funcs_test.go new file mode 100644 index 0000000000000000000000000000000000000000..af4a7717328d8d8de3f3426136dfb1d91f0ba135 --- /dev/null +++ b/template/funcs_test.go @@ -0,0 +1,267 @@ +package template + +import ( + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/collections" + mockscolls "git.perx.ru/perxis/perxis-go/pkg/collections/mocks" + "git.perx.ru/perxis/perxis-go/pkg/content" + "git.perx.ru/perxis/perxis-go/pkg/environments" + mocksenvs "git.perx.ru/perxis/perxis-go/pkg/environments/mocks" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/items" + mocksitems "git.perx.ru/perxis/perxis-go/pkg/items/mocks" + "git.perx.ru/perxis/perxis-go/pkg/spaces" + mocksspaces "git.perx.ru/perxis/perxis-go/pkg/spaces/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func Test_getLookup(t *testing.T) { + tests := []struct { + name string + setup func(svc *mocksitems.Items) + input string + want any + assertError assert.ErrorAssertionFunc + }{ + { + name: "empty input", + input: "", + assertError: assert.Error, + }, + { + name: "invalid format", + input: "coll.key", + assertError: assert.Error, + }, + { + name: "success", + setup: func(svc *mocksitems.Items) { + svc.On("Get", mock.Anything, "space_id", "env_id", "coll", "item"). + Return(&items.Item{ + Data: map[string]any{ + "key": "value", + }, + }, nil).Once() + }, + input: "coll.item.key", + want: "value", + assertError: assert.NoError, + }, + { + name: "non-existing field", + setup: func(svc *mocksitems.Items) { + svc.On("Get", mock.Anything, "space_id", "env_id", "coll", "item"). + Return(&items.Item{ + Data: map[string]any{ + "key": "value", + }, + }, nil).Once() + }, + input: "coll.item.unknown", + want: nil, + assertError: assert.NoError, + }, + { + name: "existing field with empty value", + setup: func(svc *mocksitems.Items) { + svc.On("Get", mock.Anything, "space_id", "env_id", "coll", "item"). + Return(&items.Item{ + Data: map[string]any{ + "key": "", + }, + }, nil).Once() + }, + input: "coll.item.key", + want: "", + assertError: assert.NoError, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + svc := mocksitems.NewItems(t) + if tc.setup != nil { + tc.setup(svc) + } + b := NewBuilder(&content.Content{Items: svc}, "space_id", "env_id", "coll_id") + + lookup := getLookup(b) + got, err := lookup(tc.input) + tc.assertError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} + +func Test_getSystem(t *testing.T) { + t.Run("get space id", func(t *testing.T) { + spacesSvc := mocksspaces.NewSpaces(t) + envsSvc := mocksenvs.NewEnvironments(t) + collsSvc := mockscolls.NewCollections(t) + b := NewBuilder(&content.Content{ + Spaces: spacesSvc, + Environments: envsSvc, + Collections: collsSvc, + }, "space_id", "env_id", "coll_id") + system := getSystem(b) + assert.Equal(t, "space_id", system().SpaceID()) + }) + t.Run("get environment id", func(t *testing.T) { + spacesSvc := mocksspaces.NewSpaces(t) + envsSvc := mocksenvs.NewEnvironments(t) + collsSvc := mockscolls.NewCollections(t) + b := NewBuilder(&content.Content{ + Spaces: spacesSvc, + Environments: envsSvc, + Collections: collsSvc, + }, "space_id", "env_id", "coll_id") + system := getSystem(b) + assert.Equal(t, "env_id", system().EnvID()) + }) + t.Run("get collection id", func(t *testing.T) { + spacesSvc := mocksspaces.NewSpaces(t) + envsSvc := mocksenvs.NewEnvironments(t) + collsSvc := mockscolls.NewCollections(t) + b := NewBuilder(&content.Content{ + Spaces: spacesSvc, + Environments: envsSvc, + Collections: collsSvc, + }, "space_id", "env_id", "coll_id") + system := getSystem(b) + assert.Equal(t, "coll_id", system().CollectionID()) + }) + t.Run("get space", func(t *testing.T) { + t.Run("success", func(t *testing.T) { + spacesSvc := mocksspaces.NewSpaces(t) + spacesSvc.On("Get", mock.Anything, "space_id"). + Return(&spaces.Space{ + ID: "space_id", + OrgID: "org_id", + Name: "Space", + }, nil). + Once() + envsSvc := mocksenvs.NewEnvironments(t) + collsSvc := mockscolls.NewCollections(t) + b := NewBuilder(&content.Content{ + Spaces: spacesSvc, + Environments: envsSvc, + Collections: collsSvc, + }, "space_id", "env_id", "coll_id") + system := getSystem(b) + got, err := system().Space() + require.NoError(t, err) + assert.Equal(t, &spaces.Space{ + ID: "space_id", + OrgID: "org_id", + Name: "Space", + }, got) + }) + t.Run("failure", func(t *testing.T) { + spacesSvc := mocksspaces.NewSpaces(t) + spacesSvc.On("Get", mock.Anything, "space_id"). + Return(nil, errors.New("some error")). + Once() + envsSvc := mocksenvs.NewEnvironments(t) + collsSvc := mockscolls.NewCollections(t) + b := NewBuilder(&content.Content{ + Spaces: spacesSvc, + Environments: envsSvc, + Collections: collsSvc, + }, "space_id", "env_id", "coll_id") + system := getSystem(b) + got, err := system().Space() + require.Error(t, err) + assert.Nil(t, got) + }) + }) + t.Run("get environment", func(t *testing.T) { + t.Run("success", func(t *testing.T) { + spacesSvc := mocksspaces.NewSpaces(t) + envsSvc := mocksenvs.NewEnvironments(t) + envsSvc.On("Get", mock.Anything, "space_id", "env_id"). + Return(&environments.Environment{ + ID: "env_id", + SpaceID: "space_id", + }, nil). + Once() + collsSvc := mockscolls.NewCollections(t) + b := NewBuilder(&content.Content{ + Spaces: spacesSvc, + Environments: envsSvc, + Collections: collsSvc, + }, "space_id", "env_id", "coll_id") + system := getSystem(b) + got, err := system().Environment() + require.NoError(t, err) + assert.Equal(t, &environments.Environment{ + ID: "env_id", + SpaceID: "space_id", + }, got) + }) + t.Run("failure", func(t *testing.T) { + spacesSvc := mocksspaces.NewSpaces(t) + envsSvc := mocksenvs.NewEnvironments(t) + envsSvc.On("Get", mock.Anything, "space_id", "env_id"). + Return(nil, errors.New("some error")). + Once() + collsSvc := mockscolls.NewCollections(t) + b := NewBuilder(&content.Content{ + Spaces: spacesSvc, + Environments: envsSvc, + Collections: collsSvc, + }, "space_id", "env_id", "coll_id") + system := getSystem(b) + got, err := system().Environment() + require.Error(t, err) + assert.Nil(t, got) + }) + }) + t.Run("get collection", func(t *testing.T) { + t.Run("success", func(t *testing.T) { + spacesSvc := mocksspaces.NewSpaces(t) + envsSvc := mocksenvs.NewEnvironments(t) + collsSvc := mockscolls.NewCollections(t) + collsSvc.On("Get", mock.Anything, "space_id", "env_id", "coll_id"). + Return(&collections.Collection{ + ID: "coll_id", + SpaceID: "space_id", + EnvID: "env_id", + Name: "Collection", + }, nil). + Once() + b := NewBuilder(&content.Content{ + Spaces: spacesSvc, + Environments: envsSvc, + Collections: collsSvc, + }, "space_id", "env_id", "coll_id") + system := getSystem(b) + got, err := system().Collection() + require.NoError(t, err) + assert.Equal(t, &collections.Collection{ + ID: "coll_id", + SpaceID: "space_id", + EnvID: "env_id", + Name: "Collection", + }, got) + }) + t.Run("failure", func(t *testing.T) { + spacesSvc := mocksspaces.NewSpaces(t) + envsSvc := mocksenvs.NewEnvironments(t) + collsSvc := mockscolls.NewCollections(t) + collsSvc.On("Get", mock.Anything, "space_id", "env_id", "coll_id"). + Return(nil, errors.New("some error")). + Once() + b := NewBuilder(&content.Content{ + Spaces: spacesSvc, + Environments: envsSvc, + Collections: collsSvc, + }, "space_id", "env_id", "coll_id") + system := getSystem(b) + got, err := system().Collection() + require.Error(t, err) + assert.Nil(t, got) + }) + }) +}