Skip to content
Snippets Groups Projects
Commit 932eb6c5 authored by Pavel Antonov's avatar Pavel Antonov :asterisk:
Browse files

Merge branch 'feature/PRXS-2838-SetupRuntimeConfig' into 'master'

Обновление работы Setup

See merge request perxis/perxis-go!328
parents a2533d80 47a4a6f7
No related branches found
No related tags found
No related merge requests found
...@@ -4,8 +4,9 @@ import ( ...@@ -4,8 +4,9 @@ import (
"context" "context"
"strings" "strings"
"git.perx.ru/perxis/perxis-go/pkg/collections"
"git.perx.ru/perxis/perxis-go/pkg/data" "git.perx.ru/perxis/perxis-go/pkg/data"
"git.perx.ru/perxis/perxis-go/pkg/collections"
"git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/errors"
"go.uber.org/zap" "go.uber.org/zap"
) )
...@@ -16,112 +17,145 @@ var ( ...@@ -16,112 +17,145 @@ var (
ErrUninstallCollections = errors.New("failed to uninstall collections") ErrUninstallCollections = errors.New("failed to uninstall collections")
) )
type CollectionsOption func(c *CollectionConfig) //
type UpdateCollectionFn func(s *Setup, exist, new *collections.Collection) (coll *collections.Collection, upd bool, setSchema bool, err error) // Collection Configuration
type DeleteCollectionFn func(s *Setup, col *collections.Collection) (bool, error) //
type CollectionConfig struct { type (
collection *collections.Collection Collection = Entity[CollectionConf, *collections.Collection]
UpdateFn UpdateCollectionFn Collections = EntityList[CollectionConf, *collections.Collection]
DeleteFn DeleteCollectionFn CollectionOption = EntityOption[CollectionConf, *collections.Collection]
CollectionFilter = FilterFunc[*collections.Collection]
CollectionConf struct {
SkipMigration bool SkipMigration bool
} }
)
func NewCollectionConfig(collection *collections.Collection, opt ...CollectionsOption) (c CollectionConfig, err error) { var (
collection = collection.Clone() OverwriteCollection = Overwrite[CollectionConf, *collections.Collection]
KeepCollection = Keep[CollectionConf, *collections.Collection]
DeleteCollection = Delete[CollectionConf, *collections.Collection]
DeleteCollectionIfRemoveFlag = DeleteIfRemoveFlag[CollectionConf, *collections.Collection]
)
if collection.Schema != nil { // Init инициализирует конфигурацию коллекции
// приведение внутренних типов схемы, чтобы избежать возможного несоответствия типов при func (CollectionConf) Init(e *Entity[CollectionConf, *collections.Collection]) {
// сравнивании схем (`[]interface{}/[]string`, `int/int64`, etc.) // Сформированная вручную схема может иметь разные типы данных для одних и тех же полей
err = collection.Schema.ConvertTypes() // (`[]interface{}/[]string`, `int/int64`, etc.), что может привести к ошибкам
if err != nil { // при сравнении схем. Поэтому приводим все типы данных к одному типу.
return if e.value.Schema != nil {
if err := e.value.Schema.ConvertTypes(); err != nil {
panic(err)
} }
} }
c = CollectionConfig{collection: collection} // Устанавливаем стратегии обновления и удаления коллекции по умолчанию
UpdateExistingCollection()(e)
DefaultUpdateCollectionStrategy()(&c) DeleteCollectionIfRemoveFlag()(e)
DeleteCollectionIfRemove()(&c)
for _, o := range opt {
o(&c)
} }
return c, nil func IsSchemaUpdateRequired(old, new *collections.Collection) bool {
return !new.IsView() && (old == nil || !old.Schema.Equal(new.Schema))
} }
func AddMetadata(key, value string) CollectionsOption { // AddMetadata добавляет метаданные к коллекции
return func(c *CollectionConfig) { func AddMetadata(key, value string) CollectionOption {
c.collection.Schema.WithMetadata(key, value) return func(e *Collection) {
e.ValueFunc = append(e.ValueFunc, func(s *Setup, c *collections.Collection) {
if c.Schema != nil {
c.Schema.WithMetadata(key, value)
}
})
} }
} }
func SkipMigration() CollectionsOption { // SkipMigration пропускает миграцию коллекции
return func(c *CollectionConfig) { func SkipMigration() CollectionOption {
c.SkipMigration = true return func(e *Collection) {
e.Conf.SkipMigration = true
} }
} }
func OverwriteCollection() CollectionsOption { // UpdateExistingCollection обновляет существующую коллекцию
return func(c *CollectionConfig) { func UpdateExistingCollection() CollectionOption {
c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool, error) { return func(e *Collection) {
update := new.Name != old.Name || new.IsSingle() != old.IsSingle() || new.IsSystem() != old.IsSystem() || e.UpdateFunc = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool) {
new.IsNoData() != old.IsNoData() || new.Hidden != old.Hidden || !new.View.Equal(old.View) || !data.ElementsMatch(old.Tags, new.Tags) // Копируем теги из старой коллекции в новую
if len(old.Tags) > 0 {
return new, update, !old.Schema.Equal(new.Schema), nil new.Tags = data.SetFromSlice(append(old.Tags, new.Tags...))
}
} }
var update bool
update = new.Name != old.Name || new.IsSingle() != old.IsSingle() || new.IsSystem() != old.IsSystem() ||
new.IsNoData() != old.IsNoData() || new.Hidden != old.Hidden || new.IsView() != old.IsView() && data.ElementsMatch(old.Tags, new.Tags)
if old.View != nil && new.View != nil {
update = update || *old.View != *new.View
} }
func KeepExistingCollection() CollectionsOption { return new, update || IsSchemaUpdateRequired(old, new)
return func(c *CollectionConfig) {
c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool, error) {
return old, false, false, nil
} }
} }
} }
func DeleteCollection() CollectionsOption { //
return func(c *CollectionConfig) { // Collection Setup
c.DeleteFn = func(s *Setup, collection *collections.Collection) (bool, error) { return true, nil } //
}
}
func DeleteCollectionIfRemove() CollectionsOption { // AddCollection добавляет требование к настройке коллекции в пространстве (runtime)
return func(c *CollectionConfig) { func (s *Setup) AddCollection(collection *collections.Collection, opt ...CollectionOption) *Setup {
c.DeleteFn = func(s *Setup, collection *collections.Collection) (bool, error) { return s.IsRemove(), nil } s.Config.Collections.Add(collection, opt...)
} return s
} }
func DefaultUpdateCollectionStrategyFn(_ *Setup, exist, collection *collections.Collection) (*collections.Collection, bool, bool, error) { // AddCollections добавляет несколько требований к настройке коллекций в пространстве (runtime)
if len(exist.Tags) > 0 { func (s *Setup) AddCollections(collections []*collections.Collection, opt ...CollectionOption) *Setup {
collection.Tags = data.SetFromSlice(append(exist.Tags, collection.Tags...)) for _, c := range collections {
s.AddCollection(c, opt...)
}
return s
} }
var update, setSchema bool func (s *Setup) InstallCollection(ctx context.Context, c *Collection) (updateSchema bool, err error) {
update = collection.Name != exist.Name || collection.IsSingle() != exist.IsSingle() || collection.IsSystem() != exist.IsSystem() || collection := c.Value(s)
collection.IsNoData() != exist.IsNoData() || collection.Hidden != exist.Hidden || collection.IsView() != exist.IsView() && data.ElementsMatch(exist.Tags, collection.Tags) collection.SpaceID, collection.EnvID = s.SpaceID, s.EnvironmentID
if exist.View != nil && collection.View != nil { var exist *collections.Collection
update = update && *exist.View == *collection.View // isForce - не удалять коллекцию, если она уже существует
exist, err = s.content.Collections.Get(ctx, collection.SpaceID, collection.EnvID, collection.ID)
if err != nil && !strings.Contains(err.Error(), collections.ErrNotFound.Error()) {
return false, err
} }
setSchema = !collection.IsView() && !exist.Schema.Equal(collection.Schema) if exist == nil {
if _, err = s.content.Collections.Create(ctx, collection); err != nil {
return collection, update, setSchema, nil return false, err
}
} else {
var updateCollection bool
collection, updateCollection = c.UpdateFunc(s, exist, collection)
if !updateCollection {
return false, nil
} }
func DefaultUpdateCollectionStrategy() CollectionsOption { // TODO: Проверить, что коллекция изменилась
return func(c *CollectionConfig) { // TODO: Тест на сравнение схем
c.UpdateFn = DefaultUpdateCollectionStrategyFn // Замена возможного алиаса окружения на реального ID окружения перед сравнением
collection.EnvID = exist.EnvID
if !exist.Equal(collection) {
if err = s.content.Collections.Update(ctx, collection); err != nil {
return false, err
}
} }
} }
func WithUpdateCollectionStrategy(fn UpdateCollectionFn) CollectionsOption { // Проверяем, нужно ли обновить схему коллекции
return func(c *CollectionConfig) { if IsSchemaUpdateRequired(exist, collection) {
c.UpdateFn = fn return true, s.content.Collections.SetSchema(ctx, collection.SpaceID, collection.EnvID, collection.ID, collection.Schema)
} }
return false, nil
} }
func (s *Setup) InstallCollections(ctx context.Context) (err error) { func (s *Setup) InstallCollections(ctx context.Context) (err error) {
...@@ -136,14 +170,16 @@ func (s *Setup) InstallCollections(ctx context.Context) (err error) { ...@@ -136,14 +170,16 @@ func (s *Setup) InstallCollections(ctx context.Context) (err error) {
for _, c := range s.Collections { for _, c := range s.Collections {
setSchema, err = s.InstallCollection(ctx, c) setSchema, err = s.InstallCollection(ctx, c)
if err != nil { if err != nil {
collection := c.Value(s)
s.logger.Error("Failed to install collection", s.logger.Error("Failed to install collection",
zap.String("Collection ID", c.collection.ID), zap.String("Collection ID", collection.ID),
zap.String("Collection Name", c.collection.Name), zap.String("Collection Name", collection.Name),
zap.Error(err), zap.Error(err),
) )
return errors.WithDetailf(errors.Wrap(err, "failed to install collection"), "Возникла ошибка при настройке коллекции %s(%s)", c.collection.Name, c.collection.ID) return errors.WithDetailf(errors.Wrap(err, "failed to install collection"), "Возникла ошибка при настройке коллекции %s(%s)", collection.Name, collection.ID)
} }
if !c.SkipMigration && setSchema {
if !c.Conf.SkipMigration && setSchema {
migrate = true migrate = true
} }
} }
...@@ -164,46 +200,6 @@ func (s *Setup) InstallCollections(ctx context.Context) (err error) { ...@@ -164,46 +200,6 @@ func (s *Setup) InstallCollections(ctx context.Context) (err error) {
return nil return nil
} }
func (s *Setup) InstallCollection(ctx context.Context, c CollectionConfig) (setSchema bool, err error) {
collection := c.collection
collection.SpaceID, collection.EnvID = s.SpaceID, s.EnvironmentID
var exist *collections.Collection
// isForce - не удалять коллекцию, если она уже существует
exist, err = s.content.Collections.Get(ctx, collection.SpaceID, collection.EnvID, collection.ID)
if err != nil && !strings.Contains(err.Error(), collections.ErrNotFound.Error()) {
return false, err
}
if exist == nil {
setSchema = !collection.IsView()
_, err = s.content.Collections.Create(ctx, collection)
if err != nil {
return false, err
}
} else {
var upd bool
collection, upd, setSchema, err = c.UpdateFn(s, exist, c.collection)
if err != nil {
return false, err
}
if upd {
if err = s.content.Collections.Update(ctx, collection); err != nil {
return false, err
}
}
}
if setSchema {
err = s.content.Collections.SetSchema(ctx, collection.SpaceID, collection.EnvID, collection.ID, collection.Schema)
if err != nil {
return false, err
}
}
return setSchema, nil
}
func (s *Setup) CheckCollections(ctx context.Context) error { func (s *Setup) CheckCollections(ctx context.Context) error {
if len(s.Collections) == 0 { if len(s.Collections) == 0 {
return nil return nil
...@@ -213,8 +209,9 @@ func (s *Setup) CheckCollections(ctx context.Context) error { ...@@ -213,8 +209,9 @@ func (s *Setup) CheckCollections(ctx context.Context) error {
var errs []error var errs []error
for _, c := range s.Collections { for _, c := range s.Collections {
if err := s.CheckCollection(ctx, c); err != nil { collection := c.Value(s)
errs = append(errs, errors.WithDetailf(err, "Не найдена коллекция %s(%s)", c.collection.ID, c.collection.Name)) if err := s.CheckCollection(ctx, collection); err != nil {
errs = append(errs, errors.WithDetailf(err, "Не найдена коллекция %s(%s)", collection.ID, collection.Name))
} }
} }
...@@ -225,8 +222,8 @@ func (s *Setup) CheckCollections(ctx context.Context) error { ...@@ -225,8 +222,8 @@ func (s *Setup) CheckCollections(ctx context.Context) error {
return nil return nil
} }
func (s *Setup) CheckCollection(ctx context.Context, c CollectionConfig) (err error) { func (s *Setup) CheckCollection(ctx context.Context, c *collections.Collection) (err error) {
_, err = s.content.Collections.Get(ctx, s.SpaceID, s.EnvironmentID, c.collection.ID) _, err = s.content.Collections.Get(ctx, s.SpaceID, s.EnvironmentID, c.ID)
return err return err
} }
...@@ -239,28 +236,30 @@ func (s *Setup) UninstallCollections(ctx context.Context) error { ...@@ -239,28 +236,30 @@ func (s *Setup) UninstallCollections(ctx context.Context) error {
for _, c := range s.Collections { for _, c := range s.Collections {
if err := s.UninstallCollection(ctx, c); err != nil { if err := s.UninstallCollection(ctx, c); err != nil {
collection := c.Value(s)
s.logger.Error("Failed to uninstall collection", s.logger.Error("Failed to uninstall collection",
zap.String("Collection ID", c.collection.ID), zap.String("Collection ID", collection.ID),
zap.String("Collection Name", c.collection.Name), zap.String("Collection Name", collection.Name),
zap.Error(err), zap.Error(err),
) )
return errors.WithDetailf(errors.Wrap(err, "failed to uninstall collection"), "Возникла ошибка при удалении коллекции %s(%s)", c.collection.Name, c.collection.ID) return errors.WithDetailf(errors.Wrap(err, "failed to uninstall collection"),
"Возникла ошибка при удалении коллекции %s(%s)", collection.Name, collection.ID)
} }
} }
return nil return nil
} }
func (s *Setup) UninstallCollection(ctx context.Context, c CollectionConfig) error { func (s *Setup) UninstallCollection(ctx context.Context, c *Collection) error {
ok, err := c.DeleteFn(s, c.collection) collection := c.Value(s)
if err != nil { deleteCollection := c.DeleteFunc(s, collection)
return err if deleteCollection {
} if err := s.content.Collections.Delete(ctx, s.SpaceID, s.EnvironmentID, collection.ID); err != nil && !strings.Contains(err.Error(), collections.ErrNotFound.Error()) {
if ok {
if err = s.content.Collections.Delete(ctx, s.SpaceID, s.EnvironmentID, c.collection.ID); err != nil && !strings.Contains(err.Error(), collections.ErrNotFound.Error()) {
return err return err
} }
s.removeItems(c.collection.ID) // после удаления коллекции нет смысла удалять ее элементы
// TODO: Проверить, в чем смысл происходящего
//s.removeItems(c.collection.ID) // после удаления коллекции нет смысла удалять ее элементы
} }
return nil return nil
} }
...@@ -4,20 +4,69 @@ import ( ...@@ -4,20 +4,69 @@ import (
"context" "context"
"testing" "testing"
"git.perx.ru/perxis/perxis-go/pkg/collections"
mockscollections "git.perx.ru/perxis/perxis-go/pkg/collections/mocks" mockscollections "git.perx.ru/perxis/perxis-go/pkg/collections/mocks"
"git.perx.ru/perxis/perxis-go/pkg/content" "git.perx.ru/perxis/perxis-go/pkg/content"
"git.perx.ru/perxis/perxis-go/pkg/environments" "git.perx.ru/perxis/perxis-go/pkg/environments"
envmocks "git.perx.ru/perxis/perxis-go/pkg/environments/mocks" envmocks "git.perx.ru/perxis/perxis-go/pkg/environments/mocks"
"git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/errors"
"github.com/stretchr/testify/mock"
"git.perx.ru/perxis/perxis-go/pkg/collections"
"git.perx.ru/perxis/perxis-go/pkg/schema" "git.perx.ru/perxis/perxis-go/pkg/schema"
"git.perx.ru/perxis/perxis-go/pkg/schema/field" "git.perx.ru/perxis/perxis-go/pkg/schema/field"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
) )
func TestSetup_InstallCollections(t *testing.T) { // Некорректный тест, OverwriteCollection всегда перезаписывает информацию
func TestCollection_UpdateExistingCollection(t *testing.T) {
tests := []struct {
name string
old, new *collections.Collection
wantUpdate bool
wantSetSchema bool
wantErr bool
}{
{
name: "Equal collections",
old: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Schema: schema.New("a", field.String())},
new: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Schema: schema.New("a", field.String())},
wantUpdate: false,
wantSetSchema: false,
},
{
name: "Schema changed",
old: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Name: "Coll", Schema: schema.New("a", field.String())},
new: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Name: "Coll", Schema: schema.New("b", field.String())},
wantUpdate: true,
wantSetSchema: true,
},
{
name: "Collection name changed",
old: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Name: "Coll1", Schema: schema.New("a", field.String())},
new: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Name: "Coll2", Schema: schema.New("a", field.String())},
wantUpdate: true,
wantSetSchema: false,
},
{
name: "Collection View changed",
old: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", View: &collections.View{SpaceID: "sp1", EnvID: "env1", CollectionID: "coll1"}},
new: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", View: &collections.View{SpaceID: "sp2", EnvID: "env2", CollectionID: "coll2"}},
wantUpdate: true,
wantSetSchema: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := new(Collection)
UpdateExistingCollection()(c)
collection, update := c.UpdateFunc(new(Setup), tt.old, tt.new)
assert.Equal(t, tt.wantUpdate, update)
assert.Equal(t, tt.wantSetSchema, IsSchemaUpdateRequired(tt.old, collection))
})
}
}
func TestCollections_InstallCollections(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
collections []*collections.Collection collections []*collections.Collection
...@@ -75,7 +124,9 @@ func TestSetup_InstallCollections(t *testing.T) { ...@@ -75,7 +124,9 @@ func TestSetup_InstallCollections(t *testing.T) {
collections: []*collections.Collection{{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}}, collections: []*collections.Collection{{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}},
collectionsCall: func(svc *mockscollections.Collections) { collectionsCall: func(svc *mockscollections.Collections) {
svc.On("Get", mock.Anything, "sp", "env", "1").Return(nil, errors.New("not found")).Once() svc.On("Get", mock.Anything, "sp", "env", "1").Return(nil, errors.New("not found")).Once()
svc.On("Create", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}).Return(nil, errors.New("some error")).Once() svc.On("Create", mock.Anything,
&collections.Collection{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}).
Return(nil, errors.New("some error")).Once()
}, },
wantErr: func(t *testing.T, err error) { wantErr: func(t *testing.T, err error) {
assert.Error(t, err) assert.Error(t, err)
...@@ -168,52 +219,3 @@ func TestSetup_InstallCollections(t *testing.T) { ...@@ -168,52 +219,3 @@ func TestSetup_InstallCollections(t *testing.T) {
}) })
} }
} }
func TestOverwriteCollection(t *testing.T) {
tests := []struct {
name string
old, new *collections.Collection
wantUpdate bool
wantSetSchema bool
wantErr bool
}{
{
name: "Equal collections should not be updated",
old: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Schema: schema.New("a", field.String())},
new: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Schema: schema.New("a", field.String())},
wantUpdate: false,
wantSetSchema: false,
},
{
name: "For collections with different schemas and equal other params schemas should be set",
old: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Name: "Coll", Schema: schema.New("a", field.String())},
new: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Name: "Coll", Schema: schema.New("b", field.String())},
wantUpdate: false,
wantSetSchema: true,
},
{
name: "Collections with different names should be updated",
old: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Name: "Coll1", Schema: schema.New("a", field.String())},
new: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Name: "Coll2", Schema: schema.New("a", field.String())},
wantUpdate: true,
wantSetSchema: false,
},
{
name: "Collections with different view params should be updated",
old: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", View: &collections.View{SpaceID: "sp1", EnvID: "env1", CollectionID: "coll1"}},
new: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", View: &collections.View{SpaceID: "sp2", EnvID: "env2", CollectionID: "coll2"}},
wantUpdate: true,
wantSetSchema: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := new(CollectionConfig)
OverwriteCollection()(c)
_, update, setSchema, err := c.UpdateFn(new(Setup), tt.old, tt.new)
require.NoError(t, err)
assert.Equal(t, tt.wantUpdate, update)
assert.Equal(t, tt.wantSetSchema, setSchema)
})
}
}
package setup package setup
import ( import (
"errors"
"io/fs" "io/fs"
"git.perx.ru/perxis/perxis-go" "github.com/pkg/errors"
"git.perx.ru/perxis/perxis-go/pkg/clients"
"git.perx.ru/perxis/perxis-go/pkg/collections"
"git.perx.ru/perxis/perxis-go/pkg/items"
"git.perx.ru/perxis/perxis-go/pkg/roles"
"github.com/hashicorp/go-multierror"
) )
type Config struct { type Config struct {
Roles []RoleConfig Collections Collections
Clients []ClientConfig Items Items
Collections []CollectionConfig Roles Roles
Items []ItemConfig Clients Clients
} }
func NewConfig() *Config { func NewConfig() *Config {
return &Config{} return &Config{}
} }
// Clone возвращает копию конфигурации
func (cfg *Config) Clone() *Config {
return &Config{
Collections: cfg.Collections.Clone(),
Items: cfg.Items.Clone(),
Roles: cfg.Roles.Clone(),
Clients: cfg.Clients.Clone(),
}
}
// Load загружает Config из файловой системы // Load загружает Config из файловой системы
// Файлы должны быть расположены в директории со следующей структурой: // Файлы должны быть расположены в директории со следующей структурой:
// - collections/ - директория с файлами конфигурации коллекций // - collections/ - директория с файлами конфигурации коллекций
...@@ -31,26 +34,30 @@ func NewConfig() *Config { ...@@ -31,26 +34,30 @@ func NewConfig() *Config {
// - items/ - директория с файлами конфигурации элементов // - items/ - директория с файлами конфигурации элементов
// - roles/ - директория с файлами конфигурации ролей // - roles/ - директория с файлами конфигурации ролей
func (cfg *Config) Load(fsys fs.FS) (*Config, error) { func (cfg *Config) Load(fsys fs.FS) (*Config, error) {
if _, err := fs.Stat(fsys, "."); err != nil {
return nil, errors.Wrapf(err, "Can't load config. (fs=%v)", fsys)
}
if subFS, err := fs.Sub(fsys, "collections"); err == nil { if subFS, err := fs.Sub(fsys, "collections"); err == nil {
if _, err = cfg.LoadCollections(subFS); err != nil && !errors.Is(err, fs.ErrNotExist) { if err = cfg.Collections.Load(subFS); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err return nil, err
} }
} }
if subFS, err := fs.Sub(fsys, "items"); err == nil { if subFS, err := fs.Sub(fsys, "items"); err == nil {
if _, err = cfg.LoadItems(subFS); err != nil && !errors.Is(err, fs.ErrNotExist) { if err = cfg.Items.Load(subFS, DecodeItem()); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err return nil, err
} }
} }
if subFS, err := fs.Sub(fsys, "roles"); err == nil { if subFS, err := fs.Sub(fsys, "roles"); err == nil {
if _, err = cfg.LoadRoles(subFS); err != nil && !errors.Is(err, fs.ErrNotExist) { if err = cfg.Roles.Load(subFS); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err return nil, err
} }
} }
if subFS, err := fs.Sub(fsys, "clients"); err == nil { if subFS, err := fs.Sub(fsys, "clients"); err == nil {
if _, err = cfg.LoadClients(subFS); err != nil && !errors.Is(err, fs.ErrNotExist) { if err = cfg.Clients.Load(subFS); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err return nil, err
} }
} }
...@@ -66,240 +73,210 @@ func (cfg *Config) MustLoad(fsys fs.FS) *Config { ...@@ -66,240 +73,210 @@ func (cfg *Config) MustLoad(fsys fs.FS) *Config {
return c return c
} }
func (cfg *Config) WithCollectionOptions(filter func(c *collections.Collection) bool, opts ...CollectionsOption) *Config {
for i, c := range cfg.Collections {
if filter(c.collection) {
for _, o := range opts {
o(&cfg.Collections[i])
}
}
}
return cfg
}
func (cfg *Config) WithItemsOptions(filter func(c *items.Item) bool, opts ...ItemsOption) *Config {
for i, c := range cfg.Items {
if filter(c.item) {
for _, o := range opts {
o(&cfg.Items[i])
}
}
}
return cfg
}
func (cfg *Config) WithRolesOptions(filter func(c *roles.Role) bool, opts ...RolesOption) *Config {
for i, r := range cfg.Roles {
if filter(r.role) {
for _, o := range opts {
o(&cfg.Roles[i])
}
}
}
return cfg
}
func (cfg *Config) WithClientsOptions(filter func(c *clients.Client) bool, opts ...ClientsOption) *Config {
for i, c := range cfg.Clients {
if filter(c.client) {
for _, o := range opts {
o(&cfg.Clients[i])
}
}
}
return cfg
}
// LoadItems загружает элементы из указанной файловой системы
func (cfg *Config) LoadClients(fsys fs.FS, opt ...ClientsOption) (*Config, error) {
assets := perxis.NewAssets[*clients.Client]()
cls, err := assets.FromFS(fsys)
if err != nil {
return nil, err
}
return cfg.AddClients(cls, opt...), nil
}
func (cfg *Config) MustLoadClients(fsys fs.FS, opt ...ClientsOption) *Config {
c, err := cfg.LoadClients(fsys, opt...)
if err != nil {
panic(err)
}
return c
}
// AddClients добавляет требования к настройке приложений в пространстве
func (cfg *Config) AddClients(clients []*clients.Client, opt ...ClientsOption) *Config {
for _, client := range clients {
cfg.AddClient(client, opt...)
}
return cfg
}
// AddClient добавляет требования к настройке приложений в пространстве
func (c *Config) AddClient(client *clients.Client, opt ...ClientsOption) *Config {
c.Clients = append(c.Clients, NewClientConfig(client, opt...))
return c
}
// LoadItems загружает элементы из указанной файловой системы
func (cfg *Config) LoadRoles(fsys fs.FS, opt ...RolesOption) (*Config, error) {
assets := perxis.NewAssets[*roles.Role]()
rls, err := assets.FromFS(fsys)
if err != nil {
return nil, err
}
return cfg.AddRoles(rls, opt...), nil
}
func (cfg *Config) MustLoadRoles(fsys fs.FS, opt ...RolesOption) *Config {
c, err := cfg.LoadRoles(fsys, opt...)
if err != nil {
panic(err)
}
return c
}
// AddRoles добавляет требования к настройке ролей в пространстве
func (cfg *Config) AddRoles(roles []*roles.Role, opt ...RolesOption) *Config {
for _, role := range roles {
cfg.AddRole(role, opt...)
}
return cfg
}
// AddRole добавляет требования к настройке ролей в пространстве
func (cfg *Config) AddRole(role *roles.Role, opt ...RolesOption) *Config {
cfg.Roles = append(cfg.Roles, NewRoleConfig(role, opt...))
return cfg
}
// AddCollections добавляет требования к настройке коллекций в пространстве
func (cfg *Config) AddCollections(collections []*collections.Collection, opt ...CollectionsOption) (*Config, error) {
var errs *multierror.Error
for _, col := range collections {
if _, err := cfg.AddCollection(col, opt...); err != nil {
errs = multierror.Append(errs, err)
}
}
return cfg, errs.ErrorOrNil()
}
// AddCollection добавляет требование к настройке коллекции в пространстве
func (cfg *Config) AddCollection(collection *collections.Collection, opt ...CollectionsOption) (*Config, error) {
config, err := NewCollectionConfig(collection, opt...)
if err != nil {
return nil, err
}
cfg.Collections = append(cfg.Collections, config)
return cfg, nil
}
// MustAddCollection добавляет требование к настройке коллекции в пространстве
func (cfg *Config) MustAddCollection(collection *collections.Collection, opt ...CollectionsOption) *Config {
config, err := NewCollectionConfig(collection, opt...)
if err != nil {
panic(err)
}
cfg.Collections = append(cfg.Collections, config)
return cfg
}
// LoadCollections загружает коллекции из указанной файловой системы
func (cfg *Config) LoadCollections(fsys fs.FS, opt ...CollectionsOption) (*Config, error) {
colls, err := collections.FromFS(fsys)
if err != nil {
return nil, err
}
return cfg.AddCollections(colls, opt...)
}
func (cfg *Config) MustLoadCollections(fsys fs.FS, opt ...CollectionsOption) *Config {
c, err := cfg.LoadCollections(fsys, opt...)
if err != nil {
panic(err)
}
return c
}
// GetCollection возвращает коллекцию по идентификатору
func (cfg *Config) GetCollection(id string) *collections.Collection {
for _, c := range cfg.Collections {
if c.collection.ID == id {
return c.collection
}
}
return nil
}
// GetCollection возвращает коллекцию по идентификатору
func (cfg *Config) GetAllCollections() []*collections.Collection {
res := make([]*collections.Collection, 0, len(cfg.Collections))
for _, c := range cfg.Collections {
res = append(res, c.collection)
}
return res
}
// GetCollectionConfig возвращает конфигурацию коллекции по идентификатору
func (cfg *Config) GetCollectionConfig(id string) *CollectionConfig {
for _, c := range cfg.Collections {
if c.collection.ID == id {
return &c
}
}
return nil
}
// LoadItems загружает элементы из указанной файловой системы // LoadItems загружает элементы из указанной файловой системы
func (cfg *Config) LoadItems(fsys fs.FS, opt ...ItemsOption) (*Config, error) { //func (cfg *Config) LoadClients(fsys fs.FS, opt ...ClientsOption) (*Config, error) {
assets := perxis.NewAssets[*items.Item]() // assets := perxis.NewAssets[*clients.Client]()
itms, err := assets.FromFS(fsys) // cls, err := assets.FromFS(fsys)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
return cfg.AddItems(itms, append(opt, DecodeItem())...), nil // return cfg.AddClients(cls, opt...), nil
} //}
//
func (cfg *Config) MustLoadItems(fsys fs.FS, opt ...ItemsOption) *Config { //func (cfg *Config) MustLoadClients(fsys fs.FS, opt ...ClientsOption) *Config {
cfg, err := cfg.LoadItems(fsys, opt...) // c, err := cfg.LoadClients(fsys, opt...)
if err != nil { // if err != nil {
panic(err) // panic(err)
} // }
return cfg // return c
} //}
//
// AddItems добавляет требования к настройке элементов в пространстве //// AddClients добавляет требования к настройке приложений в пространстве
func (cfg *Config) AddItems(items []*items.Item, opt ...ItemsOption) *Config { //func (cfg *Config) AddClients(clients []*clients.Client, opt ...ClientsOption) *Config {
for _, item := range items { // for _, client := range clients {
cfg.AddItem(item, opt...) // cfg.AddClient(client, opt...)
} // }
return cfg // return cfg
} //}
//
// AddItem добавляет требования к настройке элементов в пространстве //// AddClient добавляет требования к настройке приложений в пространстве
func (cfg *Config) AddItem(item *items.Item, opt ...ItemsOption) *Config { //func (c *Config) AddClient(client *clients.Client, opt ...ClientsOption) *Config {
cfg.Items = append(cfg.Items, NewItemConfig(item, opt...)) // c.Clients = append(c.Clients, NewClientConfig(client, opt...))
return cfg // return c
} //}
//
// GetItems возвращает элементы для указанной коллекции //// LoadItems загружает элементы из указанной файловой системы
func (cfg *Config) GetItems(collectionId string) []*items.Item { //func (cfg *Config) LoadRoles(fsys fs.FS, opt ...RolesOption) (*Config, error) {
var items []*items.Item // assets := perxis.NewAssets[*roles.Role]()
for _, i := range cfg.Items { // rls, err := assets.FromFS(fsys)
if i.item.CollectionID == collectionId { // if err != nil {
items = append(items, i.item) // return nil, err
} // }
} // return cfg.AddRoles(rls, opt...), nil
return items //}
} //
//func (cfg *Config) MustLoadRoles(fsys fs.FS, opt ...RolesOption) *Config {
// GetItem возвращает элемент для указанной коллекции и идентификатора // c, err := cfg.LoadRoles(fsys, opt...)
func (cfg *Config) GetItem(collectionId, itemId string) *items.Item { // if err != nil {
for _, i := range cfg.Items { // panic(err)
if i.item.CollectionID == collectionId && i.item.ID == itemId { // }
return i.item // return c
} //}
} //
return nil //// AddRoles добавляет требования к настройке ролей в пространстве
} //func (cfg *Config) AddRoles(roles []*roles.Role, opt ...RolesOption) *Config {
// for _, role := range roles {
// cfg.AddRole(role, opt...)
// }
// return cfg
//}
//
//// AddRole добавляет требования к настройке ролей в пространстве
//func (cfg *Config) AddRole(role *roles.Role, opt ...RolesOption) *Config {
// cfg.Roles = append(cfg.Roles, NewRoleConfig(role, opt...))
// return cfg
//}
//
//// AddCollections добавляет требования к настройке коллекций в пространстве
//func (cfg *Config) AddCollections(collections []*collections.Collection, opt ...CollectionsOption) (*Config, error) {
// var errs *multierror.Error
// for _, col := range collections {
// if _, err := cfg.AddCollection(col, opt...); err != nil {
// errs = multierror.Append(errs, err)
// }
// }
// return cfg, errs.ErrorOrNil()
//}
//
//// AddCollection добавляет требование к настройке коллекции в пространстве
//func (cfg *Config) AddCollection(collection *collections.Collection, opt ...EntityOption[*collections.Collection]) (*Config, error) {
// collection = collection.Clone()
//
// if collection.Schema != nil { // ??? Почему это здесь? Возможно, это должно быть снаружи в том месте, где создается коллекция и схема?
// // приведение внутренних типов схемы, чтобы избежать возможного несоответствия типов при
// // сравнивании схем (`[]interface{}/[]string`, `int/int64`, etc.)
// if err := collection.Schema.ConvertTypes(); err != nil {
// return nil, err
// }
// }
//
// e := NewEntity(collection, opt...)
// DefaultUpdateCollectionStrategy()(е)
// DeleteCollectionIfRemove()(е)
//
// cfg.Collections = append(cfg.Collections, e)
// return cfg, nil
//}
//
//// MustAddCollection добавляет требование к настройке коллекции в пространстве
//func (cfg *Config) MustAddCollection(collection *collections.Collection, opt ...CollectionsOption) *Config {
// config, err := NewCollectionConfig(collection, opt...)
// if err != nil {
// panic(err)
// }
// cfg.Collections = append(cfg.Collections, config)
// return cfg
//}
//
//// LoadCollections загружает коллекции из указанной файловой системы
//func (cfg *Config) LoadCollections(fsys fs.FS, opt ...CollectionsOption) (*Config, error) {
// colls, err := collections.FromFS(fsys)
// if err != nil {
// return nil, err
// }
// return cfg.AddCollections(colls, opt...)
//}
//
//func (cfg *Config) MustLoadCollections(fsys fs.FS, opt ...CollectionsOption) *Config {
// c, err := cfg.LoadCollections(fsys, opt...)
// if err != nil {
// panic(err)
// }
// return c
//}
//
//// GetCollection возвращает коллекцию по идентификатору
//func (cfg *Config) GetCollection(id string) *collections.Collection {
// for _, c := range cfg.Collections {
// if c.collection.ID == id {
// return c.collection
// }
// }
// return nil
//}
//
//func (cfg *Config) GetAllCollections() []*collections.Collection {
// res := make([]*collections.Collection, 0, len(cfg.Collections))
// for _, c := range cfg.Collections {
// res = append(res, c.collection)
// }
// return res
//}
//
//// GetCollectionConfig возвращает конфигурацию коллекции по идентификатору
//func (cfg *Config) GetCollectionConfig(id string) *CollectionConfig {
// for _, c := range cfg.Collections {
// if c.collection.ID == id {
// return &c
// }
// }
// return nil
//}
//
//// GetCollectionConfigList возвращает копию список конфигураций коллекций
//func (cfg *Config) GetCollectionConfigList() CollectionConfigList {
// return cfg.Collections.Clone()
//}
//
//// LoadItems загружает элементы из указанной файловой системы
//func (cfg *Config) LoadItems(fsys fs.FS, opt ...ItemsOption) (*Config, error) {
// assets := perxis.NewAssets[*items.Item]()
// itms, err := assets.FromFS(fsys)
// if err != nil {
// return nil, err
// }
// return cfg.AddItems(itms, append(opt, DecodeItem())...), nil
//}
//
//func (cfg *Config) MustLoadItems(fsys fs.FS, opt ...ItemsOption) *Config {
// cfg, err := cfg.LoadItems(fsys, opt...)
// if err != nil {
// panic(err)
// }
// return cfg
//}
//
//// AddItems добавляет требования к настройке элементов в пространстве
//func (cfg *Config) AddItems(items []*items.Item, opt ...ItemsOption) *Config {
// for _, item := range items {
// cfg.AddItem(item, opt...)
// }
// return cfg
//}
//
//// AddItem добавляет требования к настройке элементов в пространстве
//func (cfg *Config) AddItem(item *items.Item, opt ...ItemsOption) *Config {
// cfg.Items = append(cfg.Items, NewItemConfig(item, opt...))
// return cfg
//}
//
//// GetItems возвращает элементы для указанной коллекции
//func (cfg *Config) GetItems(collectionId string) []*items.Item {
// var items []*items.Item
// for _, i := range cfg.Items {
// if i.item.CollectionID == collectionId {
// items = append(items, i.item)
// }
// }
// return items
//}
//
//// GetItem возвращает элемент для указанной коллекции и идентификатора
//func (cfg *Config) GetItem(collectionId, itemId string) *items.Item {
// for _, i := range cfg.Items {
// if i.item.CollectionID == collectionId && i.item.ID == itemId {
// return i.item
// }
// }
// return nil
//}
package setup package setup
import ( import (
"os"
"testing" "testing"
"git.perx.ru/perxis/perxis-go/pkg/clients"
"git.perx.ru/perxis/perxis-go/pkg/collections"
"git.perx.ru/perxis/perxis-go/pkg/items"
"git.perx.ru/perxis/perxis-go/pkg/roles"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestConfig_WithCollectionOptions(t *testing.T) { // TODO
t.Run("With filter", func(t *testing.T) {
// создаем конфигурацию
config := &Config{Collections: []CollectionConfig{
{collection: &collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env"}},
{collection: &collections.Collection{ID: "2", SpaceID: "sp", EnvID: "env"}},
}}
// применяем опцию OverwriteCollection к конфигурации и дополнительно добавляем фильтр (опция применится только к коллекции с id 1) func TestConfig_Load(t *testing.T) {
config.WithCollectionOptions(func(c *collections.Collection) bool { return c.ID == "1" }, OverwriteCollection()) cfg := NewConfig()
assert.NotNil(t, config.Collections[0].UpdateFn, "должна быть выполнена OverwriteCollection и установлена UpdateFn для коллекции ID:1") cfg, err := cfg.Load(os.DirFS("../../assets/tests/setup"))
assert.Nil(t, config.Collections[1].UpdateFn) assert.NoError(t, err)
}) assert.Len(t, cfg.Collections, 3)
} assert.Equal(t, cfg.Collections[0].Value(nil).ID, "collection_a")
assert.Equal(t, cfg.Collections[1].Value(nil).ID, "collection_b")
func TestConfig_WithItemsOptions(t *testing.T) { assert.Equal(t, cfg.Collections[2].Value(nil).ID, "collection_c")
t.Run("With filter", func(t *testing.T) {
// создаем конфигурацию
config := &Config{Items: []ItemConfig{
{item: &items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll"}},
{item: &items.Item{ID: "2", SpaceID: "sp", EnvID: "env", CollectionID: "coll"}},
}}
// применяем опцию OverwriteItem к конфигурации и дополнительно добавляем фильтр (опция применится только к коллекции с id 1)
config.WithItemsOptions(func(c *items.Item) bool { return c.ID == "1" }, OverwriteItem())
assert.NotNil(t, config.Items[0].UpdateFn, "должна быть выполнена OverwriteItem и установлена UpdateFn для элемента ID: 1")
assert.Nil(t, config.Items[1].UpdateFn)
})
}
func TestConfig_WithRoleOptions(t *testing.T) {
t.Run("With filter", func(t *testing.T) {
// создаем конфигурацию
config := &Config{Roles: []RoleConfig{
{role: &roles.Role{ID: "1", SpaceID: "sp"}},
{role: &roles.Role{ID: "2", SpaceID: "sp"}},
}}
// применяем опцию OverwriteRole к конфигурации и дополнительно добавляем фильтр (опция применится только к коллекции с id 1)
config.WithRolesOptions(func(c *roles.Role) bool { return c.ID == "1" }, OverwriteRole())
assert.NotNil(t, config.Roles[0].UpdateFn, "должна быть выполнена опция OverwriteRole")
assert.Nil(t, config.Roles[1].UpdateFn)
})
}
func TestConfig_WithClientOptions(t *testing.T) {
t.Run("With filter", func(t *testing.T) {
// создаем конфигурацию
config := &Config{Clients: []ClientConfig{
{client: &clients.Client{ID: "1", SpaceID: "sp"}},
{client: &clients.Client{ID: "2", SpaceID: "sp"}},
}}
// применяем опцию OverwriteClient к конфигурации и дополнительно добавляем фильтр (опция применится только к коллекции с id 1)
config.WithClientsOptions(func(c *clients.Client) bool { return c.ID == "1" }, OverwriteClient())
assert.NotNil(t, config.Clients[0].UpdateFn, "должна выполнена опция OverwriteClient")
assert.Nil(t, config.Clients[1].UpdateFn)
})
} }
package setup
import (
"io/fs"
"git.perx.ru/perxis/perxis-go"
)
type Clonable[T any] interface {
GetID() string
Clone() T
}
type Conf[C any, T Clonable[T]] interface {
Init(e *Entity[C, T])
}
type ValueFunc[T Clonable[T]] func(s *Setup, e T)
type FilterFunc[T Clonable[T]] func(t T) bool
type UpdateFunc[T Clonable[T]] func(s *Setup, exist, new T) (T, bool)
type DeleteFunc[T Clonable[T]] func(s *Setup, e T) bool
type EntityOption[C any, T Clonable[T]] func(c *Entity[C, T])
type Entity[C any, T Clonable[T]] struct {
value T
ValueFunc []ValueFunc[T]
UpdateFunc UpdateFunc[T]
DeleteFunc DeleteFunc[T]
Conf C
Err error
}
func NewEntity[C any, T Clonable[T]](val T, opt ...EntityOption[C, T]) *Entity[C, T] {
e := &Entity[C, T]{value: val}
var conf any = e.Conf
if vv, ok := conf.(Conf[C, T]); ok {
vv.Init(e)
}
for _, o := range opt {
o(e)
}
return e
}
// Clone возвращает копию сущности
func (e *Entity[C, T]) Clone() *Entity[C, T] {
return &Entity[C, T]{
value: e.value.Clone(),
ValueFunc: e.ValueFunc,
UpdateFunc: e.UpdateFunc,
DeleteFunc: e.DeleteFunc,
Conf: e.Conf,
Err: e.Err,
}
}
// Value возвращает значение сущности
func (e *Entity[C, T]) Value(s *Setup) T {
ent := e.value.Clone() // TODO: Мы уже используем копию всей конфигурации, нужно ли это?
for _, fn := range e.ValueFunc {
fn(s, ent)
}
return ent
}
type EntityList[C any, T Clonable[T]] []*Entity[C, T]
// Clone возвращает копию списка сущностей
func (l *EntityList[C, T]) Clone() EntityList[C, T] {
var res EntityList[C, T]
for _, e := range *l {
res = append(res, e.Clone())
}
return res
}
// Add добавляет сущности в список EntityList
func (l *EntityList[C, T]) Add(t T, opt ...EntityOption[C, T]) {
e := NewEntity(t.Clone(), opt...)
*l = append(*l, e)
}
// AddMany добавляет несколько сущностей в список EntityList
func (l *EntityList[C, T]) AddMany(t []T, opt ...EntityOption[C, T]) {
for _, item := range t {
l.Add(item, opt...)
}
}
// Filter возвращает список сущностей, удовлетворяющих фильтру
func (l *EntityList[C, T]) Filter(filter FilterFunc[T]) *EntityList[C, T] {
res := make(EntityList[C, T], 0)
for _, e := range *l {
if filter(e.value) {
res = append(res, e)
}
}
return &res
}
// Get возвращает конфигурацию по ID
func (l *EntityList[C, T]) Get(id string) *Entity[C, T] {
for _, e := range *l {
if e.value.GetID() == id {
return e
}
}
return nil
}
// GetIDs возвращает список ID сущностей
func (l *EntityList[C, T]) GetIDs() []string {
var res []string
for _, e := range *l {
res = append(res, e.value.GetID())
}
return res
}
// Load загружает сущности в список EntityList из указанной файловой системы
func (l *EntityList[C, T]) Load(fsys fs.FS, opt ...EntityOption[C, T]) error {
assets := perxis.NewAssets[T]()
items, err := assets.FromFS(fsys)
if err != nil {
return err
}
for _, item := range items {
l.Add(item, opt...)
}
return nil
}
// MustLoad загружает сущности в список EntityList из указанной файловой системы
func (l *EntityList[C, T]) MustLoad(fsys fs.FS, opt ...EntityOption[C, T]) {
if err := l.Load(fsys, opt...); err != nil {
panic(err)
}
}
// WithOptions добавляет опции к сущности в списке EntityList согласно фильтру
func (l *EntityList[C, T]) WithOptions(filter FilterFunc[T], opt ...EntityOption[C, T]) {
for _, e := range *l {
if filter(e.value) {
for _, o := range opt {
o(e)
}
}
}
}
// Overwrite перезаписывает сущность при обновлении
func Overwrite[C any, T Clonable[T]]() EntityOption[C, T] {
return func(c *Entity[C, T]) {
c.UpdateFunc = func(s *Setup, old, new T) (T, bool) { return new, true }
}
}
// Keep сохраняет сущность при обновлении
func Keep[C any, T Clonable[T]]() EntityOption[C, T] {
return func(c *Entity[C, T]) {
c.UpdateFunc = func(s *Setup, old, new T) (T, bool) { return old, false }
}
}
// Delete удаляет сущность
func Delete[C any, T Clonable[T]]() EntityOption[C, T] {
return func(c *Entity[C, T]) {
c.DeleteFunc = func(s *Setup, e T) bool { return true }
}
}
// DeleteIfRemoveFlag удаляет сущность если флаг Setup Remove установлен
func DeleteIfRemoveFlag[C any, T Clonable[T]]() EntityOption[C, T] {
return func(c *Entity[C, T]) {
c.DeleteFunc = func(s *Setup, e T) bool { return s.IsRemove() }
}
}
package setup
import (
"testing"
"git.perx.ru/perxis/perxis-go/pkg/collections"
"github.com/stretchr/testify/assert"
)
func TestEntityList_WithOptions(t *testing.T) {
colls := make(Collections, 0)
colls.Add(&collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env"})
colls.Add(&collections.Collection{ID: "2", SpaceID: "sp", EnvID: "env"})
colls.Add(&collections.Collection{ID: "3", SpaceID: "sp", EnvID: "env"})
colls.Add(&collections.Collection{ID: "4", SpaceID: "sp", EnvID: "env"})
colls.WithOptions(func(c *collections.Collection) bool { return c.ID == "1" || c.ID == "3" }, func(c *Collection) {
c.UpdateFunc = nil
})
assert.Nil(t, colls[0].UpdateFunc)
assert.NotNil(t, colls[1].UpdateFunc)
assert.Nil(t, colls[2].UpdateFunc)
assert.NotNil(t, colls[3].UpdateFunc)
}
func TestEntityList_Add(t *testing.T) {
c := &collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env"}
colls := make(Collections, 0)
colls.Add(c)
colls.Add(c)
assert.Equal(t, colls[0].value, c)
assert.Equal(t, colls[1].value, c)
assert.True(t, colls[0].value != c)
assert.True(t, colls[1].value != c)
assert.True(t, colls[0].value != colls[1].value)
}
func TestEntityList_Get(t *testing.T) {
c := &collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env"}
colls := make(Collections, 0)
colls.Add(c)
c1 := colls[0].Value(nil)
assert.Equal(t, c, c1)
assert.Equal(t, colls[0].value, c1)
assert.True(t, c != c1)
assert.True(t, colls[0].value != c1)
}
...@@ -5,10 +5,11 @@ import ( ...@@ -5,10 +5,11 @@ import (
"reflect" "reflect"
"strings" "strings"
"go.uber.org/zap"
"git.perx.ru/perxis/perxis-go/pkg/collections" "git.perx.ru/perxis/perxis-go/pkg/collections"
"git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/errors"
"git.perx.ru/perxis/perxis-go/pkg/items" "git.perx.ru/perxis/perxis-go/pkg/items"
"go.uber.org/zap"
) )
var ( var (
...@@ -18,59 +19,60 @@ var ( ...@@ -18,59 +19,60 @@ var (
ErrItemsNotFound = errors.New("item not found") ErrItemsNotFound = errors.New("item not found")
) )
type ItemsOption func(c *ItemConfig) type (
type PublishItemFn func(s *Setup, item *items.Item) (*items.Item, bool) Item = Entity[ItemConf, *items.Item]
type UpdateItemFn func(s *Setup, exist, new *items.Item) (*items.Item, bool) Items = EntityList[ItemConf, *items.Item]
type DeleteItemFn func(s *Setup, col *items.Item) bool ItemOption = EntityOption[ItemConf, *items.Item]
type ItemConfig struct { ItemConf struct {
item *items.Item PublishFunc func(s *Setup, item *items.Item) (*items.Item, bool)
PublishFn PublishItemFn encoded bool // Если запись загружена из файла, необходимо выполнить Decode перед установкой
UpdateFn UpdateItemFn
DeleteFn DeleteItemFn
// Если запись загружена из файла, необходимо выполнить Decode перед установкой
encoded bool
} }
)
func NewItemConfig(item *items.Item, opt ...ItemsOption) ItemConfig { var (
c := ItemConfig{item: item} NewItem = NewEntity[ItemConf, *items.Item]
OverwriteItem = Overwrite[ItemConf, *items.Item]
PublishItem()(&c) KeepExistingItem = Keep[ItemConf, *items.Item]
KeepExistingItem()(&c) DeleteItem = Delete[ItemConf, *items.Item]
DeleteItemIfRemove()(&c) DeleteItemIfRemoveFlag = DeleteIfRemoveFlag[ItemConf, *items.Item]
)
for _, o := range opt {
o(&c)
}
return c func (ItemConf) Init(e *Entity[ItemConf, *items.Item]) {
PublishItem()(e)
KeepExistingItem()(e)
DeleteItemIfRemoveFlag()(e)
} }
func PrepareItems(handler func(*items.Item)) ItemsOption { // OverwriteItem перезаписывает элемент
return func(c *ItemConfig) { func PublishItem() ItemOption {
handler(c.item) return func(c *Item) {
c.Conf.PublishFunc = func(s *Setup, item *items.Item) (*items.Item, bool) { return item, true }
} }
} }
func OverwriteItem() ItemsOption { // DraftItem не публикует элемент, сохраняет его в черновике
return func(c *ItemConfig) { func DraftItem() ItemOption {
c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) { return new, true } return func(c *Item) {
c.Conf.PublishFunc = func(s *Setup, item *items.Item) (*items.Item, bool) { return item, false }
} }
} }
func DecodeItem() ItemsOption { // DecodeItem декодирует элемент перед установкой
return func(c *ItemConfig) { func DecodeItem() ItemOption {
c.encoded = true return func(c *Item) {
c.Conf.encoded = true
} }
} }
func OverwriteFields(fields ...string) ItemsOption { // OverwriteFields разрешает перезаписывать указанные поля элемента, в противном случае считается что элемент не изменился
return func(c *ItemConfig) { func OverwriteFields(fields ...string) ItemOption {
c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) { return func(c *Item) {
c.UpdateFunc = func(s *Setup, old, new *items.Item) (*items.Item, bool) {
var changed bool var changed bool
for _, field := range fields { for _, field := range fields {
// Пропускаем системные поля
if items.IsSystemField(field) { if items.IsSystemField(field) {
continue continue
} }
...@@ -79,8 +81,8 @@ func OverwriteFields(fields ...string) ItemsOption { ...@@ -79,8 +81,8 @@ func OverwriteFields(fields ...string) ItemsOption {
if err != nil { if err != nil {
continue continue
} }
oldValue, err := old.Get(field) oldValue, err := old.Get(field)
if err != nil || newValue != oldValue { if err != nil || newValue != oldValue {
changed = true changed = true
if err = old.Set(field, newValue); err != nil { if err = old.Set(field, newValue); err != nil {
...@@ -94,9 +96,10 @@ func OverwriteFields(fields ...string) ItemsOption { ...@@ -94,9 +96,10 @@ func OverwriteFields(fields ...string) ItemsOption {
} }
} }
func KeepFields(fields ...string) ItemsOption { // KeepFields сохраняет указанные поля элемента
return func(c *ItemConfig) { func KeepFields(fields ...string) ItemOption {
c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) { return func(c *Item) {
c.UpdateFunc = func(s *Setup, old, new *items.Item) (*items.Item, bool) {
for _, field := range fields { for _, field := range fields {
if items.IsSystemField(field) { if items.IsSystemField(field) {
...@@ -121,50 +124,74 @@ func KeepFields(fields ...string) ItemsOption { ...@@ -121,50 +124,74 @@ func KeepFields(fields ...string) ItemsOption {
} }
} }
func KeepExistingItem() ItemsOption { //
return func(c *ItemConfig) { // Item Setup
c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) { return old, false } //
// AddItem добавляет требования к настройке элементов в пространстве
func (s *Setup) AddItem(item *items.Item, opt ...ItemOption) *Setup {
s.Config.Items.Add(item, opt...)
return s
} }
// AddItems добавляет требования к настройке элементов в пространстве
func (s *Setup) AddItems(items []*items.Item, opt ...ItemOption) *Setup {
s.Config.Items.AddMany(items, opt...)
return s
} }
func DeleteItem() ItemsOption { // InstallItem настраивает элемент
return func(c *ItemConfig) { func (s *Setup) InstallItem(ctx context.Context, exists map[string]*items.Item, c *Item) error {
c.DeleteFn = func(s *Setup, item *items.Item) bool { return true } item := c.Value(s)
item.SpaceID, item.EnvID = s.SpaceID, s.EnvironmentID
exist, itemExists := exists[item.ID]
// Если элемент не существует, создаем его
if !itemExists {
if item, publish := c.Conf.PublishFunc(s, item); publish {
return items.CreateAndPublishItem(ctx, s.content.Items, item)
} }
if _, err := s.content.Items.Create(ctx, item); err != nil {
return errors.Wrap(err, "create item")
}
return nil
} }
func DeleteItemIfRemove() ItemsOption { // Если элемент существует, обновляем его
return func(c *ItemConfig) { if item, changed := c.UpdateFunc(s, exist, item); changed {
c.DeleteFn = func(s *Setup, item *items.Item) bool { return s.IsRemove() } if _, publish := c.Conf.PublishFunc(s, item); publish {
return items.UpdateAndPublishItem(ctx, s.content.Items, item)
} }
if err := s.content.Items.Update(ctx, item); err != nil {
return errors.Wrap(err, "update item")
} }
if err := s.content.Items.Unpublish(ctx, item); err != nil {
func PublishItem() ItemsOption { return errors.Wrap(err, "unpublish item")
return func(c *ItemConfig) {
c.PublishFn = func(s *Setup, item *items.Item) (*items.Item, bool) { return item, true }
} }
return nil
} }
func KeepDraft() ItemsOption { return nil
return func(c *ItemConfig) {
c.PublishFn = func(s *Setup, item *items.Item) (*items.Item, bool) { return item, false }
}
} }
// InstallItems устанавливает все элементы
func (s *Setup) InstallItems(ctx context.Context) (err error) { func (s *Setup) InstallItems(ctx context.Context) (err error) {
if len(s.Items) == 0 { if len(s.Items) == 0 {
return nil return nil
} }
s.logger.Debug("Install items", zap.Int("Items", len(s.Items))) s.logger.Debug("Installing items", zap.Int("Items", len(s.Items)))
for collID, itms := range s.groupByCollection() { for collID, itms := range groupByCollection(s.Items) {
var coll *collections.Collection var coll *collections.Collection
for i, c := range itms { for i, c := range itms {
if !c.encoded { // Пропускаем элементы, которые не требуют декодирования
if !c.Conf.encoded {
continue continue
} }
// Получаем коллекцию и схему для декодирования элемента
if coll == nil { if coll == nil {
coll, err = s.content.Collections.Get(ctx, s.SpaceID, s.EnvironmentID, collID) coll, err = s.content.Collections.Get(ctx, s.SpaceID, s.EnvironmentID, collID)
if err != nil { if err != nil {
...@@ -172,31 +199,29 @@ func (s *Setup) InstallItems(ctx context.Context) (err error) { ...@@ -172,31 +199,29 @@ func (s *Setup) InstallItems(ctx context.Context) (err error) {
} }
} }
decoded, err := c.item.Decode(ctx, coll.Schema) // Декодируем элемент
decoded, err := c.value.Decode(ctx, coll.Schema)
if err != nil { if err != nil {
return err return err
} }
itms[i] = ItemConfig{
item: decoded, itms[i].value = decoded
PublishFn: c.PublishFn,
UpdateFn: c.UpdateFn,
DeleteFn: c.DeleteFn,
encoded: false,
}
} }
exists, err := s.getExisting(ctx, collID, itms) // Получаем существующие элементы
exists, err := s.getItems(ctx, collID, itms)
if err != nil { if err != nil {
return err return err
} }
// Устанавливаем элементы
for _, c := range itms { for _, c := range itms {
if err := s.InstallItem(ctx, exists, c); err != nil { if err := s.InstallItem(ctx, exists, c); err != nil {
s.logger.Error("Failed to install item", item := c.Value(s)
zap.String("ID", c.item.ID), s.logger.Error("Failed to install item", zap.String("ID", item.ID),
zap.String("Collection", c.item.CollectionID), zap.String("Collection", item.CollectionID), zap.Error(err))
zap.Error(err), return errors.WithDetailf(errors.Wrap(err, "failed to install item"),
) "Возникла ошибка при добавлении элемента %s(%s)", item.ID, item.CollectionID)
return errors.WithDetailf(errors.Wrap(err, "failed to install item"), "Возникла ошибка при добавлении элемента %s(%s)", c.item.ID, c.item.CollectionID)
} }
} }
} }
...@@ -204,35 +229,15 @@ func (s *Setup) InstallItems(ctx context.Context) (err error) { ...@@ -204,35 +229,15 @@ func (s *Setup) InstallItems(ctx context.Context) (err error) {
return nil return nil
} }
func (s *Setup) InstallItem(ctx context.Context, exists map[string]*items.Item, c ItemConfig) error { // UninstallItem удаляет элемент
item := c.item func (s *Setup) UninstallItem(ctx context.Context, c *Item) error {
item.SpaceID, item.EnvID = s.SpaceID, s.EnvironmentID item := c.Value(s)
if c.DeleteFunc(s, item) {
exist, ok := exists[item.ID] err := s.content.Items.Delete(ctx, &items.Item{SpaceID: s.SpaceID, EnvID: s.EnvironmentID, CollectionID: item.CollectionID, ID: item.ID})
if !ok { if err != nil && !strings.Contains(err.Error(), items.ErrNotFound.Error()) {
if item, publish := c.PublishFn(s, item); publish { return err
return items.CreateAndPublishItem(ctx, s.content.Items, item)
}
if _, err := s.content.Items.Create(ctx, item); err != nil {
return errors.Wrap(err, "create item")
}
return nil
}
if item, changed := c.UpdateFn(s, exist, item); changed {
if _, publish := c.PublishFn(s, item); publish {
return items.UpdateAndPublishItem(ctx, s.content.Items, item)
}
if err := s.content.Items.Update(ctx, item); err != nil {
return errors.Wrap(err, "update item")
}
if err := s.content.Items.Unpublish(ctx, item); err != nil {
return errors.Wrap(err, "unpublish item")
} }
return nil
} }
return nil return nil
} }
...@@ -245,25 +250,15 @@ func (s *Setup) UninstallItems(ctx context.Context) error { ...@@ -245,25 +250,15 @@ func (s *Setup) UninstallItems(ctx context.Context) error {
for _, c := range s.Items { for _, c := range s.Items {
if err := s.UninstallItem(ctx, c); err != nil { if err := s.UninstallItem(ctx, c); err != nil {
s.logger.Error("Failed to uninstall item", item := c.Value(s)
zap.String("Item", c.item.ID), s.logger.Error("Failed to uninstall item", zap.String("Item", item.ID),
zap.String("Item", c.item.CollectionID), zap.String("Item", item.CollectionID), zap.Error(err),
zap.Error(err),
) )
return errors.WithDetailf(errors.Wrap(err, "failed to uninstall item"), "Возникла ошибка при удалении элемента %s(%s)", c.item.ID, c.item.CollectionID) return errors.WithDetailf(errors.Wrap(err, "failed to uninstall item"),
} "Возникла ошибка при удалении элемента %s(%s)", item.ID, item.CollectionID)
} }
return nil
} }
func (s *Setup) UninstallItem(ctx context.Context, c ItemConfig) error {
if c.DeleteFn(s, c.item) {
err := s.content.Items.Delete(ctx, &items.Item{SpaceID: s.SpaceID, EnvID: s.EnvironmentID, CollectionID: c.item.CollectionID, ID: c.item.ID})
if err != nil && !strings.Contains(err.Error(), items.ErrNotFound.Error()) {
return err
}
}
return nil return nil
} }
...@@ -275,15 +270,17 @@ func (s *Setup) CheckItems(ctx context.Context) error { ...@@ -275,15 +270,17 @@ func (s *Setup) CheckItems(ctx context.Context) error {
var errs []error var errs []error
s.logger.Debug("Check items", zap.Int("Items", len(s.Items))) s.logger.Debug("Check items", zap.Int("Items", len(s.Items)))
for col, itms := range s.groupByCollection() { for col, itms := range groupByCollection(s.Items) {
exists, err := s.getExisting(ctx, col, itms) exists, err := s.getItems(ctx, col, itms)
if err != nil { if err != nil {
return err return err
} }
for _, c := range itms { for _, c := range itms {
if _, ok := exists[c.item.ID]; !ok { item := c.Value(s)
errs = append(errs, errors.WithDetailf(errors.New("not found"), "Не найден элемент %s(%s)", c.item.ID, c.item.CollectionID)) if _, ok := exists[item.ID]; !ok {
errs = append(errs, errors.WithDetailf(errors.New("not found"),
"Не найден элемент %s(%s)", item.ID, item.CollectionID))
} }
} }
} }
...@@ -295,44 +292,44 @@ func (s *Setup) CheckItems(ctx context.Context) error { ...@@ -295,44 +292,44 @@ func (s *Setup) CheckItems(ctx context.Context) error {
return nil return nil
} }
func (s *Setup) removeItems(collID string) { //
itms := make([]ItemConfig, 0, len(s.Items)) //func (s *Setup) removeItems(collID string) {
for _, i := range s.Items { // itms := make([]ItemConfig, 0, len(s.Items))
if i.item.CollectionID != collID { // for _, i := range s.Items {
itms = append(itms, i) // if i.item.CollectionID != collID {
} // itms = append(itms, i)
} // }
s.Items = itms // }
} // s.Items = itms
//}
func (s *Setup) groupByCollection() map[string][]ItemConfig {
itemsByColl := map[string][]ItemConfig{} func groupByCollection(itms Items) map[string]Items {
for _, i := range s.Items { res := map[string]Items{}
cfg, ok := itemsByColl[i.item.CollectionID] for _, conf := range itms {
collectionID := conf.value.CollectionID
l, ok := res[collectionID]
if !ok { if !ok {
itemsByColl[i.item.CollectionID] = []ItemConfig{i} res[collectionID] = make(Items, 0, 1)
continue
} }
itemsByColl[i.item.CollectionID] = append(cfg, i) res[collectionID] = append(l, conf)
} }
return itemsByColl return res
} }
func (s *Setup) getExisting(ctx context.Context, collID string, configs []ItemConfig) (map[string]*items.Item, error) { func (s *Setup) getItems(ctx context.Context, collID string, confs Items) (map[string]*items.Item, error) {
itms, _, err := s.content.Items.Find( itms, _, err := s.content.Items.Find(
ctx, ctx,
s.SpaceID, s.SpaceID,
s.EnvironmentID, s.EnvironmentID,
collID, collID,
&items.Filter{ID: getItemIds(configs)}, &items.Filter{ID: confs.GetIDs()},
&items.FindOptions{Regular: true, Hidden: true, Templates: true}, &items.FindOptions{Regular: true, Hidden: true, Templates: true},
) )
if err != nil { if err != nil {
s.logger.Error("Failed to find existing items", s.logger.Error("Failed to find existing items", zap.String("Collection", collID), zap.Error(err))
zap.String("Collection", collID), return nil, errors.WithDetailf(errors.Wrap(err, "failed to find existing items"),
zap.Error(err), "Возникла ошибка при получении элементов в коллекции %s", collID)
)
return nil, errors.WithDetailf(errors.Wrap(err, "failed to find existing items"), "Возникла ошибка при поиске элементов в коллекции %s", collID)
} }
exists := make(map[string]*items.Item, len(itms)) exists := make(map[string]*items.Item, len(itms))
...@@ -341,11 +338,3 @@ func (s *Setup) getExisting(ctx context.Context, collID string, configs []ItemCo ...@@ -341,11 +338,3 @@ func (s *Setup) getExisting(ctx context.Context, collID string, configs []ItemCo
} }
return exists, nil return exists, nil
} }
func getItemIds(items []ItemConfig) []string {
var ids []string
for _, i := range items {
ids = append(ids, i.item.ID)
}
return ids
}
...@@ -6,10 +6,11 @@ import ( ...@@ -6,10 +6,11 @@ import (
"git.perx.ru/perxis/perxis-go/pkg/content" "git.perx.ru/perxis/perxis-go/pkg/content"
"git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/errors"
"git.perx.ru/perxis/perxis-go/pkg/items"
itemsMock "git.perx.ru/perxis/perxis-go/pkg/items/mocks" itemsMock "git.perx.ru/perxis/perxis-go/pkg/items/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"git.perx.ru/perxis/perxis-go/pkg/items"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
...@@ -82,17 +83,16 @@ func TestItem_OverwriteFields(t *testing.T) { ...@@ -82,17 +83,16 @@ func TestItem_OverwriteFields(t *testing.T) {
changed: false, changed: false,
}, },
} }
for _, test := range tests { for _, tt := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := ItemConfig{item: test.new} c := NewItem(tt.new)
OverwriteFields(test.fields...)(&c) OverwriteFields(tt.fields...)(c)
got, changed := c.UpdateFunc(nil, tt.old, tt.new)
got, changed := c.UpdateFn(nil, test.old, test.new) require.Equal(t, tt.changed, changed)
require.Equal(t, test.changed, changed) if !tt.changed {
if !test.changed {
return return
} }
assert.Equal(t, test.want, got) assert.Equal(t, tt.want, got)
}) })
} }
...@@ -172,24 +172,22 @@ func TestItem_KeepFields(t *testing.T) { ...@@ -172,24 +172,22 @@ func TestItem_KeepFields(t *testing.T) {
changed: true, changed: true,
}, },
} }
for _, test := range tests { for _, tt := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := ItemConfig{item: test.new} c := NewItem(tt.new)
KeepFields(test.fields...)(&c) KeepFields(tt.fields...)(c)
got, changed := c.UpdateFunc(nil, tt.old, tt.new)
got, changed := c.UpdateFn(nil, test.old, test.new) require.Equal(t, tt.changed, changed)
require.Equal(t, test.changed, changed) if !tt.changed {
if !test.changed {
return return
} }
assert.Equal(t, test.want, got) assert.Equal(t, tt.want, got)
}) })
} }
} }
func TestSetup_InstallItems(t *testing.T) { func TestSetup_InstallItems(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
items []*items.Item items []*items.Item
...@@ -279,7 +277,6 @@ func TestSetup_InstallItems(t *testing.T) { ...@@ -279,7 +277,6 @@ func TestSetup_InstallItems(t *testing.T) {
} }
func TestSetup_CreateDraft(t *testing.T) { func TestSetup_CreateDraft(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
items []*items.Item items []*items.Item
...@@ -322,14 +319,13 @@ func TestSetup_CreateDraft(t *testing.T) { ...@@ -322,14 +319,13 @@ func TestSetup_CreateDraft(t *testing.T) {
} }
s := NewSetup(&content.Content{Items: i}, "sp", "env", nil) s := NewSetup(&content.Content{Items: i}, "sp", "env", nil)
s.AddItems(tt.items, KeepDraft()) s.AddItems(tt.items, DraftItem())
tt.wantErr(t, s.InstallItems(context.Background())) tt.wantErr(t, s.InstallItems(context.Background()))
}) })
} }
} }
func TestSetup_UpdateDraft(t *testing.T) { func TestSetup_UpdateDraft(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
items []*items.Item items []*items.Item
...@@ -358,7 +354,7 @@ func TestSetup_UpdateDraft(t *testing.T) { ...@@ -358,7 +354,7 @@ func TestSetup_UpdateDraft(t *testing.T) {
} }
s := NewSetup(&content.Content{Items: i}, "sp", "env", nil) s := NewSetup(&content.Content{Items: i}, "sp", "env", nil)
s.AddItems(tt.items, KeepDraft()) s.AddItems(tt.items, DraftItem())
tt.wantErr(t, s.InstallItems(context.Background())) tt.wantErr(t, s.InstallItems(context.Background()))
}) })
} }
......
...@@ -4,11 +4,12 @@ import ( ...@@ -4,11 +4,12 @@ import (
"context" "context"
"strings" "strings"
"go.uber.org/zap"
"git.perx.ru/perxis/perxis-go/pkg/data" "git.perx.ru/perxis/perxis-go/pkg/data"
"git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/errors"
"git.perx.ru/perxis/perxis-go/pkg/permission" "git.perx.ru/perxis/perxis-go/pkg/permission"
"git.perx.ru/perxis/perxis-go/pkg/roles" "git.perx.ru/perxis/perxis-go/pkg/roles"
"go.uber.org/zap"
) )
var ( var (
...@@ -17,67 +18,28 @@ var ( ...@@ -17,67 +18,28 @@ var (
ErrUninstallRoles = errors.New("failed to uninstall role") ErrUninstallRoles = errors.New("failed to uninstall role")
) )
type RolesOption func(c *RoleConfig) type Role = Entity[RoleConf, *roles.Role]
type UpdateRoleFn func(s *Setup, exist, new *roles.Role) (*roles.Role, bool) type Roles = EntityList[RoleConf, *roles.Role]
type DeleteRoleFn func(s *Setup, role *roles.Role) bool type RoleOption = EntityOption[RoleConf, *roles.Role]
type RoleConf struct{}
type RoleConfig struct {
role *roles.Role
UpdateFn UpdateRoleFn
DeleteFn DeleteRoleFn
}
func NewRoleConfig(role *roles.Role, opt ...RolesOption) RoleConfig {
c := RoleConfig{role: role}
UpdateExistingRole()(&c)
DeleteRoleIfRemove()(&c)
for _, o := range opt {
o(&c)
}
return c
}
func OverwriteRole() RolesOption {
return func(c *RoleConfig) {
c.UpdateFn = func(s *Setup, old, new *roles.Role) (*roles.Role, bool) { return new, true }
}
}
//func OverwriteRoleIfChanged() RolesOption {
// return func(c *RoleConfig) {
// c.UpdateFn = func(s *Setup, old, new *roles.Role) (*roles.Role, bool) {
// changed := old.Description != new.Description || old.AllowManagement != new.AllowManagement ||
// !util.ElementsMatch(old.Environments, new.Environments)
// return new, changed
// }
// }
//}
func KeepExistingRole() RolesOption { // Init инициализирует конфигурацию коллекции
return func(c *RoleConfig) { func (RoleConf) Init(e *Entity[RoleConf, *roles.Role]) {
c.UpdateFn = func(s *Setup, old, new *roles.Role) (*roles.Role, bool) { return old, false } DefaultRoleUpdateFunc()(e)
} DeleteRoleIfRemoveFlag()(e)
} }
func DeleteRole() RolesOption { var (
return func(c *RoleConfig) { NewRole = NewEntity[RoleConf, *roles.Role]
c.DeleteFn = func(s *Setup, role *roles.Role) bool { return true } OverwriteRole = Overwrite[RoleConf, *roles.Role]
} KeepExistingRole = Keep[RoleConf, *roles.Role]
} DeleteRole = Delete[RoleConf, *roles.Role]
DeleteRoleIfRemoveFlag = DeleteIfRemoveFlag[RoleConf, *roles.Role]
func DeleteRoleIfRemove() RolesOption { )
return func(c *RoleConfig) {
c.DeleteFn = func(s *Setup, role *roles.Role) bool { return s.IsRemove() }
}
}
func UpdateExistingRole() RolesOption {
return func(c *RoleConfig) {
c.UpdateFn = func(s *Setup, exist, new *roles.Role) (*roles.Role, bool) {
// если передан флаг force, то обновляем все поля роли на переданные func DefaultRoleUpdateFunc() RoleOption {
return func(c *Role) {
c.UpdateFunc = func(s *Setup, exist, new *roles.Role) (*roles.Role, bool) {
if s.IsForce() { if s.IsForce() {
return new, true return new, true
} }
...@@ -105,68 +67,74 @@ func UpdateExistingRole() RolesOption { ...@@ -105,68 +67,74 @@ func UpdateExistingRole() RolesOption {
} }
} }
func (s *Setup) InstallRoles(ctx context.Context) error { //
if len(s.Roles) == 0 { // Role Setup
return nil //
}
s.logger.Debug("Install role", zap.String("Space ID", s.SpaceID), zap.Int("Roles", len(s.Roles)))
for _, c := range s.Roles { // AddRoles добавляет требования к настройке ролей в пространстве
if err := s.InstallRole(ctx, c); err != nil { func (s *Setup) AddRoles(roles []*roles.Role, opt ...RoleOption) *Setup {
s.logger.Error("Failed to install role", zap.String("Role ID", c.role.ID), zap.Error(err)) s.Config.Roles.AddMany(roles, opt...)
return errors.Wrap(errors.WithDetailf(err, "Возникла ошибка при настройке роли %s(%s)", c.role.ID, c.role.Description), "failed to install role") return s
}
} }
return nil // AddRole добавляет требования к настройке элементов в пространстве
func (s *Setup) AddRole(role *roles.Role, opt ...RoleOption) *Setup {
s.Config.Roles.Add(role, opt...)
return s
} }
func (s *Setup) InstallRole(ctx context.Context, c RoleConfig) error { // InstallRole устанавливает роль
role := c.role func (s *Setup) InstallRole(ctx context.Context, c *Role) error {
role := c.Value(s)
role.SpaceID = s.SpaceID role.SpaceID = s.SpaceID
if !data.Contains(s.EnvironmentID, c.role.Environments) { if !data.Contains(s.EnvironmentID, role.Environments) {
role.Environments = append(role.Environments, s.EnvironmentID) role.Environments = append(role.Environments, s.EnvironmentID)
} }
exist, err := s.content.Roles.Get(ctx, s.SpaceID, role.ID) exist, err := s.content.Roles.Get(ctx, s.SpaceID, role.ID)
// Если роль не найдена, создаем новую
if err != nil { if err != nil {
if !strings.Contains(err.Error(), roles.ErrNotFound.Error()) { if !strings.Contains(err.Error(), roles.ErrNotFound.Error()) {
return err return err
} }
_, err = s.content.Roles.Create(ctx, role) _, err = s.content.Roles.Create(ctx, role)
return err return err
} }
if r, upd := c.UpdateFn(s, exist, role); upd { // Если роль найдена, обновляем ее
if r, needUpdate := c.UpdateFunc(s, exist, role); needUpdate {
return s.content.Roles.Update(ctx, r) return s.content.Roles.Update(ctx, r)
} }
return nil return nil
} }
func (s *Setup) UninstallRoles(ctx context.Context) error { // InstallRoles устанавливает все роли
func (s *Setup) InstallRoles(ctx context.Context) error {
if len(s.Roles) == 0 { if len(s.Roles) == 0 {
return nil return nil
} }
s.logger.Debug("Uninstall role", zap.String("Space ID", s.SpaceID), zap.Int("Roles", len(s.Roles))) s.logger.Debug("Install roles", zap.String("Space ID", s.SpaceID), zap.Int("Roles", len(s.Roles)))
for _, c := range s.Roles { for _, c := range s.Roles {
if err := s.UninstallRole(ctx, c); err != nil { if err := s.InstallRole(ctx, c); err != nil {
s.logger.Error("Failed to uninstall role", zap.String("Role ID", c.role.ID), zap.Error(err)) role := c.Value(s)
return errors.WithDetailf(errors.Wrap(err, "failed to uninstall role"), "Возникла ошибка при удалении роли %s(%s)", c.role.ID, c.role.Description) s.logger.Error("Failed to install role", zap.String("Role ID", role.ID), zap.Error(err))
return errors.Wrap(errors.WithDetailf(err, "Возникла ошибка при настройке роли %s(%s)", role.ID, role.Description), "failed to install role")
} }
} }
return nil return nil
} }
func (s *Setup) UninstallRole(ctx context.Context, c RoleConfig) error { // UninstallRole удаляет роль
if c.DeleteFn(s, c.role) { func (s *Setup) UninstallRole(ctx context.Context, c *Role) error {
err := s.content.Roles.Delete(ctx, s.SpaceID, c.role.ID) role := c.Value(s)
if c.DeleteFunc(s, role) {
err := s.content.Roles.Delete(ctx, s.SpaceID, role.ID)
if err != nil && !strings.Contains(err.Error(), roles.ErrNotFound.Error()) { if err != nil && !strings.Contains(err.Error(), roles.ErrNotFound.Error()) {
return err return err
} }
...@@ -174,6 +142,34 @@ func (s *Setup) UninstallRole(ctx context.Context, c RoleConfig) error { ...@@ -174,6 +142,34 @@ func (s *Setup) UninstallRole(ctx context.Context, c RoleConfig) error {
return nil return nil
} }
// UninstallRoles удаляет все роли
func (s *Setup) UninstallRoles(ctx context.Context) error {
if len(s.Roles) == 0 {
return nil
}
s.logger.Debug("Uninstall role", zap.String("Space ID", s.SpaceID), zap.Int("Roles", len(s.Roles)))
for _, c := range s.Roles {
if err := s.UninstallRole(ctx, c); err != nil {
role := c.Value(s)
s.logger.Error("Failed to uninstall role", zap.String("Role ID", role.ID), zap.Error(err))
return errors.WithDetailf(errors.Wrap(err, "failed to uninstall role"),
"Возникла ошибка при удалении роли %s(%s)", role.ID, role.Description)
}
}
return nil
}
// CheckRole проверяет наличие роли
func (s *Setup) CheckRole(ctx context.Context, c *Role) error {
role := c.Value(s)
_, err := s.content.Roles.Get(ctx, s.SpaceID, role.ID)
return err
}
// CheckRoles проверяет наличие всех ролей
func (s *Setup) CheckRoles(ctx context.Context) error { func (s *Setup) CheckRoles(ctx context.Context) error {
if len(s.Roles) == 0 { if len(s.Roles) == 0 {
return nil return nil
...@@ -183,8 +179,9 @@ func (s *Setup) CheckRoles(ctx context.Context) error { ...@@ -183,8 +179,9 @@ func (s *Setup) CheckRoles(ctx context.Context) error {
var errs []error var errs []error
for _, c := range s.Roles { for _, c := range s.Roles {
if err := s.CheckRole(ctx, c.role); err != nil { if err := s.CheckRole(ctx, c); err != nil {
errs = append(errs, errors.WithDetailf(err, "Не найдена роль %s(%s)", c.role.ID, c.role.Description)) role := c.Value(s)
errs = append(errs, errors.WithDetailf(err, "Не найдена роль %s(%s)", role.ID, role.Description))
} }
} }
...@@ -194,8 +191,3 @@ func (s *Setup) CheckRoles(ctx context.Context) error { ...@@ -194,8 +191,3 @@ func (s *Setup) CheckRoles(ctx context.Context) error {
return nil return nil
} }
func (s *Setup) CheckRole(ctx context.Context, role *roles.Role) error {
_, err := s.content.Roles.Get(ctx, s.SpaceID, role.ID)
return err
}
...@@ -4,12 +4,8 @@ import ( ...@@ -4,12 +4,8 @@ import (
"context" "context"
"io/fs" "io/fs"
"git.perx.ru/perxis/perxis-go/pkg/clients"
"git.perx.ru/perxis/perxis-go/pkg/collections"
"git.perx.ru/perxis/perxis-go/pkg/content" "git.perx.ru/perxis/perxis-go/pkg/content"
"git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/errors"
"git.perx.ru/perxis/perxis-go/pkg/items"
"git.perx.ru/perxis/perxis-go/pkg/roles"
"git.perx.ru/perxis/perxis-go/pkg/spaces" "git.perx.ru/perxis/perxis-go/pkg/spaces"
"go.uber.org/zap" "go.uber.org/zap"
) )
...@@ -52,13 +48,20 @@ func NewSetup(content *content.Content, spaceID, environmentID string, logger *z ...@@ -52,13 +48,20 @@ func NewSetup(content *content.Content, spaceID, environmentID string, logger *z
content: content, content: content,
logger: logger, logger: logger,
waitSpaceAvailable: true, waitSpaceAvailable: true,
Config: NewConfig(), Config: NewConfig(), // Копируем конфигурацию, чтобы не изменять исходную в процессе работы
} }
} }
func (s *Setup) WithConfig(cfg *Config) *Setup { // Logger возвращает логгер
func (s *Setup) Logger() *zap.Logger {
return s.logger
}
// WithConfig устанавливает конфигурацию требований к пространству
// копируем конфигурацию, чтобы не изменять исходную в процессе работы
func (s *Setup) WithConfig(config *Config) *Setup {
setup := *s setup := *s
setup.Config = cfg setup.Config = config.Clone()
return &setup return &setup
} }
...@@ -171,7 +174,6 @@ func (s *Setup) Uninstall(ctx context.Context) error { ...@@ -171,7 +174,6 @@ func (s *Setup) Uninstall(ctx context.Context) error {
return nil return nil
} }
// Deprecated: use Config
func (s *Setup) Load(fsys fs.FS) *Setup { func (s *Setup) Load(fsys fs.FS) *Setup {
var err error var err error
s.Config, err = s.Config.Load(fsys) s.Config, err = s.Config.Load(fsys)
...@@ -180,63 +182,3 @@ func (s *Setup) Load(fsys fs.FS) *Setup { ...@@ -180,63 +182,3 @@ func (s *Setup) Load(fsys fs.FS) *Setup {
} }
return s return s
} }
// Deprecated: use Config instead
// AddCollections добавляет требования к настройке коллекций в пространстве
func (s *Setup) AddCollections(collections []*collections.Collection, opt ...CollectionsOption) *Setup {
for _, col := range collections {
s.AddCollection(col, opt...)
}
return s
}
// Deprecated: use Config instead
// AddCollection добавляет требование к настройке коллекции в пространстве
func (s *Setup) AddCollection(collection *collections.Collection, opt ...CollectionsOption) *Setup {
if _, err := s.Config.AddCollection(collection, opt...); err != nil {
s.AddError(err)
}
return s
}
// Deprecated: use Config instead
// AddRoles добавляет требования к настройке ролей в пространстве
func (s *Setup) AddRoles(roles []*roles.Role, opt ...RolesOption) *Setup {
s.Config = s.Config.AddRoles(roles, opt...)
return s
}
// Deprecated: use Config instead
// AddRole добавляет требования к настройке элементов в пространстве
func (s *Setup) AddRole(role *roles.Role, opt ...RolesOption) *Setup {
s.Config = s.Config.AddRole(role, opt...)
return s
}
// Deprecated: use Config instead
// AddItem добавляет требования к настройке элементов в пространстве
func (s *Setup) AddItem(item *items.Item, opt ...ItemsOption) *Setup {
s.Config = s.Config.AddItem(item, opt...)
return s
}
// Deprecated: use Config instead
// AddItems добавляет требования к настройке элементов в пространстве
func (s *Setup) AddItems(items []*items.Item, opt ...ItemsOption) *Setup {
s.Config = s.Config.AddItems(items, opt...)
return s
}
// Deprecated: use Config instead
// AddClient добавляет требования к настройке элементов в пространстве
func (s *Setup) AddClient(client *clients.Client, opt ...ClientsOption) *Setup {
s.Config = s.Config.AddClient(client, opt...)
return s
}
// Deprecated: use Config instead
// AddClients добавляет требования к настройке элементов в пространстве
func (s *Setup) AddClients(clients []*clients.Client, opt ...ClientsOption) *Setup {
s.Config = s.Config.AddClients(clients, opt...)
return s
}
...@@ -2,26 +2,27 @@ package setup ...@@ -2,26 +2,27 @@ package setup
import ( import (
"context" "context"
"errors"
"testing" "testing"
"git.perx.ru/perxis/perxis-go/pkg/clients"
clientsMock "git.perx.ru/perxis/perxis-go/pkg/clients/mocks" clientsMock "git.perx.ru/perxis/perxis-go/pkg/clients/mocks"
"git.perx.ru/perxis/perxis-go/pkg/collections"
collectionMock "git.perx.ru/perxis/perxis-go/pkg/collections/mocks" collectionMock "git.perx.ru/perxis/perxis-go/pkg/collections/mocks"
"git.perx.ru/perxis/perxis-go/pkg/content"
"git.perx.ru/perxis/perxis-go/pkg/data" "git.perx.ru/perxis/perxis-go/pkg/data"
environmentMock "git.perx.ru/perxis/perxis-go/pkg/environments/mocks" environmentMock "git.perx.ru/perxis/perxis-go/pkg/environments/mocks"
"git.perx.ru/perxis/perxis-go/pkg/errors"
"git.perx.ru/perxis/perxis-go/pkg/items"
itemsMock "git.perx.ru/perxis/perxis-go/pkg/items/mocks" itemsMock "git.perx.ru/perxis/perxis-go/pkg/items/mocks"
"git.perx.ru/perxis/perxis-go/pkg/roles"
rolesMock "git.perx.ru/perxis/perxis-go/pkg/roles/mocks" rolesMock "git.perx.ru/perxis/perxis-go/pkg/roles/mocks"
"git.perx.ru/perxis/perxis-go/pkg/schema"
"git.perx.ru/perxis/perxis-go/pkg/spaces" "git.perx.ru/perxis/perxis-go/pkg/spaces"
"git.perx.ru/perxis/perxis-go/pkg/spaces/mocks" "git.perx.ru/perxis/perxis-go/pkg/spaces/mocks"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"git.perx.ru/perxis/perxis-go/pkg/clients"
"git.perx.ru/perxis/perxis-go/pkg/collections"
"git.perx.ru/perxis/perxis-go/pkg/content"
"git.perx.ru/perxis/perxis-go/pkg/items"
"git.perx.ru/perxis/perxis-go/pkg/roles"
"git.perx.ru/perxis/perxis-go/pkg/schema"
"go.uber.org/zap/zaptest" "go.uber.org/zap/zaptest"
) )
...@@ -78,13 +79,14 @@ func getActions() []*items.Item { ...@@ -78,13 +79,14 @@ func getActions() []*items.Item {
} }
func newSetup(content *content.Content, t *testing.T) *Setup { func newSetup(content *content.Content, t *testing.T) *Setup {
logger := zaptest.NewLogger(t, zaptest.WrapOptions()) config := NewConfig()
setup := NewSetup(content, spaceID, envID, logger) config.Collections.AddMany(getCollections())
setup.AddCollections(getCollections()) config.Roles.AddMany(getRoles())
setup.AddRoles(getRoles()) config.Clients.AddMany(getClients())
setup.AddClients(getClients()) config.Items.AddMany(getActions(), OverwriteItem())
setup.AddItems(getActions(), OverwriteItem())
logger := zaptest.NewLogger(t, zaptest.WrapOptions())
setup := NewSetup(content, spaceID, envID, logger).WithConfig(config)
return setup return setup
} }
...@@ -101,10 +103,8 @@ func TestSetupInstall(t *testing.T) { ...@@ -101,10 +103,8 @@ func TestSetupInstall(t *testing.T) {
spcMock.On("Get", mock.Anything, mock.Anything).Return(sps, nil).Once() spcMock.On("Get", mock.Anything, mock.Anything).Return(sps, nil).Once()
setup := NewSetup(&content.Content{Spaces: spcMock}, spaceID, envID, logger) setup := NewSetup(&content.Content{Spaces: spcMock}, spaceID, envID, logger)
err := setup.Install(context.Background()) err := setup.Install(context.Background())
require.NoError(t, err) require.NoError(t, err)
spcMock.AssertExpectations(t) spcMock.AssertExpectations(t)
}) })
t.Run("Success, no force", func(t *testing.T) { t.Run("Success, no force", func(t *testing.T) {
...@@ -116,23 +116,19 @@ func TestSetupInstall(t *testing.T) { ...@@ -116,23 +116,19 @@ func TestSetupInstall(t *testing.T) {
collsMock := &collectionMock.Collections{} collsMock := &collectionMock.Collections{}
for _, collection := range getCollections() { for _, collection := range getCollections() {
collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID). collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
Return(nil, collections.ErrNotFound). Return(nil, collections.ErrNotFound).Once()
Once()
collsMock.On("Create", mock.Anything, collection). collsMock.On("Create", mock.Anything, collection).
Return(collection, nil). Return(collection, nil).Once()
Once()
collsMock.On("SetSchema", mock.Anything, spaceID, envID, collection.ID, collection.Schema). collsMock.On("SetSchema", mock.Anything, spaceID, envID, collection.ID, collection.Schema).
Return(nil). Return(nil).Once()
Once()
} }
rMock := &rolesMock.Roles{} rMock := &rolesMock.Roles{}
for _, role := range getRoles() { for _, role := range getRoles() {
rMock.On("Get", mock.Anything, spaceID, role.ID). rMock.On("Get", mock.Anything, spaceID, role.ID).
Return(nil, roles.ErrNotFound). Return(nil, roles.ErrNotFound).Once()
Once()
rMock.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { rMock.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
create := args[1].(*roles.Role) create := args[1].(*roles.Role)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment