Skip to content
Snippets Groups Projects
Select Git revision
  • 80c8f341de024aa0f53b9adab447af1bf726a2ec
  • master default protected
  • feature/PRXS-3383-CollectionsSort
  • refactor/PRXS-3053-Files
  • feature/PRXS-3143-3235-ReferenceOptions
  • feature/PRXS-3421-ImplementNewRefAPI
  • feature/PRXS-3143-LimitReferenceFields
  • feature/PRXS-3234-FeaturePruneIdents
  • feature/3149-LocaleCodeAsID-Feature
  • PRXS-3421-RecursiveReferences
  • 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
  • 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

storage.go

Blame
  • field.go 16.30 KiB
    package field
    
    import (
    	"context"
    	"strings"
    
    	"git.perx.ru/perxis/perxis-go/data"
    	"git.perx.ru/perxis/perxis-go/expr"
    	"git.perx.ru/perxis/perxis-go/pkg/errors"
    )
    
    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"`
    }
    
    // State - состояние поля времени выполнения
    type State struct {
    	Name         string
    	DataPath     string
    	SchemaPath   string
    	SingleLocale bool
    	Parent       *Field
    	Inlined      bool
    	HasInline    bool
    }
    
    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"` // Разрешает дополнительные значения вне ограничений правил
    	State            *State        `json:"-"`                           // Состояние поля времени выполнения
    }
    
    // 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
    }
    
    // GetState возвращает состояние поля времени выполнения
    func (f Field) GetState() *State {
    	return f.State
    }
    
    // ClearState очищает состояние поля и всех вложенных полей
    //
    // Схемы нельзя сравнивать с активным состоянием с помощью `reflect.DeepEqual` или `assert.Equal`.
    // Предварительно нужно сделать `ClearState`.
    // После очистки состояния полей не будут рассчитываться. Для повторного включения состояния используйте `EnableState`
    func (f *Field) ClearState() *Field {
    	f.State = nil
    	for _, fld := range f.ListFields() {
    		fld.ClearState()
    	}
    	return f
    }
    
    // EnableState включает расчет состояния поля и всех вложенных полей
    //
    // Без включения состояния поля, невозможно получить доступ к данным времени выполнения
    // schema.New включает состояние для схемы при создании
    func (f *Field) EnableState() {
    	f.State = &State{}
    }
    
    func (f Field) GetType() Type {
    	if f.Params == nil {
    		return nil
    	}
    
    	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) IsSingleLocale() bool {
    	return f.SingleLocale || (f.State != nil && f.State.SingleLocale)
    }
    
    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
    }
    
    func (f *Field) SetFieldState(name string, fld *Field) *Field {
    	if f != nil && fld != nil && f.State != nil && fld.State == nil {
    		fld.State = f.getFieldState(name, fld)
    	}
    	return fld
    }
    
    // GetFieldState возвращает состояние вложенного поля
    func (f *Field) getFieldState(name string, fld *Field) *State {
    	if f.State == nil {
    		return nil
    	}
    
    	state := State{
    		SchemaPath: name,
    		DataPath:   name,
    		Name:       name,
    	}
    
    	dataPath := f.State.DataPath
    
    	switch params := f.Params.(type) {
    	case *ObjectParameters:
    		if params.Inline {
    			last := strings.LastIndex(dataPath, ".")
    			if last > 0 {
    				dataPath = dataPath[:last]
    			} else {
    				dataPath = ""
    			}
    			state.Inlined = true
    		}
    		if dataPath != "" {
    			state.DataPath = dataPath + FieldSeparator + state.DataPath
    		}
    
    	case *ArrayParameters:
    		state.DataPath = dataPath // Remove item from path
    	}
    
    	state.SingleLocale = f.IsSingleLocale() || fld.SingleLocale
    
    	if f.State.SchemaPath != "" {
    		state.SchemaPath = f.State.SchemaPath + FieldSeparator + state.SchemaPath
    	}
    	state.Parent = f
    	state.HasInline = f.State.HasInline || state.Inlined
    	return &state
    }
    
    func (f *Field) GetFieldByName(name string) *Field {
    	return f.Params.GetField(f, name)
    }
    
    // GetField возвращает поле по строковому пути
    func (f *Field) GetField(path string) *Field {
    	name := ""
    	parts := strings.SplitN(path, FieldSeparator, 2)
    
    	if len(parts) > 0 {
    		name = parts[0]
    	}
    
    	fld := f.GetFieldByName(name)
    
    	if fld != nil && len(parts) > 1 {
    		return fld.GetField(parts[1])
    	}
    
    	return fld
    }
    
    // ListFields возвращает массив вложенных полей данного поля
    func (f *Field) ListFields(filter ...FieldFilterFunc) []*Field {
    	fields := f.Params.ListFields(f, filter...)
    	return fields
    }
    
    // ListFieldsRecursive возвращает массив всех вложенных полей рекурсивно
    func (f *Field) ListFieldsRecursive(filter ...FieldFilterFunc) []*Field {
    	fields := f.ListFields(filter...)
    	for _, fld := range f.ListFields() {
    		fields = append(fields, fld.ListFieldsRecursive(filter...)...)
    	}
    	return fields
    }
    
    // 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 }
    
    // GetFields возвращает массив полей с путем???
    // DEPRECATED: использовать ListFields или ListFieldsRecursive
    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
    }
    
    // GetNestedFields возвращает вложенные поля
    // DEPRECATED: использовать ListFields
    func (f *Field) GetNestedFields() []*Field {
    	return f.ListFields()
    
    	//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)
    }