Skip to content
Snippets Groups Projects
Commit e6f46eb4 authored by Semyon Krestyaninov's avatar Semyon Krestyaninov :dog2:
Browse files

wip

parent f15a1d52
No related branches found
No related tags found
No related merge requests found
Subproject commit 95aca241a0cb17f5e1e9f584b1993bf7b933588e
Subproject commit 0627c9f829178bc6de2623a0b6d42964c44de496
package executor
import (
"context"
"strings"
"git.perx.ru/perxis/perxis-go/pkg/schema/field"
)
type Executor interface {
// Execute выполняет подстановку значений.
Execute(ctx context.Context, input string) (string, error)
// IsCanceled проверяет, было ли выполнение отменено.
IsCanceled() bool
}
// Executable определяет опции полей, использующие Executor для подстановки значений.
type Executable interface {
Execute(ctx context.Context, exec Executor, field *field.Field) error
}
// ExecuteAll последовательно выполняет Execute для каждого элемента inputs.
// Возвращает собранные непустые результаты (игнорируя пустые строки) или первую возникшую ошибку.
func ExecuteAll(ctx context.Context, exec Executor, inputs []string) ([]string, error) {
var result []string
for _, input := range inputs {
output, err := exec.Execute(ctx, input)
if err != nil {
return nil, err
}
if strings.TrimSpace(output) == "" {
continue
}
result = append(result, output)
}
return result, nil
}
func Execute(ctx context.Context, w field.Walker, exec Executor) error {
_, _, err := w.Walk(ctx,
nil,
func(ctx context.Context, fld *field.Field, _ any) (field.WalkFuncResult, error) {
var (
result field.WalkFuncResult
err error
)
enabled, _ := fld.IsEnabled(ctx)
if !enabled {
result.Stop = true
return result, err
}
for _, op := range fld.Options {
executable, ok := op.(Executable)
if !ok {
continue
}
err = executable.Execute(ctx, exec, fld)
if err != nil {
return result, err
}
}
return result, err
},
field.WalkSchema(),
)
return err
}
//nolint:gochecknoinits // init нужен для регистрации опций
func init() {
field.RegisterOption(CollectionFilter{})
field.RegisterOption(ItemFilter{})
}
package executor
import (
"bytes"
"context"
"sync"
"text/template"
"git.perx.ru/perxis/perxis-go/pkg/errors"
)
type TemplateExecutor struct {
data map[string]any
funcMap template.FuncMap
}
func NewTemplateExecutor(data map[string]any) *TemplateExecutor {
te := &TemplateExecutor{
data: data,
}
te.init()
return te
}
func (exec *TemplateExecutor) init() {
if exec.data == nil {
exec.data = make(map[string]any)
}
delete(exec.data, "Error")
exec.funcMap = template.FuncMap{
"error": exec.errorFunc,
}
}
func (exec *TemplateExecutor) Execute(_ context.Context, input string) (string, error) {
templ, err := template.New("template_executor").Funcs(exec.funcMap).Parse(input)
if err != nil {
return "", err
}
buf := getBuffer()
defer releaseBuffer(buf)
err = templ.Execute(buf, exec.data)
if err != nil {
return "", err
}
output := buf.String()
if output == "<no value>" {
return "", nil
}
return output, nil
}
// IsCanceled возвращает true, если в процессе выполнения шаблона была вызвана
// шаблонная функция error.
func (exec *TemplateExecutor) IsCanceled() bool {
_, ok := exec.data["Error"]
return ok
}
func (exec *TemplateExecutor) errorFunc(text string) (string, error) {
exec.data["Error"] = text
return "", errors.New(text)
}
//nolint:gochecknoglobals // Доступ к пулу необходим на глобальном уровне.
var bufferPool = &sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func getBuffer() *bytes.Buffer {
buf, _ := bufferPool.Get().(*bytes.Buffer)
return buf
}
func releaseBuffer(buf *bytes.Buffer) {
const maxBufCap = 1024
if buf.Cap() > maxBufCap {
return
}
buf.Reset()
bufferPool.Put(buf)
}
......@@ -6,9 +6,9 @@ import (
"git.perx.ru/perxis/perxis-go/pkg/errors"
"git.perx.ru/perxis/perxis-go/pkg/expr"
"git.perx.ru/perxis/perxis-go/pkg/schema/executor"
"git.perx.ru/perxis/perxis-go/pkg/schema/field"
"git.perx.ru/perxis/perxis-go/pkg/schema/modify"
"git.perx.ru/perxis/perxis-go/pkg/schema/template"
"git.perx.ru/perxis/perxis-go/pkg/schema/validate"
)
......@@ -196,6 +196,42 @@ func (s *Schema) ToValue(ctx context.Context, data map[string]interface{}) (res
return data, err
}
func (s *Schema) ResolveFieldsTemplates(ctx context.Context, exec template.Executor) (*Schema, error) {
resolved := s.Clone(false)
_, _, err := resolved.Walk(ctx,
nil,
func(ctx context.Context, fld *field.Field, _ any) (field.WalkFuncResult, error) {
var result field.WalkFuncResult
enabled, _ := fld.IsEnabled(ctx)
if !enabled {
result.Stop = true
return result, nil
}
for _, op := range fld.Options {
executable, ok := op.(template.Executable)
if !ok {
continue
}
err := executable.Execute(exec, fld)
if err != nil {
return result, err
}
}
return result, nil
},
field.WalkSchema(),
)
if err != nil {
return nil, err
}
return resolved, nil
}
type parentFieldCtxKey struct{}
func (s *Schema) Introspect(ctx context.Context, data map[string]interface{}) (map[string]interface{}, *Schema, error) {
......@@ -267,20 +303,6 @@ func (s *Schema) Introspect(ctx context.Context, data map[string]interface{}) (m
return val, mutatedSchema, nil
}
func (s *Schema) Execute(ctx context.Context, exec executor.Executor) (*Schema, error) {
if err := s.Load(ctx); err != nil {
return nil, err
}
clone := s.Clone(false)
err := executor.Execute(ctx, clone, exec)
if err != nil {
return nil, err
}
return clone, nil
}
// GetEnum возвращает список опций перечисления для поля
func (s *Schema) GetEnum(fieldPath string) []validate.EnumOpt {
f := s.Field.GetField(fieldPath)
......
package schema
import (
"context"
"testing"
"git.perx.ru/perxis/perxis-go/pkg/schema/executor"
"git.perx.ru/perxis/perxis-go/pkg/schema/field"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSchema_Clone(t *testing.T) {
......@@ -19,47 +16,3 @@ func TestSchema_Clone(t *testing.T) {
fld = f.Clone(false)
assert.NotNil(t, fld.State)
}
func TestSchema_Execute(t *testing.T) {
sch := New("layout", field.String().AddOptions(
executor.ItemFilter{
Query: []string{
`icontains(group, "{{ .Collection.ID }}")`,
`{{ if .Item.Data.name -}} name == "{{ .Item.Data.name }}" {{- else }} {{ error "Пожалуйста, укажите значение для поля \"name\"" }} {{ end }}`,
`age >= {{ .Item.Data.age }}`,
`{{ if ge .Item.Data.age 18 -}} adult == true {{- end }}`,
},
QueryError: "{{ .Error }}",
},
))
executed, err := sch.Execute(context.Background(), executor.NewTemplateExecutor(map[string]any{
"Item": map[string]any{
"Data": map[string]any{
"age": 18,
"name": "John",
},
},
"Collection": map[string]any{
"ID": "coll_id",
},
}))
require.NoError(t, err)
executed = executed.ClearState()
want := New("layout", field.String().AddOptions(
executor.ItemFilter{
Query: []string{
`icontains(group, "coll_id")`,
`name == "John"`,
`age >= 18`,
`adult == true`,
},
},
))
err = want.Load(context.Background())
want = want.ClearState()
require.NoError(t, err)
assert.Equal(t, want, executed)
}
package executor
package template
import (
"context"
"git.perx.ru/perxis/perxis-go/pkg/schema/field"
)
......@@ -28,20 +26,19 @@ func (opt CollectionFilter) GetName() string {
return "collection_filter"
}
func (opt CollectionFilter) Execute(ctx context.Context, exec Executor, fld *field.Field) error {
func (opt CollectionFilter) Execute(exec Executor, fld *field.Field) error {
var err error
opt.ID, err = ExecuteAll(ctx, exec, opt.ID)
opt.ID, err = exec.ExecuteList(opt.ID)
if err != nil && !exec.IsCanceled() {
return err
}
opt.Name, err = ExecuteAll(ctx, exec, opt.Name)
opt.Name, err = exec.ExecuteList(opt.Name)
if err != nil && !exec.IsCanceled() {
return err
}
opt.Tag, err = ExecuteAll(ctx, exec, opt.Tag)
opt.Tag, err = exec.ExecuteList(opt.Tag)
if err != nil && !exec.IsCanceled() {
return err
}
......
package executor
package template
import (
"context"
"testing"
"git.perx.ru/perxis/perxis-go/pkg/schema/field"
......@@ -12,13 +11,17 @@ func TestCollectionFilterExecute(t *testing.T) {
tests := []struct {
name string
input CollectionFilter
want CollectionFilter
want *CollectionFilter
assertErr assert.ErrorAssertionFunc
}{
{
name: "empty",
input: CollectionFilter{},
want: CollectionFilter{},
want: &CollectionFilter{
ID: []string{},
Name: []string{},
Tag: []string{},
},
assertErr: assert.NoError,
},
{
......@@ -28,10 +31,12 @@ func TestCollectionFilterExecute(t *testing.T) {
"{{ .data.id }}",
},
},
want: CollectionFilter{
want: &CollectionFilter{
ID: []string{
"adebcfg",
},
Name: []string{},
Tag: []string{},
},
assertErr: assert.NoError,
},
......@@ -42,10 +47,12 @@ func TestCollectionFilterExecute(t *testing.T) {
"{{ .data.name }}",
},
},
want: CollectionFilter{
want: &CollectionFilter{
Name: []string{
"John",
},
ID: []string{},
Tag: []string{},
},
assertErr: assert.NoError,
},
......@@ -56,10 +63,12 @@ func TestCollectionFilterExecute(t *testing.T) {
"{{ .data.layout }}",
},
},
want: CollectionFilter{
want: &CollectionFilter{
Tag: []string{
"post",
},
ID: []string{},
Name: []string{},
},
assertErr: assert.NoError,
},
......@@ -70,10 +79,12 @@ func TestCollectionFilterExecute(t *testing.T) {
"tag",
},
},
want: CollectionFilter{
want: &CollectionFilter{
Tag: []string{
"tag",
},
ID: []string{},
Name: []string{},
},
assertErr: assert.NoError,
},
......@@ -84,14 +95,17 @@ func TestCollectionFilterExecute(t *testing.T) {
`{{ error "some error" }}`,
},
},
want: CollectionFilter{},
want: &CollectionFilter{
ID: []string{},
Name: []string{},
},
assertErr: assert.NoError,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
exec := NewTemplateExecutor(map[string]any{
exec := newTestExecutor(map[string]any{
"data": map[string]any{
"id": "adebcfg",
"name": "John",
......@@ -99,11 +113,14 @@ func TestCollectionFilterExecute(t *testing.T) {
},
})
fld := field.String().AddOptions(tc.input)
err := Execute(context.Background(), fld, exec)
fld := field.String()
err := tc.input.Execute(exec, fld)
tc.assertErr(t, err)
want := field.String().AddOptions(tc.want)
want := field.String()
if tc.want != nil {
want = want.AddOptions(*tc.want)
}
assert.Equal(t, want, fld)
})
}
......
package executor
package template
import (
"context"
"git.perx.ru/perxis/perxis-go/pkg/schema/field"
)
......@@ -26,15 +24,14 @@ func (opt ItemFilter) GetName() string {
return "item_filter"
}
func (opt ItemFilter) Execute(ctx context.Context, exec Executor, fld *field.Field) error {
func (opt ItemFilter) Execute(exec Executor, fld *field.Field) error {
var err error
opt.Query, err = ExecuteAll(ctx, exec, opt.Query)
opt.Query, err = exec.ExecuteList(opt.Query)
if err != nil && !exec.IsCanceled() {
return err
}
opt.QueryError, err = exec.Execute(ctx, opt.QueryError)
opt.QueryError, err = exec.Execute(opt.QueryError)
if err != nil {
return err
}
......
package executor
package template
import (
"context"
"testing"
"git.perx.ru/perxis/perxis-go/pkg/schema/field"
......@@ -12,13 +11,15 @@ func TestItemFilter_Execute(t *testing.T) {
tests := []struct {
name string
input ItemFilter
want ItemFilter
want *ItemFilter
assertErr assert.ErrorAssertionFunc
}{
{
name: "empty",
input: ItemFilter{},
want: ItemFilter{},
want: &ItemFilter{
Query: []string{},
},
assertErr: assert.NoError,
},
{
......@@ -31,7 +32,7 @@ func TestItemFilter_Execute(t *testing.T) {
``,
},
},
want: ItemFilter{
want: &ItemFilter{
Query: []string{
`adebcfg`,
`first_name == "John"`,
......@@ -47,7 +48,7 @@ func TestItemFilter_Execute(t *testing.T) {
`{{ if .data.last_name -}} last_name == "{{ .data.last_name }}" {{- else -}} first_name == "{{ .data.first_name }}" {{- end }}`,
},
},
want: ItemFilter{
want: &ItemFilter{
Query: []string{
`first_name == "John"`,
`last_name == "Smith"`,
......@@ -64,7 +65,7 @@ func TestItemFilter_Execute(t *testing.T) {
},
QueryError: `{{ .Error }}`,
},
want: ItemFilter{
want: &ItemFilter{
QueryError: `Пожалуйста, укажите значение для поля "login"`,
},
assertErr: assert.NoError,
......@@ -76,18 +77,13 @@ func TestItemFilter_Execute(t *testing.T) {
`{{ if `,
},
},
want: ItemFilter{
Query: []string{
`{{ if `,
},
},
assertErr: assert.Error,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
exec := NewTemplateExecutor(map[string]any{
exec := newTestExecutor(map[string]any{
"data": map[string]any{
"id": "adebcfg",
"first_name": "John",
......@@ -95,11 +91,14 @@ func TestItemFilter_Execute(t *testing.T) {
},
})
fld := field.String().AddOptions(tc.input)
err := Execute(context.Background(), fld, exec)
fld := field.String()
err := tc.input.Execute(exec, fld)
tc.assertErr(t, err)
want := field.String().AddOptions(tc.want)
want := field.String()
if tc.want != nil {
want = want.AddOptions(*tc.want)
}
assert.Equal(t, want, fld)
})
}
......
package template
import (
"git.perx.ru/perxis/perxis-go/pkg/schema/field"
)
type Executor interface {
Execute(input string, data ...any) (string, error)
ExecuteList(inputs []string, data ...any) ([]string, error)
IsCanceled() bool
}
type Executable interface {
Execute(exec Executor, fld *field.Field) error
}
//nolint:gochecknoinits // init нужен для регистрации опций
func init() {
field.RegisterOption(CollectionFilter{})
field.RegisterOption(ItemFilter{})
}
package template
import (
"bytes"
"text/template"
"git.perx.ru/perxis/perxis-go/pkg/errors"
)
type testExecutor struct {
data map[string]any
}
func newTestExecutor(data map[string]any) *testExecutor {
return &testExecutor{
data: data,
}
}
func (exec *testExecutor) Execute(input string, _ ...any) (string, error) {
templ, err := exec.template().Parse(input)
if err != nil {
return "", err
}
var buf bytes.Buffer
err = templ.Execute(&buf, exec.data)
if err != nil {
return "", err
}
output := buf.String()
if output == "<no value>" {
return "", nil
}
return output, nil
}
func (exec *testExecutor) ExecuteList(inputs []string, _ ...any) ([]string, error) {
templ := exec.template()
result := make([]string, 0, len(inputs))
var buf bytes.Buffer
for _, input := range inputs {
t, err := templ.Parse(input)
if err != nil {
return nil, err
}
buf.Reset()
err = t.Execute(&buf, exec.data)
if err != nil {
return nil, err
}
output := buf.String()
if output == "" || output == "<no value>" {
continue
}
result = append(result, output)
}
return result, nil
}
func (exec *testExecutor) IsCanceled() bool {
_, ok := exec.data["Error"]
return ok
}
func (exec *testExecutor) template() *template.Template {
return template.New("test_executor").Funcs(template.FuncMap{
"error": func(text string) (string, error) {
if exec.data == nil {
exec.data = make(map[string]any)
}
exec.data["Error"] = text
return "", errors.New(text)
},
})
}
......@@ -3,6 +3,7 @@ package template
import (
"bytes"
"context"
"sync"
"text/template"
"git.perx.ru/perxis/perxis-go/pkg/collections"
......@@ -11,6 +12,16 @@ import (
"git.perx.ru/perxis/perxis-go/pkg/spaces"
)
type Option func(*Builder)
// WithStripEmptyValue заменяет "<no value>" на пустую строку при одиночной обработке,
// при множественной обработке удаляет строки с "<no value>" вместе с пустыми строками из результата.
func WithStripEmptyValue() Option {
return func(builder *Builder) {
builder.StripEmptyValue = true
}
}
type Builder struct {
ctx context.Context
cnt *content.Content
......@@ -19,26 +30,36 @@ type Builder struct {
CollID string
data map[string]interface{}
// Опции билдера
StripEmptyValue bool
// Для кеширования запросов
space *spaces.Space
environment *environments.Environment
collection *collections.Collection
}
func NewBuilder(cnt *content.Content, space, env, col string) *Builder {
return &Builder{
func NewBuilder(cnt *content.Content, space, env, col string, opts ...Option) *Builder {
b := &Builder{
ctx: context.Background(),
cnt: cnt,
SpaceID: space,
EnvID: env,
CollID: col,
}
for _, opt := range opts {
opt(b)
}
return b
}
func (b *Builder) getFuncs() template.FuncMap {
return template.FuncMap{
"lookup": getLookup(b),
"system": getSystem(b),
"error": getErrorFunc(b),
}
}
......@@ -89,23 +110,34 @@ func (b *Builder) Template() *template.Template {
}
func (b *Builder) Execute(str string, data ...any) (string, error) {
t := b.Template()
buf := new(bytes.Buffer)
t, err := t.Parse(str)
t, err := b.Template().Parse(str)
if err != nil {
return "", err
}
buf := getBuffer()
defer releaseBuffer(buf)
if err = t.Execute(buf, b.getData(data...)); err != nil {
return "", err
}
return buf.String(), nil
output := buf.String()
if b.StripEmptyValue && output == "<no value>" {
output = ""
}
return output, 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 {
result := make([]string, 0, len(str))
buf := getBuffer()
defer releaseBuffer(buf)
for _, tmpl := range str {
if tmpl == "" {
continue
}
......@@ -113,12 +145,20 @@ func (b *Builder) ExecuteList(str []string, data ...any) ([]string, error) {
if err != nil {
return []string{}, err
}
if err = t.Execute(buffer, b.getData(data...)); err != nil {
buf.Reset()
if err = t.Execute(buf, b.getData(data...)); err != nil {
return []string{}, err
}
result[i] = buffer.String()
buffer.Reset()
output := buf.String()
if b.StripEmptyValue && (output == "" || output == "<no value>") {
continue
}
result = append(result, output)
}
return result, nil
}
......@@ -150,6 +190,13 @@ func (b *Builder) ExecuteMap(str map[string]interface{}, data ...any) (map[strin
return result, nil
}
// IsCanceled возвращает true, если в процессе выполнения шаблона была вызвана
// шаблонная функция error.
func (b *Builder) IsCanceled() bool {
_, ok := b.data["Error"]
return ok
}
func (b *Builder) getData(data ...any) any {
if len(data) == 0 {
return b.data
......@@ -177,3 +224,20 @@ func mergeMaps(in ...map[string]interface{}) map[string]interface{} {
}
return out
}
//nolint:gochecknoglobals // Доступ к пулу необходим на глобальном уровне.
var bufferPool = &sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func getBuffer() *bytes.Buffer {
buf, _ := bufferPool.Get().(*bytes.Buffer)
return buf
}
func releaseBuffer(buf *bytes.Buffer) {
buf.Reset()
bufferPool.Put(buf)
}
......@@ -41,3 +41,15 @@ func getSystem(b *Builder) any {
return &System{builder: b}
}
}
// getErrorFunc возвращает функцию, которая устанавливает сообщение об ошибке с заданным текстом.
// Текст ошибки можно использовать в других шаблонах с помощью конструкции {{ .Error }}.
func getErrorFunc(b *Builder) func(string) (string, error) {
return func(text string) (string, error) {
if b.data == nil {
b.data = make(map[string]any)
}
b.data["Error"] = text
return "", errors.New(text)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment