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

chore(core): Рефакторинг `template.Builder`

Issue: #3360
parent 4613dc12
No related branches found
No related tags found
No related merge requests found
package template
import (
"bytes"
"context"
html "html/template"
"io"
"maps"
text "text/template"
"git.perx.ru/perxis/perxis-go/pkg/collections"
"git.perx.ru/perxis/perxis-go/pkg/content"
......@@ -15,198 +10,138 @@ import (
)
type Template interface {
Execute(w io.Writer, data any) error
// Execute выполняет заданный шаблон pattern с предоставленными данными data и возвращает результат в виде строки.
Execute(pattern string, data ...any) (string, error)
// ExecuteList выполняет каждый шаблон из списка patterns с предоставленными данными data и возвращает результаты в виде среза строк.
// Каждый шаблон обрабатывается индивидуально.
ExecuteList(patterns []string, data ...any) ([]string, error)
// ExecuteMap выполняет шаблоны из мапы patternMap с предоставленными данными data и возвращает результаты в виде мапы.
// Шаблоны применяются к значениям мапы, если они являются строками или срезами строк; в противном случае, значения остаются без изменений.
ExecuteMap(patternMap map[string]any, data ...any) (map[string]any, error)
}
type Builder struct {
conf *BuilderConfig
ctx context.Context
cnt *content.Content
SpaceID string
EnvID string
CollID string
data map[string]any
funcMap 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 NewHTMLBuilder(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 := html.New("main").
Funcs(b.getFuncs()).
Parse(templ)
if err != nil {
return nil, err
}
return t, nil
}
return b
}
type BuilderConfig struct {
// DefaultHTML если true, по умолчанию используется шаблонизатор для HTML данных,
// иначе для текстовых.
DefaultHTML bool
func (b *Builder) getFuncs() map[string]any {
return map[string]any{
"lookup": getLookup(b),
"system": getSystem(b),
}
Content *content.Content
SpaceID string
EnvironmentID string
CollectionID string
}
func (b *Builder) WithData(data map[string]any) *Builder {
bld := *b
bld.data = data
return &bld
func NewBuilder(conf *BuilderConfig) *Builder {
if conf == nil {
conf = &BuilderConfig{}
}
func (b *Builder) WithKV(kv ...any) *Builder {
bld := *b
if bld.data == nil {
//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)
v := kv[i+1]
if k != "" && v != nil {
bld.data[k] = v
}
builder := &Builder{
conf: conf,
ctx: context.Background(),
data: make(map[string]any),
}
return &bld
builder.funcMap = map[string]any{
"lookup": getLookup(builder),
"system": getSystem(builder),
}
func (b *Builder) GetData() map[string]any {
return b.data
return builder
}
func (b *Builder) WithSpace(space, env string) *Builder {
bld := *b
bld.SpaceID = space
bld.EnvID = env
return &bld
// TextTemplate возвращает новый шаблонизатор для работы с текстовыми данными.
// При выполнении шаблонов доступны данные и функции из Builder.
func (b *Builder) TextTemplate() Template {
return NewCommonTemplate(false, b.data, b.funcMap)
}
func (b *Builder) WithContext(ctx context.Context) *Builder {
bld := *b
bld.ctx = ctx
return &bld
// HTMLTemplate возвращает новый шаблонизатор для работы с HTML данными.
// При выполнении шаблонов доступны данные и функции из Builder.
func (b *Builder) HTMLTemplate() Template {
return NewCommonTemplate(true, b.data, b.funcMap)
}
func (b *Builder) Context() context.Context {
return b.ctx
}
func (b *Builder) Template(text string) (Template, error) {
return b.template(text)
func (b *Builder) WithContext(ctx context.Context) *Builder {
clone := *b
clone.ctx = ctx
return &clone
}
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
func (b *Builder) Content() *content.Content {
return b.conf.Content
}
return buf.String(), nil
}
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 nil, err
func (b *Builder) SpaceID() string {
return b.conf.SpaceID
}
buf.Reset()
err = t.Execute(buf, d)
if err != nil {
return nil, err
func (b *Builder) EnvironmentID() string {
return b.conf.EnvironmentID
}
result[i] = buf.String()
func (b *Builder) CollectionID() string {
return b.conf.CollectionID
}
return result, nil
// WithData возвращает копию Builder с новыми переданными данными.
func (b *Builder) WithData(data map[string]any) *Builder {
clone := *b
clone.data = data
return &clone
}
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, d)
if err != nil {
return nil, err
func (b *Builder) WithKV(kv ...any) *Builder {
clone := *b
if clone.data == nil {
//nolint:mnd // Количество аргументов делится на 2, так как они представлены в виде пар ключ-значение.
clone.data = make(map[string]any, len(kv)/2)
}
result[k] = value
case []string:
var err error
result[k], err = b.ExecuteList(t, d)
if err != nil {
return nil, err
for i := 0; i < len(kv)-1; i += 2 {
k, _ := kv[i].(string)
v := kv[i+1]
if k != "" && v != nil {
clone.data[k] = v
}
default:
result[k] = v
}
return &clone
}
return result, nil
func (b *Builder) Execute(pattern string, data ...any) (string, error) {
if b.conf.DefaultHTML {
return b.HTMLTemplate().Execute(pattern, data...)
}
func (b *Builder) getData(data ...any) any {
if len(data) == 0 {
return b.data
return b.TextTemplate().Execute(pattern, data...)
}
res := maps.Clone(b.data)
if res == nil {
res = make(map[string]any)
}
for _, v := range data {
if m, ok := v.(map[string]any); ok {
maps.Copy(res, m)
func (b *Builder) ExecuteList(patterns []string, data ...any) ([]string, error) {
if b.conf.DefaultHTML {
return b.HTMLTemplate().ExecuteList(patterns, data...)
}
return b.TextTemplate().ExecuteList(patterns, data...)
}
if len(res) == 0 {
return data[0]
func (b *Builder) ExecuteMap(patternMap map[string]any, data ...any) (map[string]any, error) {
if b.conf.DefaultHTML {
return b.HTMLTemplate().ExecuteMap(patternMap, data...)
}
return res
return b.TextTemplate().ExecuteMap(patternMap, data...)
}
package template
import (
"context"
"testing"
"git.perx.ru/perxis/perxis-go/pkg/collections"
collectionsmocks "git.perx.ru/perxis/perxis-go/pkg/collections/mocks"
"git.perx.ru/perxis/perxis-go/pkg/content"
"git.perx.ru/perxis/perxis-go/pkg/errors"
"git.perx.ru/perxis/perxis-go/pkg/environments"
environmentsmocks "git.perx.ru/perxis/perxis-go/pkg/environments/mocks"
"git.perx.ru/perxis/perxis-go/pkg/items"
mocksitems "git.perx.ru/perxis/perxis-go/pkg/items/mocks"
itemsmocks "git.perx.ru/perxis/perxis-go/pkg/items/mocks"
"git.perx.ru/perxis/perxis-go/pkg/spaces"
spacesmocks "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 TestBuilder_Execute(t *testing.T) {
tests := []struct {
name string
SpaceID string
EnvID string
CollID string
template string
data any
want any
htmlBuilder bool
wantErr bool
func TestBuilder(t *testing.T) {
t.Run("using default HTML template", func(t *testing.T) {
builder := NewBuilder(&BuilderConfig{
DefaultHTML: true,
}).WithData(map[string]any{
"foo": "<b>bar</b>",
})
got, err := builder.Execute("{{ .foo }}")
require.NoError(t, err)
assert.Equal(t, "&lt;b&gt;bar&lt;/b&gt;", got)
})
}
getCnt func(t *testing.T) *content.Content
func TestBuilder_Funcs(t *testing.T) {
for _, tc := range []struct {
name string
pattern string
setupContent func(t *testing.T) *content.Content
spaceID string
environmentID string
collectionID string
want string
assertError assert.ErrorAssertionFunc
}{
{
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\">" +
"&lt;script&gt;alert(localStorage.getItem(&#39;secret&#39;))&lt;/script&gt;" +
"</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").
name: "basic lookup",
pattern: "{{ lookup \"coll_id.item_id.field\" }}",
setupContent: func(t *testing.T) *content.Content {
itemsService := itemsmocks.NewItems(t)
itemsService.On("Get", mock.Anything, "space_id", "env_id", "coll_id", "item_id").
Return(&items.Item{
ID: "dev",
SpaceID: "space",
EnvID: "env",
CollectionID: "secrets",
Data: map[string]any{
"id": "dev",
"key": "Luk",
"field": "value",
},
}, nil).Once()
return &content.Content{Items: itemsSvc}
}, nil).
Once()
return &content.Content{
Items: itemsService,
}
},
spaceID: "space_id",
environmentID: "env_id",
want: "value",
assertError: assert.NoError,
},
{
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").
name: "lookup with unknown field",
pattern: "{{ lookup \"coll_id.item_id.unknown\" }}",
setupContent: func(t *testing.T) *content.Content {
itemsService := itemsmocks.NewItems(t)
itemsService.On("Get", mock.Anything, "space_id", "env_id", "coll_id", "item_id").
Return(&items.Item{
ID: "dev",
SpaceID: "space",
EnvID: "env",
CollectionID: "secrets",
Data: map[string]any{
"id": "dev",
"slice": []int{1, 2, 3},
},
}, nil).Once()
return &content.Content{Items: itemsSvc}
},
"field": "value",
},
{
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").
Return(&items.Item{
ID: "dev",
SpaceID: "space",
EnvID: "env",
CollectionID: "secrets",
Data: map[string]any{},
}, nil).Once()
return &content.Content{Items: itemsSvc}
},
},
{
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").
Return(nil, errors.New("not found")).Once()
return &content.Content{Items: itemsSvc}
}, nil).
Once()
return &content.Content{
Items: itemsService,
}
},
spaceID: "space_id",
environmentID: "env_id",
want: "<no value>",
assertError: assert.NoError,
},
{
name: "system function",
SpaceID: "space",
template: "hello {{ system.SpaceID }}",
data: "",
want: "hello space",
wantErr: false,
},
name: "invalid lookup",
pattern: "{{ lookup \"coll_id.item_id\" }}",
setupContent: func(t *testing.T) *content.Content {
itemsService := itemsmocks.NewItems(t)
return &content.Content{
Items: itemsService,
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cnt *content.Content
if tt.getCnt != nil {
cnt = tt.getCnt(t)
}
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.template, tt.data)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.want, got)
})
}
}
func TestBuilder_ExecuteList(t *testing.T) {
tests := []struct {
name string
templs []string
data any
want []string
htmlBuilder bool
assertError assert.ErrorAssertionFunc
getCnt func(t *testing.T) *content.Content
}{
{
name: "invalid data",
templs: []string{
"hello {{ .a }}",
},
data: "world",
want: nil,
spaceID: "space_id",
environmentID: "env_id",
want: "",
assertError: assert.Error,
},
{
name: "invalid template",
templs: []string{
"hello {{ . ",
},
data: "world",
want: nil,
assertError: assert.Error,
name: "system space id",
pattern: "{{ system.SpaceID }}",
spaceID: "space_id",
want: "space_id",
assertError: assert.NoError,
},
{
name: "empty template",
templs: []string{
"",
},
data: "world",
want: []string{
"",
name: "field from system space",
pattern: "{{ system.Space.Name }}",
setupContent: func(t *testing.T) *content.Content {
spacesService := spacesmocks.NewSpaces(t)
spacesService.On("Get", mock.Anything, "space_id").
Return(&spaces.Space{
Name: "name",
}, nil).
Once()
return &content.Content{
Spaces: spacesService,
}
},
spaceID: "space_id",
environmentID: "env_id",
want: "name",
assertError: assert.NoError,
},
{
name: "empty data",
templs: []string{
"{{ . }}",
},
data: "",
want: []string{
"",
},
name: "system environment id",
pattern: "{{ system.EnvID }}",
environmentID: "env_id",
want: "env_id",
assertError: assert.NoError,
},
{
name: "success",
templs: []string{
"hello {{ . }}",
"world {{ . }}",
},
data: "world",
want: []string{
"hello world",
"world world",
name: "field from system environment",
pattern: "{{ system.Environment.ID }}",
setupContent: func(t *testing.T) *content.Content {
environmentsService := environmentsmocks.NewEnvironments(t)
environmentsService.On("Get", mock.Anything, "space_id", "env_id").
Return(&environments.Environment{
ID: "env_id",
}, nil).
Once()
return &content.Content{
Environments: environmentsService,
}
},
spaceID: "space_id",
environmentID: "env_id",
want: "env_id",
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\">" +
"&lt;script&gt;alert(localStorage.getItem(&#39;secret&#39;))&lt;/script&gt;" +
"</span>",
},
htmlBuilder: true,
name: "system collection id",
pattern: "{{ system.CollectionID }}",
collectionID: "coll_id",
want: "coll_id",
assertError: assert.NoError,
},
{
name: "template as plain text",
templs: []string{
"hello",
},
data: "world",
want: []string{
"hello",
name: "field from system collection",
pattern: "{{ system.Collection.ID }}",
setupContent: func(t *testing.T) *content.Content {
collectionsService := collectionsmocks.NewCollections(t)
collectionsService.On("Get", mock.Anything, "space_id", "env_id", "coll_id").
Return(&collections.Collection{
ID: "coll_id",
}, nil).
Once()
return &content.Content{
Collections: collectionsService,
}
},
spaceID: "space_id",
environmentID: "env_id",
collectionID: "coll_id",
want: "coll_id",
assertError: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cnt *content.Content
if tt.getCnt != nil {
cnt = tt.getCnt(t)
}
var b *Builder
if tt.htmlBuilder {
b = NewHTMLBuilder(cnt, "space_id", "env_id", "coll_id")
} else {
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)
})
}
}
func TestBuilder_ExecuteMap(t *testing.T) {
tests := []struct {
name string
SpaceID string
EnvID string
templateMap map[string]any
data any
want map[string]any
wantErr bool
itemsCall func(itemsSvc *mocksitems.Items)
} {
{
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",
},
},
data: map[string]any{
"key": "foo",
},
want: map[string]any{
"{{ .key }}": []string{
"value",
},
},
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.Items{}
if tt.itemsCall != nil {
tt.itemsCall(itemsSvc)
}
b := NewBuilder(&content.Content{Items: itemsSvc}, tt.SpaceID, tt.EnvID, "")
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)
t.Run(tc.name, func(t *testing.T) {
conf := &BuilderConfig{
SpaceID: tc.spaceID,
EnvironmentID: tc.environmentID,
CollectionID: tc.collectionID,
}
if tc.setupContent != nil {
conf.Content = tc.setupContent(t)
}
builder := NewBuilder(conf)
got, err := builder.TextTemplate().Execute(tc.pattern)
tc.assertError(t, err)
assert.Equal(t, tc.want, got)
})
}
}
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},
),
)
})
}
......@@ -19,7 +19,7 @@ func getLookup(b *Builder) func(string) (any, error) {
collectionID := parsedName[0]
itemID := parsedName[1]
field := parsedName[2]
item, err := b.cnt.Items.Get(b.Context(), b.SpaceID, b.EnvID, collectionID, itemID)
item, err := b.Content().Items.Get(b.Context(), b.SpaceID(), b.EnvironmentID(), collectionID, itemID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get \"%s\"")
}
......
......@@ -85,7 +85,12 @@ func Test_getLookup(t *testing.T) {
if tc.setup != nil {
tc.setup(svc)
}
b := NewBuilder(&content.Content{Items: svc}, "space_id", "env_id", "coll_id")
b := NewBuilder(&BuilderConfig{
Content: &content.Content{Items: svc},
SpaceID: "space_id",
EnvironmentID: "env_id",
CollectionID: "coll_id",
})
lookup := getLookup(b)
got, err := lookup(tc.input)
......@@ -100,11 +105,16 @@ func Test_getSystem(t *testing.T) {
spacesSvc := mocksspaces.NewSpaces(t)
envsSvc := mocksenvs.NewEnvironments(t)
collsSvc := mockscolls.NewCollections(t)
b := NewBuilder(&content.Content{
b := NewBuilder(&BuilderConfig{
Content: &content.Content{
Spaces: spacesSvc,
Environments: envsSvc,
Collections: collsSvc,
}, "space_id", "env_id", "coll_id")
},
SpaceID: "space_id",
EnvironmentID: "env_id",
CollectionID: "coll_id",
})
system := getSystem(b)
assert.Equal(t, "space_id", system().SpaceID())
})
......@@ -112,11 +122,16 @@ func Test_getSystem(t *testing.T) {
spacesSvc := mocksspaces.NewSpaces(t)
envsSvc := mocksenvs.NewEnvironments(t)
collsSvc := mockscolls.NewCollections(t)
b := NewBuilder(&content.Content{
b := NewBuilder(&BuilderConfig{
Content: &content.Content{
Spaces: spacesSvc,
Environments: envsSvc,
Collections: collsSvc,
}, "space_id", "env_id", "coll_id")
},
SpaceID: "space_id",
EnvironmentID: "env_id",
CollectionID: "coll_id",
})
system := getSystem(b)
assert.Equal(t, "env_id", system().EnvID())
})
......@@ -124,11 +139,16 @@ func Test_getSystem(t *testing.T) {
spacesSvc := mocksspaces.NewSpaces(t)
envsSvc := mocksenvs.NewEnvironments(t)
collsSvc := mockscolls.NewCollections(t)
b := NewBuilder(&content.Content{
b := NewBuilder(&BuilderConfig{
Content: &content.Content{
Spaces: spacesSvc,
Environments: envsSvc,
Collections: collsSvc,
}, "space_id", "env_id", "coll_id")
},
SpaceID: "space_id",
EnvironmentID: "env_id",
CollectionID: "coll_id",
})
system := getSystem(b)
assert.Equal(t, "coll_id", system().CollectionID())
})
......@@ -144,11 +164,16 @@ func Test_getSystem(t *testing.T) {
Once()
envsSvc := mocksenvs.NewEnvironments(t)
collsSvc := mockscolls.NewCollections(t)
b := NewBuilder(&content.Content{
b := NewBuilder(&BuilderConfig{
Content: &content.Content{
Spaces: spacesSvc,
Environments: envsSvc,
Collections: collsSvc,
}, "space_id", "env_id", "coll_id")
},
SpaceID: "space_id",
EnvironmentID: "env_id",
CollectionID: "coll_id",
})
system := getSystem(b)
got, err := system().Space()
require.NoError(t, err)
......@@ -165,11 +190,16 @@ func Test_getSystem(t *testing.T) {
Once()
envsSvc := mocksenvs.NewEnvironments(t)
collsSvc := mockscolls.NewCollections(t)
b := NewBuilder(&content.Content{
b := NewBuilder(&BuilderConfig{
Content: &content.Content{
Spaces: spacesSvc,
Environments: envsSvc,
Collections: collsSvc,
}, "space_id", "env_id", "coll_id")
},
SpaceID: "space_id",
EnvironmentID: "env_id",
CollectionID: "coll_id",
})
system := getSystem(b)
got, err := system().Space()
require.Error(t, err)
......@@ -187,11 +217,16 @@ func Test_getSystem(t *testing.T) {
}, nil).
Once()
collsSvc := mockscolls.NewCollections(t)
b := NewBuilder(&content.Content{
b := NewBuilder(&BuilderConfig{
Content: &content.Content{
Spaces: spacesSvc,
Environments: envsSvc,
Collections: collsSvc,
}, "space_id", "env_id", "coll_id")
},
SpaceID: "space_id",
EnvironmentID: "env_id",
CollectionID: "coll_id",
})
system := getSystem(b)
got, err := system().Environment()
require.NoError(t, err)
......@@ -207,11 +242,16 @@ func Test_getSystem(t *testing.T) {
Return(nil, errors.New("some error")).
Once()
collsSvc := mockscolls.NewCollections(t)
b := NewBuilder(&content.Content{
b := NewBuilder(&BuilderConfig{
Content: &content.Content{
Spaces: spacesSvc,
Environments: envsSvc,
Collections: collsSvc,
}, "space_id", "env_id", "coll_id")
},
SpaceID: "space_id",
EnvironmentID: "env_id",
CollectionID: "coll_id",
})
system := getSystem(b)
got, err := system().Environment()
require.Error(t, err)
......@@ -231,11 +271,16 @@ func Test_getSystem(t *testing.T) {
Name: "Collection",
}, nil).
Once()
b := NewBuilder(&content.Content{
b := NewBuilder(&BuilderConfig{
Content: &content.Content{
Spaces: spacesSvc,
Environments: envsSvc,
Collections: collsSvc,
}, "space_id", "env_id", "coll_id")
},
SpaceID: "space_id",
EnvironmentID: "env_id",
CollectionID: "coll_id",
})
system := getSystem(b)
got, err := system().Collection()
require.NoError(t, err)
......@@ -253,11 +298,16 @@ func Test_getSystem(t *testing.T) {
collsSvc.On("Get", mock.Anything, "space_id", "env_id", "coll_id").
Return(nil, errors.New("some error")).
Once()
b := NewBuilder(&content.Content{
b := NewBuilder(&BuilderConfig{
Content: &content.Content{
Spaces: spacesSvc,
Environments: envsSvc,
Collections: collsSvc,
}, "space_id", "env_id", "coll_id")
},
SpaceID: "space_id",
EnvironmentID: "env_id",
CollectionID: "coll_id",
})
system := getSystem(b)
got, err := system().Collection()
require.Error(t, err)
......
......@@ -10,44 +10,45 @@ type System struct {
builder *Builder
}
func (s *System) SpaceID() string {
return s.builder.SpaceID
}
func (s *System) EnvID() string {
return s.builder.EnvID
}
func (s *System) CollectionID() string {
return s.builder.CollID
}
func (s *System) Space() (*spaces.Space, error) {
if s.builder.space != nil {
return s.builder.space, nil
}
space, err := s.builder.cnt.Spaces.Get(s.builder.ctx, s.builder.SpaceID)
space, err := s.builder.Content().Spaces.Get(s.builder.Context(), s.builder.SpaceID())
s.builder.space = space
return s.builder.space, err
}
func (s *System) SpaceID() string {
return s.builder.SpaceID()
}
func (s *System) Environment() (*environments.Environment, error) {
if s.builder.environment != nil {
return s.builder.environment, nil
}
env, err := s.builder.cnt.Environments.Get(s.builder.ctx, s.builder.SpaceID, s.builder.EnvID)
env, err := s.builder.Content().Environments.Get(s.builder.ctx, s.builder.SpaceID(), s.builder.EnvironmentID())
s.builder.environment = env
return s.builder.environment, err
}
func (s *System) EnvID() string {
return s.builder.EnvironmentID()
}
func (s *System) Collection() (*collections.Collection, error) {
if s.builder.collection != nil {
return s.builder.collection, nil
}
coll, err := s.builder.cnt.Collections.Get(s.builder.ctx, s.builder.SpaceID, s.builder.EnvID, s.builder.CollID)
coll, err := s.builder.Content().Collections.Get(s.builder.ctx, s.builder.SpaceID(),
s.builder.EnvironmentID(), s.builder.CollectionID())
s.builder.collection = coll
return s.builder.collection, err
}
func (s *System) CollectionID() string {
return s.builder.CollectionID()
}
package template
import (
"bytes"
html "html/template"
"io"
"maps"
text "text/template"
)
func NewCommonTemplate(html bool, data map[string]any, funcMap map[string]any) *CommonTemplate {
return &CommonTemplate{
html: html,
data: data,
funcMap: funcMap,
}
}
type CommonTemplate struct {
html bool
data map[string]any
funcMap map[string]any
}
func (t *CommonTemplate) Execute(pattern string, data ...any) (string, error) {
return t.execute(pattern, t.getData(data...))
}
func (t *CommonTemplate) ExecuteList(patterns []string, data ...any) ([]string, error) {
return t.executeList(patterns, t.getData(data...))
}
func (t *CommonTemplate) ExecuteMap(patternMap map[string]any, data ...any) (map[string]any, error) {
result := make(map[string]any, len(patternMap))
d := t.getData(data...)
for key, value := range patternMap {
var err error
switch v := value.(type) {
case string:
result[key], err = t.execute(v, d)
if err != nil {
return nil, err
}
case []string:
result[key], err = t.executeList(v, d)
if err != nil {
return nil, err
}
default:
result[key] = value
}
}
return result, nil
}
func (t *CommonTemplate) execute(pattern string, data any) (string, error) {
var (
exec interface {
Execute(w io.Writer, data any) error
}
err error
)
if t.html {
exec, err = html.New("main").Funcs(t.funcMap).Parse(pattern)
} else {
exec, err = text.New("main").Funcs(t.funcMap).Parse(pattern)
}
if err != nil {
return "", err
}
var buf bytes.Buffer
err = exec.Execute(&buf, data)
if err != nil {
return "", err
}
return buf.String(), nil
}
func (t *CommonTemplate) executeList(patterns []string, data any) ([]string, error) {
result := make([]string, len(patterns))
var err error
for i, pattern := range patterns {
result[i], err = t.execute(pattern, data)
if err != nil {
return nil, err
}
}
return result, nil
}
func (t *CommonTemplate) getData(data ...any) any {
if len(data) == 0 {
return t.data
}
m, ok := data[0].(map[string]any)
if !ok {
return data[0]
}
result := maps.Clone(t.data)
if result == nil {
result = make(map[string]any)
}
maps.Copy(result, m)
return result
}
package template
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCommonTemplate_Execute(t *testing.T) {
for _, tc := range []struct {
name string
pattern string
data map[string]any
additionalData []any
funcMap map[string]any
wantText string
wantHTML string
assertError assert.ErrorAssertionFunc
}{
{
name: "basic",
pattern: "{{ .foo }}",
data: map[string]any{
"foo": "bar",
},
wantText: "bar",
wantHTML: "bar",
assertError: assert.NoError,
},
{
name: "additional data",
pattern: "{{ . }}",
additionalData: []any{
"bar",
},
wantText: "bar",
wantHTML: "bar",
assertError: assert.NoError,
},
{
name: "map in additional data",
pattern: "{{ .foo }}",
additionalData: []any{
map[string]any{
"foo": "bar",
},
},
wantText: "bar",
wantHTML: "bar",
assertError: assert.NoError,
},
{
name: "html in additional data",
pattern: "{{ . }}",
additionalData: []any{
"<a href=\"https://example.com/\">Link</a>",
},
wantText: "<a href=\"https://example.com/\">Link</a>",
wantHTML: "&lt;a href=&#34;https://example.com/&#34;&gt;Link&lt;/a&gt;",
assertError: assert.NoError,
},
{
name: "empty pattern",
pattern: "",
wantText: "",
wantHTML: "",
assertError: assert.NoError,
},
{
name: "plain text",
pattern: "text",
wantText: "text",
wantHTML: "text",
assertError: assert.NoError,
},
{
name: "func map",
pattern: "{{ foo }}",
funcMap: map[string]any{
"foo": func() string {
return "bar"
},
},
wantText: "bar",
wantHTML: "bar",
assertError: assert.NoError,
},
{
name: "data and additional data",
pattern: "{{ .foo }},{{ .bar }}",
data: map[string]any{
"foo": "foo",
},
additionalData: []any{
map[string]any{
"bar": "bar",
},
},
wantText: "foo,bar",
wantHTML: "foo,bar",
assertError: assert.NoError,
},
{
name: "invalid pattern",
pattern: "{{ .",
wantText: "",
wantHTML: "",
assertError: assert.Error,
},
{
name: "no value",
pattern: "{{ .foo }}",
wantText: "<no value>",
wantHTML: "",
assertError: assert.NoError,
},
} {
t.Run(tc.name, func(t *testing.T) {
for _, enc := range []struct {
name string
template *CommonTemplate
want string
}{
{
name: "text",
template: NewCommonTemplate(false, tc.data, tc.funcMap),
want: tc.wantText,
},
{
name: "html",
template: NewCommonTemplate(true, tc.data, tc.funcMap),
want: tc.wantHTML,
},
} {
t.Run(enc.name, func(t *testing.T) {
got, err := enc.template.Execute(tc.pattern, tc.additionalData...)
tc.assertError(t, err)
assert.Equal(t, enc.want, got)
})
}
})
}
}
func TestCommonTemplate_ExecuteList(t *testing.T) {
for _, tc := range []struct {
name string
patterns []string
data map[string]any
additionalData []any
funcMap map[string]any
wantText []string
wantHTML []string
assertError assert.ErrorAssertionFunc
}{
{
name: "basic",
patterns: []string{"{{ .foo }}", "{{ .bar }}"},
data: map[string]any{
"foo": "hello",
"bar": "world",
},
wantText: []string{"hello", "world"},
wantHTML: []string{"hello", "world"},
assertError: assert.NoError,
},
{
name: "additional data",
patterns: []string{"{{ . }}"},
additionalData: []any{
"test",
},
wantText: []string{"test"},
wantHTML: []string{"test"},
assertError: assert.NoError,
},
{
name: "map in additional data",
patterns: []string{"{{ .foo }}", "{{ .bar }}"},
additionalData: []any{
map[string]any{
"foo": "value1",
"bar": "value2",
},
},
wantText: []string{"value1", "value2"},
wantHTML: []string{"value1", "value2"},
assertError: assert.NoError,
},
{
name: "html escaping",
patterns: []string{"{{ . }}"},
additionalData: []any{
"<script>alert('secret')</script>",
},
wantText: []string{"<script>alert('secret')</script>"},
wantHTML: []string{"&lt;script&gt;alert(&#39;secret&#39;)&lt;/script&gt;"},
assertError: assert.NoError,
},
{
name: "empty list",
patterns: []string{},
wantText: []string{},
wantHTML: []string{},
assertError: assert.NoError,
},
{
name: "single plain text",
patterns: []string{"just text"},
wantText: []string{"just text"},
wantHTML: []string{"just text"},
assertError: assert.NoError,
},
{
name: "func map",
patterns: []string{"{{ greet }}", "{{ 42 | add }}"},
funcMap: map[string]any{
"greet": func() string { return "hello" },
"add": func(i int) int { return i + 1 },
},
wantText: []string{"hello", "43"},
wantHTML: []string{"hello", "43"},
assertError: assert.NoError,
},
{
name: "data merging",
patterns: []string{"{{ .foo }}", "{{ .bar }}"},
data: map[string]any{
"foo": "base",
},
additionalData: []any{
map[string]any{
"bar": "extra",
},
},
wantText: []string{"base", "extra"},
wantHTML: []string{"base", "extra"},
assertError: assert.NoError,
},
{
name: "invalid pattern",
patterns: []string{"valid", "{{ .invalid "},
wantText: []string(nil),
wantHTML: []string(nil),
assertError: assert.Error,
},
{
name: "unknown value",
patterns: []string{"{{ .unknown }}"},
wantText: []string{"<no value>"},
wantHTML: []string{""},
assertError: assert.NoError,
},
} {
t.Run(tc.name, func(t *testing.T) {
for _, enc := range []struct {
name string
template *CommonTemplate
want []string
}{
{
name: "text",
template: NewCommonTemplate(false, tc.data, tc.funcMap),
want: tc.wantText,
},
{
name: "html",
template: NewCommonTemplate(true, tc.data, tc.funcMap),
want: tc.wantHTML,
},
} {
t.Run(enc.name, func(t *testing.T) {
got, err := enc.template.ExecuteList(tc.patterns, tc.additionalData...)
tc.assertError(t, err)
assert.Equal(t, enc.want, got)
})
}
})
}
}
func TestCommonTemplate_ExecuteMap(t *testing.T) {
for _, tc := range []struct {
name string
patternMap map[string]any
data map[string]any
additionalData []any
funcMap map[string]any
wantText map[string]any
wantHTML map[string]any
assertError assert.ErrorAssertionFunc
}{
{
name: "mixed types",
patternMap: map[string]any{
"key1": "{{ .foo }}",
"key2": []string{"{{ .bar }}", "static"},
"key3": 42,
},
data: map[string]any{
"foo": "value1",
"bar": "value2",
},
wantText: map[string]any{
"key1": "value1",
"key2": []string{"value2", "static"},
"key3": 42,
},
wantHTML: map[string]any{
"key1": "value1",
"key2": []string{"value2", "static"},
"key3": 42,
},
assertError: assert.NoError,
},
{
name: "html escaping",
patternMap: map[string]any{
"html": "{{ .text }}",
},
data: map[string]any{
"text": "<b>content</b>",
},
wantText: map[string]any{
"html": "<b>content</b>",
},
wantHTML: map[string]any{
"html": "&lt;b&gt;content&lt;/b&gt;",
},
assertError: assert.NoError,
},
{
name: "func map",
patternMap: map[string]any{
"func": "{{ add 1 2 }}",
"slice": []string{"{{ greet }}"},
},
funcMap: map[string]any{
"add": func(a, b int) int { return a + b },
"greet": func() string { return "hello" },
},
wantText: map[string]any{
"func": "3",
"slice": []string{"hello"},
},
wantHTML: map[string]any{
"func": "3",
"slice": []string{"hello"},
},
assertError: assert.NoError,
},
{
name: "data merging",
patternMap: map[string]any{
"merged": "{{ .base }}, {{ .extra }}",
},
data: map[string]any{
"base": "foo",
},
additionalData: []any{
map[string]any{
"extra": "bar",
},
},
wantText: map[string]any{
"merged": "foo, bar",
},
wantHTML: map[string]any{
"merged": "foo, bar",
},
assertError: assert.NoError,
},
{
name: "invalid pattern",
patternMap: map[string]any{
"ok": "valid",
"error": "{{ .invalid",
},
wantText: map[string]any(nil),
wantHTML: map[string]any(nil),
assertError: assert.Error,
},
{
name: "missing value",
patternMap: map[string]any{
"missing": "{{ .unknown }}",
},
wantText: map[string]any{
"missing": "<no value>",
},
wantHTML: map[string]any{
"missing": "",
},
assertError: assert.NoError,
},
{
name: "nested slices",
patternMap: map[string]any{
"slice": []string{
"{{ .item1 }}",
"{{ .item2 }}",
"plain text",
},
},
additionalData: []any{
map[string]any{
"item1": "value1",
"item2": "value2",
},
},
wantText: map[string]any{
"slice": []string{"value1", "value2", "plain text"},
},
wantHTML: map[string]any{
"slice": []string{"value1", "value2", "plain text"},
},
assertError: assert.NoError,
},
{
name: "empty map",
patternMap: map[string]any{},
wantText: map[string]any{},
wantHTML: map[string]any{},
assertError: assert.NoError,
},
} {
t.Run(tc.name, func(t *testing.T) {
for _, enc := range []struct {
name string
template *CommonTemplate
want map[string]any
}{
{
name: "text",
template: NewCommonTemplate(false, tc.data, tc.funcMap),
want: tc.wantText,
},
{
name: "html",
template: NewCommonTemplate(true, tc.data, tc.funcMap),
want: tc.wantHTML,
},
} {
t.Run(enc.name, func(t *testing.T) {
got, err := enc.template.ExecuteMap(tc.patternMap, tc.additionalData...)
tc.assertError(t, err)
assert.Equal(t, enc.want, got)
})
}
})
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment