Skip to content
Snippets Groups Projects
Select Git revision
  • d5f1daad3e38c63289a059199a36abd0c7162ba4
  • master default protected
  • fix/PRXS-3401-ValidateValidationOpts
  • feature/PRXS-3383-CollectionsRankSortAPI
  • feature/3149-LocaleCodeAsID-Feature
  • feature/PRXS-3383-CollectionsSort
  • feature/3109-SerializeFeature
  • release/0.33
  • feature/3109-RecoverySchema
  • feature/3109-feature
  • fix/PRXS-3369-ValidateFields
  • refactor/PRXS-3306-MovePkgGroup1
  • refactor/6-pkg-refactor-expr
  • fix/PRXS-3360-TemplateBuilderPatch
  • feature/3293-MongoV2
  • feature/3272-GoVersionUp
  • feature/PRXS-3218-HideTemplateActions
  • feature/PRXS-3234-PruneIdents
  • feature/3146-UpdateItemStorageInterface
  • feature/3274-ObjectIndexesFixes
  • feature/PRXS-3143-3235-ReferenceOptions
  • v0.33.1
  • v0.32.0
  • v0.31.1
  • v0.31.0
  • v0.30.0
  • v0.29.0
  • v0.28.0
  • v0.27.0-alpha.1+16
  • v0.27.0-alpha.1+15
  • v0.27.0-alpha.1+14
  • v0.27.0-alpha.1+13
  • v0.27.0-alpha.1+12
  • v0.27.0-alpha.1+11
  • v0.27.0-alpha.1+10
  • v0.27.0-alpha.1+9
  • v0.27.0-alpha.1+8
  • v0.27.0-alpha.1+7
  • v0.27.0-alpha.1+6
  • v0.27.0-alpha.1+5
  • v0.27.0-alpha.1+4
41 results

metrics_cache.go

Blame
  • 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)
    }