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
Branches
Tags
No related merge requests found
......@@ -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)
default:
result[k] = v
}
v = vv
}
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
}
if len(res) == 0 {
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
}
}
return out
return res
}
This diff is collapsed.
......@@ -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}
}
......
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