Select Git revision
metrics_cache.go
field.go 12.97 KiB
package field
import (
"context"
"strings"
"git.perx.ru/perxis/perxis-go/pkg/data"
"git.perx.ru/perxis/perxis-go/pkg/errors"
"git.perx.ru/perxis/perxis-go/pkg/expr"
)
const (
FieldSeparator = "."
IncludeLimit = 10
)
type (
Preparer interface {
Prepare(f *Field) error
}
Fielder interface {
GetField(path string) *Field
}
)
type Translation struct {
Locale string `json:"locale,omitempty"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
}
type View struct {
Widget string `json:"widget,omitempty"` // Виджет для отображения поля в списке
Options map[string]interface{} `json:"options,omitempty"` // Опции виджета, на усмотрения виджета
}
type UI struct {
Widget string `json:"widget,omitempty"` // Имя виджета для отображения поля в пользовательском интерфейсе
Placeholder string `json:"placeholder,omitempty"` // Подсказка для заполнения значения
Options map[string]interface{} `json:"options,omitempty"` // Опции виджета для отображения
ReadView *View `json:"read_view,omitempty"` // Настройки для отображения экрана в режиме просмотра элемента
EditView *View `json:"edit_view,omitempty"` // Настройки для отображения экрана в режиме редактирования элемента
ListView *View `json:"list_view,omitempty"` // Настройки для отображения экрана в режиме списке элементов
}
type Include struct {
Ref string `json:"ref,omitempty"`
Optional bool `json:"optional,omitempty"`
}
type Field struct {
Title string `json:"title,omitempty"` // Название поля (Например: name)
Description string `json:"description,omitempty"` // Описание поле (Например: User name)
Translations []Translation `json:"translations,omitempty"` // Переводы данных на разных языках
UI *UI `json:"ui,omitempty"` // Опции пользовательского интерфейса
Includes []Include `json:"includes,omitempty"` // Импорт схем
SingleLocale bool `json:"single_locale,omitempty"` // Без перевода
Indexed bool `json:"indexed,omitempty"` // Построить индекс для поля
Unique bool `json:"unique,omitempty"` // Значение поля должны быть уникальными
TextSearch bool `json:"text_search,omitempty"` // Значение поля доступны для полнотекстового поиска
Params Parameters `json:"-"` // Параметры поля, определяет так же тип поля
Options Options `json:"options,omitempty"` // Дополнительные опции
Condition string `json:"condition,omitempty"` // Условие отображения поля
AdditionalValues bool `json:"additional_values,omitempty"` // Разрешает дополнительные значения вне ограничений правил
prepared bool
}
// TODO: Replace with Named field???
type PathField struct {
Field
Name string
Path string
}
type NamedField struct {
*Field
Name string
}
func NewField(params Parameters, opts ...interface{}) *Field {
f := &Field{}
f.Params = params
f.Options.Add(opts...)
return f
}
func (f Field) GetType() Type {
return f.Params.Type()
}
func (f *Field) AddOptions(t ...interface{}) *Field {
f.Options.Add(t...)
return f
}
func (f Field) WithUI(ui *UI) *Field {
f.UI = ui
return &f
}
func (f *Field) SetIncludes(includes ...interface{}) {
f.Includes = make([]Include, 0, len(includes))
for _, i := range includes {
switch v := i.(type) {
case string:
f.Includes = append(f.Includes, Include{Ref: v})
case Include:
f.Includes = append(f.Includes, v)
default:
panic("incorrect import type")
}
}
}
func (f Field) WithIncludes(includes ...interface{}) *Field {
f.SetIncludes(includes...)
return &f
}
func (f Field) GetIncludes() []string {
return f.getIncludes()
}
func (f Field) getIncludes() []string {
res := make([]string, len(f.Includes))
for i, inc := range f.Includes {
res[i] = inc.Ref
}
nested := f.GetNestedFields()
for _, fld := range nested {
res = append(res, fld.getIncludes()...)
}
return res
}
func (f Field) IsIncluded(name string) bool {
return data.GlobMatch(name, f.GetIncludes()...)
}
func (f Field) SetTitle(title string) *Field {
f.Title = title
return &f
}
func (f Field) SetDescription(desc string) *Field {
f.Description = desc
return &f
}
func (f Field) AddTranslation(locale, title, desc string) *Field {
for i, t := range f.Translations {
if t.Locale == locale {
f.Translations[i] = Translation{Locale: locale, Title: title, Description: desc}
return &f
}
}
f.Translations = append(f.Translations, Translation{Locale: locale, Title: title, Description: desc})
return &f
}
func (f Field) SetSingleLocale(r bool) *Field {
f.SingleLocale = r
return &f
}
func (f Field) SetIndexed(r bool) *Field {
f.Indexed = r
return &f
}
func (f Field) SetAdditionalValues() *Field {
f.AdditionalValues = true
return &f
}
func (f Field) SetUnique(r bool) *Field {
f.Unique = r
return &f
}
func (f Field) SetTextSearch(r bool) *Field {
f.TextSearch = r
return &f
}
func (f Field) SetCondition(c string) *Field {
f.Condition = c
return &f
}
func (f *Field) MustEnabled(ctx context.Context) bool {
if enabled, err := f.IsEnabled(ctx); !enabled || err != nil {
return false
}
return true
}
func (f *Field) IsEnabled(ctx context.Context) (bool, error) {
if f.Condition != "" {
out, err := expr.Eval(ctx, f.Condition, nil)
if err != nil {
return false, err
}
if enabled, ok := out.(bool); ok {
return enabled, nil
}
return false, errors.New("condition returns non-boolean value")
}
return true, nil
}
// Walk - выполняет обход данных по схеме и выполняет функцию, которая может модифицировать данные при необходимости
func (f *Field) Walk(ctx context.Context, v interface{}, fn WalkFunc, opt ...WalkOption) (interface{}, bool, error) {
res, err := fn(ctx, f, v)
if err != nil {
return nil, false, err
}
if res.Changed || res.Stop {
return res.Value, res.Changed, err
}
if res.Context != nil {
ctx = res.Context
}
if walker, ok := f.GetType().(FieldWalker); ok {
val, changed, err := walker.Walk(ctx, f, v, fn, NewWalkOptions(opt...))
if err != nil {
return nil, false, err
}
return val, changed, err
}
return v, false, nil
}
// DEPRECATED
func (f *Field) Prepare() error {
if preparer, ok := f.GetType().(Preparer); ok {
if err := preparer.Prepare(f); err != nil {
return err
}
}
for _, o := range f.Options {
if preparer, ok := o.(Preparer); ok {
if err := preparer.Prepare(f); err != nil {
return err
}
}
}
return nil
}
// GetField возвращает поле по строковому пути
func (f *Field) GetField(path string) *Field {
if path == "" {
switch params := f.Params.(type) {
case *ArrayParameters:
// Возвращаем поле Item если путь указан как "arr."
return params.Item
}
return nil
}
switch params := f.Params.(type) {
case *ObjectParameters:
pp := strings.SplitN(path, FieldSeparator, 2)
for k, v := range params.Fields {
p, ok := v.Params.(*ObjectParameters)
if ok && p.Inline {
f := v.GetField(path)
if f != nil {
return f
}
}
if k == pp[0] {
if len(pp) == 1 {
return v
}
return v.GetField(pp[1])
}
}
case Fielder:
return params.GetField(path)
case *ArrayParameters:
return params.Item.GetField(path)
}
return nil
}
// GetFieldsPath возвращает полный путь для массива полей
func GetFieldsPath(flds []PathField) (res []string) {
for _, f := range flds {
res = append(res, f.Path)
}
return res
}
type FilterFunc func(*Field, string) bool
func GetAll(field *Field, path string) bool { return true }
func (f *Field) GetFields(filterFunc FilterFunc, pathPrefix ...string) (res []PathField) {
var path string
if len(pathPrefix) > 0 {
path = pathPrefix[0]
}
// добавление корневого объекта для чего-то нужно?
if path != "" && filterFunc(f, path) {
res = append(res, PathField{
Field: *f,
Path: path,
})
}
switch params := f.Params.(type) {
case *ObjectParameters:
res = append(res, getFieldsObject(path, params, filterFunc, false)...)
case *ArrayParameters:
res = append(res, getFieldsArray(path, params, filterFunc)...)
}
// if len(pathPrefix) > 0 {
// for _, r := range res {
// r.Path = strings.Join([]string{pathPrefix[0], r.Path}, FieldSeparator)
// }
// }
return res
}
func getFieldsArray(path string, params *ArrayParameters, filterFunc FilterFunc) (res []PathField) {
switch params := params.Item.Params.(type) {
case *ObjectParameters:
res = append(res, getFieldsObject(path, params, filterFunc, params.Inline)...)
case *ArrayParameters:
res = append(res, getFieldsArray(path, params, filterFunc)...)
}
return res
}
func getFieldsObject(path string, params *ObjectParameters, filterFunc FilterFunc, ignoreInline bool) (res []PathField) {
for k, v := range params.Fields {
if v == nil {
continue
}
var newPath string
lastIdx := strings.LastIndex(path, ".")
if path == "" || !ignoreInline && params.Inline && lastIdx < 0 {
newPath = k
} else {
if !params.Inline || ignoreInline {
newPath = strings.Join([]string{path, k}, FieldSeparator)
} else {
newPath = strings.Join([]string{path[:lastIdx], k}, FieldSeparator)
}
}
if flds := v.GetFields(filterFunc, newPath); len(flds) > 0 {
res = append(res, flds...)
}
}
return res
}
func (f *Field) GetNestedFields() []*Field {
switch params := f.Params.(type) {
case *ObjectParameters:
flds := make([]*Field, 0, len(params.Fields))
for _, v := range params.Fields {
if v == nil {
continue
}
flds = append(flds, v)
}
return flds
case *ArrayParameters:
return []*Field{params.Item}
}
return nil
}
// Clone создает копию поля
// Параметр reset указывает необходимо ли отвязать параметры поля от вложенных полей
func (f Field) Clone(reset bool) *Field {
if f.UI != nil {
ui := *f.UI
f.UI = &ui
}
if len(f.Translations) > 0 {
f.Translations = append(make([]Translation, 0, len(f.Translations)), f.Translations...)
}
if f.Options != nil {
opts := make(Options)
for k, v := range f.Options {
opts[k] = v
}
f.Options = opts
}
if f.Params != nil {
f.Params = f.Params.Clone(reset)
}
return &f
}
func (f *Field) mergeField(fld *Field) error {
if f.Title == "" {
f.Title = fld.Title
}
if f.Description == "" {
f.Description = fld.Description
}
if len(f.Translations) == 0 {
f.Translations = fld.Translations
}
if f.UI == nil {
f.UI = fld.UI
}
if len(f.Includes) > 0 {
f.Includes = fld.Includes
}
if f.Params == nil {
f.Params = fld.Params
} else if fld.Params != nil {
type Merger interface {
Merge(parameters Parameters) error
}
if merger, ok := f.Params.(Merger); ok {
if err := merger.Merge(fld.Params); err != nil {
return err
}
}
}
if f.Options == nil {
f.Options = fld.Options
}
if f.Condition == "" {
f.Condition = fld.Condition
}
return nil
}
func (f *Field) Merge(fields ...*Field) error {
for _, fld := range fields {
f.mergeField(fld)
}
return nil
}
func (f *Field) loadIncludes(ctx context.Context, loader Loader, depth int) error {
if depth > IncludeLimit {
return errors.New("limit for included fields exceeded")
}
for _, i := range f.Includes {
if loader == nil {
panic("schema loader not set")
}
importedField, err := loader.Load(ctx, i.Ref)
if err != nil {
if i.Optional {
continue
}
return err
}
for _, fld := range importedField {
depth += 1
if err := fld.loadIncludes(ctx, loader, depth); err != nil {
return err
}
}
if err = f.Merge(importedField...); err != nil {
return err
}
}
for _, i := range f.GetNestedFields() {
if err := i.loadIncludes(ctx, loader, depth); err != nil {
return err
}
}
return nil
}
func (f *Field) LoadIncludes(ctx context.Context, loader Loader) error {
return f.loadIncludes(ctx, loader, 0)
}
func (f *Field) LoadRef(ctx context.Context, ref string, loader Loader) error {
f.SetIncludes(ref)
return f.LoadIncludes(ctx, loader)
}