Skip to content
Snippets Groups Projects
Commit f78895aa authored by Semyon Krestyaninov's avatar Semyon Krestyaninov :dog2: Committed by Pavel Antonov
Browse files

feat: Добавлена возможность создания template.Builder для текста или HTML

parent ecb2f118
No related branches found
No related tags found
No related merge requests found
...@@ -3,7 +3,10 @@ package template ...@@ -3,7 +3,10 @@ package template
import ( import (
"bytes" "bytes"
"context" "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/collections"
"git.perx.ru/perxis/perxis-go/pkg/content" "git.perx.ru/perxis/perxis-go/pkg/content"
...@@ -11,38 +14,74 @@ import ( ...@@ -11,38 +14,74 @@ import (
"git.perx.ru/perxis/perxis-go/pkg/spaces" "git.perx.ru/perxis/perxis-go/pkg/spaces"
) )
type Template interface {
Execute(w io.Writer, data any) error
}
type Builder struct { type Builder struct {
ctx context.Context ctx context.Context
cnt *content.Content cnt *content.Content
SpaceID string SpaceID string
EnvID string EnvID string
CollID string CollID string
data map[string]interface{} data map[string]any
// Для кеширования запросов // Для кеширования запросов
space *spaces.Space space *spaces.Space
environment *environments.Environment environment *environments.Environment
collection *collections.Collection 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 { func NewHTMLBuilder(cnt *content.Content, space, env, coll string) *Builder {
return &Builder{ b := &Builder{
ctx: context.Background(), ctx: context.Background(),
cnt: cnt, cnt: cnt,
SpaceID: space, SpaceID: space,
EnvID: env, 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 { func (b *Builder) getFuncs() map[string]any {
return template.FuncMap{ return map[string]any{
"lookup": getLookup(b), "lookup": getLookup(b),
"system": getSystem(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 := *b
bld.data = data bld.data = data
return &bld return &bld
...@@ -51,7 +90,8 @@ func (b *Builder) WithData(data map[string]interface{}) *Builder { ...@@ -51,7 +90,8 @@ func (b *Builder) WithData(data map[string]interface{}) *Builder {
func (b *Builder) WithKV(kv ...any) *Builder { func (b *Builder) WithKV(kv ...any) *Builder {
bld := *b bld := *b
if bld.data == nil { 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 { for i := 0; i < len(kv)-1; i += 2 {
k, _ := kv[i].(string) k, _ := kv[i].(string)
...@@ -63,7 +103,7 @@ func (b *Builder) WithKV(kv ...any) *Builder { ...@@ -63,7 +103,7 @@ func (b *Builder) WithKV(kv ...any) *Builder {
return &bld return &bld
} }
func (b *Builder) GetData() map[string]interface{} { func (b *Builder) GetData() map[string]any {
return b.data return b.data
} }
...@@ -84,69 +124,68 @@ func (b *Builder) Context() context.Context { ...@@ -84,69 +124,68 @@ func (b *Builder) Context() context.Context {
return b.ctx return b.ctx
} }
func (b *Builder) Template() *template.Template { func (b *Builder) Template(text string) (Template, error) {
return template.New("main").Funcs(b.getFuncs()) return b.template(text)
} }
func (b *Builder) Execute(str string, data ...any) (string, error) { func (b *Builder) Execute(templ string, data ...any) (string, error) {
t := b.Template() t, err := b.Template(templ)
buf := new(bytes.Buffer)
t, err := t.Parse(str)
if err != nil { if err != nil {
return "", err return "", err
} }
buf := new(bytes.Buffer)
if err = t.Execute(buf, b.getData(data...)); err != nil { if err = t.Execute(buf, b.getData(data...)); err != nil {
return "", err return "", err
} }
return buf.String(), nil return buf.String(), nil
} }
func (b *Builder) ExecuteList(str []string, data ...any) ([]string, error) { func (b *Builder) ExecuteList(templs []string, data ...any) ([]string, error) {
t := b.Template() result := make([]string, len(templs))
result := make([]string, len(str)) buf := new(bytes.Buffer)
buffer := new(bytes.Buffer) d := b.getData(data...)
for i, tmpl := range str {
if tmpl == "" { for i, templ := range templs {
continue t, err := b.Template(templ)
}
t, err := t.Parse(tmpl)
if err != nil { 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 return result, nil
} }
func (b *Builder) ExecuteMap(str map[string]interface{}, data ...any) (map[string]interface{}, error) { func (b *Builder) ExecuteMap(templMap map[string]any, data ...any) (map[string]any, error) {
result := make(map[string]interface{}, len(str)) result := make(map[string]any, len(templMap))
for k, v := range str { d := b.getData(data...)
for k, v := range templMap {
switch t := v.(type) { switch t := v.(type) {
case string: case string:
value, err := b.Execute(t, data...) value, err := b.Execute(t, d)
if err != nil { if err != nil {
return nil, err return nil, err
} }
v = value result[k] = value
case []string: case []string:
values, err := b.ExecuteList(append([]string{k}, t...), data...) var err error
result[k], err = b.ExecuteList(t, d)
if err != nil { if err != nil {
return nil, err return nil, err
} }
k = values[0] default:
vv := make([]interface{}, 0, len(t)) result[k] = v
for _, val := range values[1:] {
vv = append(vv, val)
} }
v = vv
} }
result[k] = v
}
return result, nil return result, nil
} }
...@@ -155,25 +194,19 @@ func (b *Builder) getData(data ...any) any { ...@@ -155,25 +194,19 @@ func (b *Builder) getData(data ...any) any {
return b.data 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 { for _, v := range data {
if m, ok := v.(map[string]interface{}); ok && b.data != nil { if m, ok := v.(map[string]any); ok {
res = mergeMaps(b.data, m) maps.Copy(res, m)
} }
} }
if res != nil {
return res
}
if len(res) == 0 {
return data[0] return data[0]
} }
func mergeMaps(in ...map[string]interface{}) map[string]interface{} { return res
out := make(map[string]interface{})
for _, i := range in {
for k, v := range i {
out[k] = v
}
}
return out
} }
This diff is collapsed.
...@@ -9,11 +9,11 @@ import ( ...@@ -9,11 +9,11 @@ import (
// getLookup возвращает функцию для шаблонизатора для получения значений из записи коллекции // getLookup возвращает функцию для шаблонизатора для получения значений из записи коллекции
// name указывается в виде "<collection id>.<item id>.<field>" // name указывается в виде "<collection id>.<item id>.<field>"
// Использование в шаблонах: {{ lookup "secrets.key.value" }} // Использование в шаблонах: {{ lookup "secrets.key.value" }}
func getLookup(b *Builder) any { func getLookup(b *Builder) func(string) (any, error) {
return func(name string) (any, error) { return func(name string) (any, error) {
parsedName := strings.Split(name, ".") parsedName := strings.Split(name, ".")
if len(parsedName) < 3 { if len(parsedName) < 3 {
return "", errors.Errorf("incorrect parameter \"%s\"", name) return nil, errors.Errorf("incorrect parameter \"%s\"", name)
} }
collectionID := parsedName[0] collectionID := parsedName[0]
...@@ -21,7 +21,7 @@ func getLookup(b *Builder) any { ...@@ -21,7 +21,7 @@ func getLookup(b *Builder) any {
field := parsedName[2] field := parsedName[2]
item, err := b.cnt.Items.Get(b.Context(), b.SpaceID, b.EnvID, collectionID, itemID) item, err := b.cnt.Items.Get(b.Context(), b.SpaceID, b.EnvID, collectionID, itemID)
if err != nil { 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 { if len(item.Data) > 0 {
...@@ -36,7 +36,7 @@ func getLookup(b *Builder) any { ...@@ -36,7 +36,7 @@ func getLookup(b *Builder) any {
// getSys возвращает функцию получения System // getSys возвращает функцию получения System
// Использование в шаблонах: {{ system.SpaceID }} // Использование в шаблонах: {{ system.SpaceID }}
func getSystem(b *Builder) any { func getSystem(b *Builder) func() *System {
return func() *System { return func() *System {
return &System{builder: b} return &System{builder: b}
} }
......
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)
})
})
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment