diff --git a/assets/tests/invalid.txt b/assets/tests/assets/invalid.txt similarity index 100% rename from assets/tests/invalid.txt rename to assets/tests/assets/invalid.txt diff --git a/assets/tests/item.json b/assets/tests/assets/item.json similarity index 100% rename from assets/tests/item.json rename to assets/tests/assets/item.json diff --git a/assets/tests/items.yaml b/assets/tests/assets/items.yaml similarity index 100% rename from assets/tests/items.yaml rename to assets/tests/assets/items.yaml diff --git a/assets/tests/setup/collections/collections.yaml b/assets/tests/setup/collections/collections.yaml new file mode 100644 index 0000000000000000000000000000000000000000..13c7fe7f4f3fd4806d52e29f8a2aa06ba40b94c4 --- /dev/null +++ b/assets/tests/setup/collections/collections.yaml @@ -0,0 +1,42 @@ +--- +type: object +params: + inline: false + fields: + name: + title: Название + description: Название коллекции + type: string + params: + required: true +metadata: + collection_id: collection_a + collection_name: Коллекция A +--- +type: object +params: + inline: false + fields: + name: + title: Название + description: Название коллекции + type: string + params: + required: true +metadata: + collection_id: collection_b + collection_name: Коллекция B +--- +id: collection_c +name: Коллекция C +schema: + type: object + params: + inline: false + fields: + name: + title: Название + description: Название коллекции + type: string + params: + required: true \ No newline at end of file diff --git a/assets_test.go b/assets_test.go index 25f33bb5e7d5d20fd01f25ed248e661fb6c19bf0..69a71b8a5323ca6bd8d7b103f66a70b62054ef29 100644 --- a/assets_test.go +++ b/assets_test.go @@ -47,7 +47,7 @@ func TestFromFS(t *testing.T) { i3.ID = "item3" assets := NewAssets[*testEntry]() - r, err := assets.FromFS(os.DirFS("assets/tests")) + r, err := assets.FromFS(os.DirFS("assets/tests/assets")) require.NoError(t, err) require.Len(t, r, 3) assert.ElementsMatch(t, []*testEntry{i1, &i2, &i3}, r) @@ -74,12 +74,12 @@ func TestFrom(t *testing.T) { i3.ID = "item3" assets := NewAssets[*testEntry]() - r, err := assets.From(os.DirFS("assets"), "tests") + r, err := assets.From(os.DirFS("assets"), "tests/assets") require.NoError(t, err) require.Len(t, r, 3) assert.ElementsMatch(t, []*testEntry{i1, &i2, &i3}, r) - r, err = assets.From(os.DirFS("assets"), "tests/items.yaml") + r, err = assets.From(os.DirFS("assets"), "tests/assets/items.yaml") require.NoError(t, err) require.Len(t, r, 2) assert.Equal(t, []*testEntry{i1, &i2}, r) diff --git a/pkg/clients/client.go b/pkg/clients/client.go index f38b5acc9b442be39139523bcd316947d5d54fe7..6c4c6735b8ec91f226ba5306b2649252390ffc67 100644 --- a/pkg/clients/client.go +++ b/pkg/clients/client.go @@ -45,6 +45,11 @@ type TLS struct { Key string `json:"key,omitempty"` } +// GetID возвращает идентификатор клиента +func (c Client) GetID() string { + return c.ID +} + func (c *Client) SetDisabled(b bool) *Client { c.Disabled = &b return c diff --git a/pkg/collections/collection.go b/pkg/collections/collection.go index 9cdac94e26925a6683df954237f7b79bafd4f5d7..b5d2418aac40332c99b08b31a5df128007e1b7a6 100644 --- a/pkg/collections/collection.go +++ b/pkg/collections/collection.go @@ -1,11 +1,9 @@ package collections import ( - "io/fs" - "strconv" "time" - "git.perx.ru/perxis/perxis-go/pkg/optional" + "git.perx.ru/perxis/perxis-go/pkg/data" "git.perx.ru/perxis/perxis-go/pkg/permission" "git.perx.ru/perxis/perxis-go/pkg/schema" ) @@ -54,16 +52,16 @@ func (a Access) Can(action permission.Action) bool { } type Collection struct { - ID string `json:"id" bson:"id"` - SpaceID string `json:"spaceId" bson:"-"` - EnvID string `json:"envId" bson:"-"` - Name string `json:"name" bson:"name"` - Single *bool `json:"single" bson:"single,omitempty"` // В коллекции может быть только один документ - System *bool `json:"system" bson:"system,omitempty"` // Системная коллекция - NoData *bool `json:"no_data" bson:"no_data"` // Коллекция не содержит элементы. Схема используется для включения в другие схемы - Hidden bool `json:"hidden" bson:"hidden"` // Коллекция скрыта в административном интерфейсе + ID string `json:"id" bson:"id"` + SpaceID string `json:"spaceId" bson:"-"` + EnvID string `json:"envId" bson:"-"` + Name string `json:"name" bson:"name"` + Single *bool `json:"single" bson:"single,omitempty"` // В коллекции может быть только один документ + System *bool `json:"system" bson:"system,omitempty"` // Системная коллекция + NoData *bool `json:"no_data" bson:"no_data"` // Коллекция не содержит элементы. Схема используется для включения в другие схемы + Hidden bool `json:"hidden" bson:"hidden"` // Коллекция скрыта в административном интерфейсе - NoArchive bool `json:"no_archive" bson:"no_archive,omitempty"` // Коллекция без архива + NoArchive bool `json:"no_archive" bson:"no_archive,omitempty"` // Коллекция без архива NoRevisions bool `json:"no_revisions" bson:"no_revisions,omitempty"` // Не хранить историю изменений MaxRevisions uint32 `json:"max_revisions" bson:"max_revisions,omitempty"` // Максимальное количество хранимых ревизий @@ -94,6 +92,33 @@ type Collection struct { Config *Config `json:"-" bson:"-"` } +// GetID возвращает идентификатор коллекции +func (c Collection) GetID() string { + return c.ID +} + +// Equal сравнивает две коллекции, за исключением Schema, Access, StateInfo и Config +func (c Collection) Equal(other *Collection) bool { + if c.ID != other.ID || + c.SpaceID != other.SpaceID || + c.EnvID != other.EnvID || + c.Name != other.Name || + c.IsNoData() != other.IsNoData() || + c.IsSingle() != other.IsSingle() || + c.IsSystem() != other.IsSystem() || + c.Hidden != other.Hidden || + c.NoPublish != other.NoPublish || + c.NoArchive != other.NoArchive || + c.NoRevisions != other.NoRevisions || + c.MaxRevisions != other.MaxRevisions || + c.RevisionTTL != other.RevisionTTL || + !c.View.Equal(other.View) || + !data.ElementsMatch(c.Tags, other.Tags) { + return false + } + return true +} + type View struct { SpaceID string `json:"space_id" bson:"space_id"` // SpaceID оригинальной коллекции EnvID string `json:"environment_id" bson:"environment_id"` // EnvID оригинальной коллекции @@ -148,16 +173,15 @@ const ( ) func (c Collection) Clone() *Collection { - clone := &Collection{ - ID: c.ID, - SpaceID: c.SpaceID, - EnvID: c.EnvID, - Name: c.Name, - NoData: c.NoData, - Hidden: c.Hidden, - NoPublish: c.NoPublish, - NoArchive: c.NoArchive, + ID: c.ID, + SpaceID: c.SpaceID, + EnvID: c.EnvID, + Name: c.Name, + NoData: c.NoData, + Hidden: c.Hidden, + NoPublish: c.NoPublish, + NoArchive: c.NoArchive, NoRevisions: c.NoRevisions, MaxRevisions: c.MaxRevisions, RevisionTTL: c.RevisionTTL, @@ -219,69 +243,3 @@ func GetCollectionsIDs(collections []*Collection) []string { } return res } - -func FromSchemaMetadata(schemas ...*schema.Schema) []*Collection { - result := make([]*Collection, 0, len(schemas)) - for _, sch := range schemas { - coll := &Collection{Schema: sch} - - coll.ID = sch.Metadata["collection_id"] - coll.Name = sch.Metadata["collection_name"] - - if single, ok := sch.Metadata["collection_single"]; ok && single == "true" { - coll.Single = optional.True - } - if system, ok := sch.Metadata["collection_system"]; ok && system == "true" { - coll.System = optional.True - } - if nodata, ok := sch.Metadata["collection_nodata"]; ok && nodata == "true" { - coll.NoData = optional.True - } - if hidden, ok := sch.Metadata["collection_hidden"]; ok && hidden == "true" { - coll.Hidden = true - } - if disablePublishing, ok := sch.Metadata["collection_no_publish"]; ok && disablePublishing == "true" { - coll.NoPublish = true - } - if noArchive, ok := sch.Metadata["collection_no_archive"]; ok && noArchive == "true" { - coll.NoArchive = true - } - - if noRevisions, ok := sch.Metadata["collection_no_revisions"]; ok && noRevisions == "true" { - coll.NoRevisions = true - } - if mr, ok := sch.Metadata["collection_max_revisions"]; ok { - if maxRevisions, err := strconv.ParseUint(mr, 10, 32); err == nil { - coll.MaxRevisions = uint32(maxRevisions) - } - } - if ttl, ok := sch.Metadata["collection_revisions_ttl"]; ok { - if revisionTTL, err := time.ParseDuration(ttl); err == nil { - coll.RevisionTTL = revisionTTL - } - } - - if _, ok := sch.Metadata["collection_view_id"]; ok { - coll.View = &View{ - SpaceID: sch.Metadata["collection_view_space"], - EnvID: sch.Metadata["collection_view_env"], - CollectionID: sch.Metadata["collection_view_id"], - Filter: sch.Metadata["collection_view_filter"], - } - } - - result = append(result, coll) - } - - return result -} - -// TODO: использовать загрузку из файлов коллекций вместо файлов схем -func FromFS(filesystem fs.FS) ([]*Collection, error) { - schemas, err := schema.FromFS(filesystem) - if err != nil { - return nil, err - } - - return FromSchemaMetadata(schemas...), nil -} diff --git a/pkg/collections/collection_test.go b/pkg/collections/collection_test.go index 44a3ee709a25cde20f627df3e4307a4d1b3c4c26..79e16a8e6a397418ef2418979bf0e38947b730c8 100644 --- a/pkg/collections/collection_test.go +++ b/pkg/collections/collection_test.go @@ -3,9 +3,6 @@ package collections import ( "testing" - "git.perx.ru/perxis/perxis-go/pkg/optional" - "git.perx.ru/perxis/perxis-go/pkg/schema" - "git.perx.ru/perxis/perxis-go/pkg/schema/field" "github.com/stretchr/testify/require" ) @@ -103,106 +100,3 @@ func TestView_Equal(t *testing.T) { }) } } - -func TestFromSchemaMetadata(t *testing.T) { - testCases := []struct { - name string - schemas []*schema.Schema - want []*Collection - }{ - { - name: "Nil", - schemas: nil, - want: []*Collection{}, - }, - { - name: "Empty", - schemas: []*schema.Schema{}, - want: []*Collection{}, - }, - { - name: "Without metadata", - schemas: []*schema.Schema{schema.New("a", field.String())}, - want: []*Collection{{Schema: schema.New("a", field.String())}}, - }, - { - name: "With metadata", - schemas: []*schema.Schema{schema.New("a", field.String()).WithMetadata( - "collection_id", "collID", - "collection_name", "collName", - "collection_single", "true", - "collection_system", "true", - "collection_nodata", "true", - "collection_hidden", "true", - "collection_no_publish", "true", - "collection_no_archive", "true", - "collection_view_space", "viewSpaceID", - "collection_view_env", "viewEnvID", - "collection_view_id", "viewCollID", - "collection_view_filter", "viewFilter", - )}, - want: []*Collection{{ - ID: "collID", - Name: "collName", - Single: optional.True, - System: optional.True, - NoData: optional.True, - Hidden: true, - NoPublish: true, - NoArchive: true, - Schema: schema.New("a", field.String()).WithMetadata("collection_id", "collID", "collection_name", "collName", "collection_single", "true", "collection_system", "true", "collection_nodata", "true", "collection_hidden", "true", "collection_no_publish", "true", "collection_no_archive", "true", "collection_view_space", "viewSpaceID", "collection_view_env", "viewEnvID", "collection_view_id", "viewCollID", "collection_view_filter", "viewFilter"), - View: &View{ - SpaceID: "viewSpaceID", - EnvID: "viewEnvID", - CollectionID: "viewCollID", - Filter: "viewFilter", - }, - }}, - }, - { - name: "With metadata revisions settings", - schemas: []*schema.Schema{schema.New("a", field.String()).WithMetadata( - "collection_id", "collID", - "collection_name", "collName", - "collection_no_revisions", "true", - "collection_max_revisions", "10", - "collection_revisions_ttl", "1h", - )}, - want: []*Collection{{ - ID: "collID", - Name: "collName", - NoRevisions: true, - MaxRevisions: 10, - RevisionTTL: 3600000000000, - Schema: schema.New("a", field.String()).WithMetadata("collection_id", "collID", "collection_name", "collName", "collection_no_revisions", "true", "collection_max_revisions", "10", "collection_revisions_ttl", "1h"), - }}, - }, - { - name: "Multiple", - schemas: []*schema.Schema{ - schema.New("a", field.String()).WithMetadata("collection_id", "collID"), - schema.New("b", field.String()).WithMetadata("collection_name", "collName"), - }, - want: []*Collection{ - { - ID: "collID", - Name: "", - Schema: schema.New("a", field.String()).WithMetadata("collection_id", "collID"), - }, - { - - ID: "", - Name: "collName", - Schema: schema.New("b", field.String()).WithMetadata("collection_name", "collName"), - }, - }, - }, - } - - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - result := FromSchemaMetadata(tt.schemas...) - require.Equal(t, tt.want, result) - }) - } -} diff --git a/pkg/collections/marshal.go b/pkg/collections/marshal.go new file mode 100644 index 0000000000000000000000000000000000000000..21c7bed0c5a8edb83780a19d31045d8b8c07f9ef --- /dev/null +++ b/pkg/collections/marshal.go @@ -0,0 +1,92 @@ +package collections + +import ( + "reflect" + "strconv" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/optional" + "git.perx.ru/perxis/perxis-go/pkg/schema" + jsoniter "github.com/json-iterator/go" +) + +// UnmarshalJSON implements json.Unmarshaler interface +func (c *Collection) UnmarshalJSON(b []byte) error { + type collection Collection + var cc, zero collection + + // Пытаемся распарсить как коллекцию + if err := jsoniter.Unmarshal(b, &cc); err != nil { + return err + } + + // Если это не пустая коллекция, то просто присваиваем + if !reflect.DeepEqual(cc, zero) { + *c = Collection(cc) + return nil + } + + // Пытаемся распарсить как схему + var s schema.Schema + if err := jsoniter.Unmarshal(b, &s); err != nil { + return err + } + *c = *FromSchema(&s) + return nil +} + +// FromSchema создает новую коллекцию из схемы +func FromSchema(sch *schema.Schema) *Collection { + if sch == nil { + return nil + } + + coll := &Collection{Schema: sch} + + coll.ID = sch.Metadata["collection_id"] + coll.Name = sch.Metadata["collection_name"] + + if single, ok := sch.Metadata["collection_single"]; ok && single == "true" { + coll.Single = optional.True + } + if system, ok := sch.Metadata["collection_system"]; ok && system == "true" { + coll.System = optional.True + } + if nodata, ok := sch.Metadata["collection_nodata"]; ok && nodata == "true" { + coll.NoData = optional.True + } + if hidden, ok := sch.Metadata["collection_hidden"]; ok && hidden == "true" { + coll.Hidden = true + } + if disablePublishing, ok := sch.Metadata["collection_no_publish"]; ok && disablePublishing == "true" { + coll.NoPublish = true + } + if noArchive, ok := sch.Metadata["collection_no_archive"]; ok && noArchive == "true" { + coll.NoArchive = true + } + + if noRevisions, ok := sch.Metadata["collection_no_revisions"]; ok && noRevisions == "true" { + coll.NoRevisions = true + } + if mr, ok := sch.Metadata["collection_max_revisions"]; ok { + if maxRevisions, err := strconv.ParseUint(mr, 10, 32); err == nil { + coll.MaxRevisions = uint32(maxRevisions) + } + } + if ttl, ok := sch.Metadata["collection_revisions_ttl"]; ok { + if revisionTTL, err := time.ParseDuration(ttl); err == nil { + coll.RevisionTTL = revisionTTL + } + } + + if _, ok := sch.Metadata["collection_view_id"]; ok { + coll.View = &View{ + SpaceID: sch.Metadata["collection_view_space"], + EnvID: sch.Metadata["collection_view_env"], + CollectionID: sch.Metadata["collection_view_id"], + Filter: sch.Metadata["collection_view_filter"], + } + } + + return coll +} diff --git a/pkg/collections/marshal_test.go b/pkg/collections/marshal_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2cc1bf52499422160d189a1e454144de39da4c95 --- /dev/null +++ b/pkg/collections/marshal_test.go @@ -0,0 +1,130 @@ +package collections + +import ( + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/optional" + "git.perx.ru/perxis/perxis-go/pkg/schema" + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + jsoniter "github.com/json-iterator/go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCollection_UnmarshalJSON(t *testing.T) { + sch := schema.New("a", field.String()).WithMetadata( + "collection_id", "collID", + "collection_name", "collName") + sch.ClearState() + + tests := []struct { + name string + in any + expect *Collection + wantErr bool + }{ + {name: "from schema", + in: sch, + expect: &Collection{Schema: sch, ID: "collID", Name: "collName"}, + wantErr: false}, + {name: "from collection", + in: &Collection{Schema: sch, ID: "id", Name: "name"}, + expect: &Collection{Schema: sch, ID: "id", Name: "name"}, + wantErr: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var c Collection + b, _ := jsoniter.Marshal(tt.in) + if err := jsoniter.Unmarshal(b, &c); (err != nil) != tt.wantErr { + t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + } + + assert.True(t, c.Equal(tt.expect)) + assert.True(t, c.Schema.Equal(tt.expect.Schema)) + }) + } +} + +func TestFromSchemaMetadata(t *testing.T) { + testCases := []struct { + name string + schema *schema.Schema + want *Collection + }{ + { + name: "Nil", + schema: nil, + want: nil, + }, + { + name: "Empty", + schema: &schema.Schema{}, + want: &Collection{Schema: &schema.Schema{}}, + }, + { + name: "Without metadata", + schema: schema.New("a", field.String()), + want: &Collection{Schema: schema.New("a", field.String())}, + }, + { + name: "With metadata", + schema: schema.New("a", field.String()).WithMetadata( + "collection_id", "collID", + "collection_name", "collName", + "collection_single", "true", + "collection_system", "true", + "collection_nodata", "true", + "collection_hidden", "true", + "collection_no_publish", "true", + "collection_no_archive", "true", + "collection_view_space", "viewSpaceID", + "collection_view_env", "viewEnvID", + "collection_view_id", "viewCollID", + "collection_view_filter", "viewFilter", + ), + want: &Collection{ + ID: "collID", + Name: "collName", + Single: optional.True, + System: optional.True, + NoData: optional.True, + Hidden: true, + NoPublish: true, + NoArchive: true, + Schema: schema.New("a", field.String()).WithMetadata("collection_id", "collID", "collection_name", "collName", "collection_single", "true", "collection_system", "true", "collection_nodata", "true", "collection_hidden", "true", "collection_no_publish", "true", "collection_no_archive", "true", "collection_view_space", "viewSpaceID", "collection_view_env", "viewEnvID", "collection_view_id", "viewCollID", "collection_view_filter", "viewFilter"), + View: &View{ + SpaceID: "viewSpaceID", + EnvID: "viewEnvID", + CollectionID: "viewCollID", + Filter: "viewFilter", + }, + }, + }, + { + name: "With metadata revisions settings", + schema: schema.New("a", field.String()).WithMetadata( + "collection_id", "collID", + "collection_name", "collName", + "collection_no_revisions", "true", + "collection_max_revisions", "10", + "collection_revisions_ttl", "1h", + ), + want: &Collection{ + ID: "collID", + Name: "collName", + NoRevisions: true, + MaxRevisions: 10, + RevisionTTL: 3600000000000, + Schema: schema.New("a", field.String()).WithMetadata("collection_id", "collID", "collection_name", "collName", "collection_no_revisions", "true", "collection_max_revisions", "10", "collection_revisions_ttl", "1h"), + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + result := FromSchema(tt.schema) + require.Equal(t, tt.want, result) + }) + } +} diff --git a/pkg/data/list.go b/pkg/data/list.go index 4da827db9680bc20237dfc87023db1c31ff8a664..4481a18861c3524c8636b4f9fbfbdf14efb6f711 100644 --- a/pkg/data/list.go +++ b/pkg/data/list.go @@ -185,7 +185,13 @@ func mergeMaps(a, b map[string]interface{}) map[string]interface{} { return out } +// CloneSlice возвращает копию среза с клонированными элементами func CloneSlice[T interface{ Clone() T }](s []T) []T { + // Если s == nil, то возвращаем nil + if s == nil { + return nil + } + result := make([]T, 0, len(s)) for _, t := range s { result = append(result, t.Clone()) diff --git a/pkg/extension/extension.go b/pkg/extension/extension.go index ee87e981e6d31bb39b52516870d3bc637182a5af..15b92676be86b1782eb0ca8e5feb98bf1f8ba186 100644 --- a/pkg/extension/extension.go +++ b/pkg/extension/extension.go @@ -3,6 +3,8 @@ package extension import ( "context" + "go.uber.org/zap" + "git.perx.ru/perxis/perxis-go" "git.perx.ru/perxis/perxis-go/pkg/collections" "git.perx.ru/perxis/perxis-go/pkg/content" @@ -18,28 +20,24 @@ const ( StateInstalled = pb.State_INSTALLED StateInProgress = pb.State_IN_PROGRESS StateFail = pb.State_FAIL - - MetadataKey = "extension" + MetadataKey = "extension" ) type ( - InstallRequest = pb.InstallRequest - CheckRequest = pb.CheckRequest - UninstallRequest = pb.UninstallRequest - + InstallRequest = pb.InstallRequest + CheckRequest = pb.CheckRequest + UninstallRequest = pb.UninstallRequest ExtensionDescriptor = pb.ExtensionDescriptor State = pb.State ) var ( - ErrStart = errors.New("start failed") - ErrStop = errors.New("stop failed") - - ErrInstall = errors.New("install failed") - ErrUpdate = errors.New("update failed") - ErrCheck = errors.New("check failed") - ErrUninstall = errors.New("uninstall failed") - + ErrStart = errors.New("start failed") + ErrStop = errors.New("stop failed") + ErrInstall = errors.New("install failed") + ErrUpdate = errors.New("update failed") + ErrCheck = errors.New("check failed") + ErrUninstall = errors.New("uninstall failed") ErrNotInstalled = errors.New("not installed") ErrUnknownExtension = errors.New("unknown extension") @@ -79,7 +77,8 @@ func CheckInstalled(ctx context.Context, content *content.Content, spaceID, envI return status.State == StateInstalled, nil } -func isMetadataEqual(s1, s2 *schema.Schema) bool { +// isSameExtension возвращает true, если значение метаданных для ключа расширения совпадает +func isSameExtension(s1, s2 *schema.Schema) bool { if s1.Metadata == nil && s2.Metadata == nil { return true } @@ -89,13 +88,16 @@ func isMetadataEqual(s1, s2 *schema.Schema) bool { return s1.Metadata[MetadataKey] == s2.Metadata[MetadataKey] } -// UpdateCollectionStrategy В дополнение к стратегии по умолчанию делает проверку, что обновляемая -// коллекция была установлена расширением. Если в метаданных схемы отсутствует специальный ключ `MetadataKey`, -// это означает, что коллекция была создана пользователем и отношения к расширению не имеет - вернется ошибка. -func UpdateCollectionStrategy(s *setup.Setup, exist, collection *collections.Collection) (*collections.Collection, bool, bool, error) { - if !s.IsForce() && !collection.IsView() && !exist.IsView() && !isMetadataEqual(collection.Schema, exist.Schema) { - return nil, false, false, errors.WithDetailf(collections.ErrAlreadyExists, "Коллекция с идентификатором '%s' "+ - "уже существует. Удалите ее или вызовите установку расширения с флагом Force", collection.ID) +// UpdateExtensionCollections возвращает опцию для обновления коллекций расширения +func UpdateExtensionCollections() setup.CollectionOption { + return func(e *setup.Collection) { + next := e.UpdateFunc + e.UpdateFunc = func(s *setup.Setup, old, new *collections.Collection) (*collections.Collection, bool) { + if !s.IsForce() && !new.IsView() && !old.IsView() && !isSameExtension(new.Schema, old.Schema) { + s.Logger().Warn("Collection is already exists and not updated", zap.String("ID", new.ID)) + return nil, false + } + return next(s, old, new) + } } - return setup.DefaultUpdateCollectionStrategyFn(s, exist, collection) } diff --git a/pkg/extension/extension_test.go b/pkg/extension/extension_test.go index 4c552b394f7a67cc9de5bb4aa25e90d22fe6058a..1e5755e67fe17d02d6d7b7fbf239da49e402ceb0 100644 --- a/pkg/extension/extension_test.go +++ b/pkg/extension/extension_test.go @@ -3,8 +3,11 @@ package extension import ( "testing" + "go.uber.org/zap" + + "go.uber.org/zap/zaptest/observer" + "git.perx.ru/perxis/perxis-go/pkg/collections" - "git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/schema" "git.perx.ru/perxis/perxis-go/pkg/schema/field" "git.perx.ru/perxis/perxis-go/pkg/setup" @@ -45,69 +48,80 @@ func Test_isMetadataEqual(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, isMetadataEqual(tt.s1, tt.s2), "isMetadataExtensionEqual(%v, %v)", tt.s1, tt.s2) + assert.Equalf(t, tt.want, isSameExtension(tt.s1, tt.s2), "isMetadataExtensionEqual(%v, %v)", tt.s1, tt.s2) }) } } -func TestDefaultUpdateCollectionStrategyFn(t *testing.T) { +func TestDefaultUpdate_ExtensionCollections(t *testing.T) { tests := []struct { name string exist *collections.Collection collection *collections.Collection force bool - wantErr func(err error) + update bool + wantLogs int }{ { - name: "collection belongs to extension", + name: "Same extension", exist: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata("extension", "extension-1")}, collection: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Name: "new name", Schema: schema.New("name", field.String()).WithMetadata("extension", "extension-1")}, - wantErr: nil, + update: true, + }, + { + name: "Other extension", + exist: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", + Schema: schema.New("name", field.String()).WithMetadata("extension", "extension-1")}, + collection: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Name: "new name", + Schema: schema.New("name", field.String()).WithMetadata("extension", "extension-2")}, + update: false, + wantLogs: 1, }, { - name: "collection belongs to another extension", + name: "Other extension force", exist: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata("extension", "extension-1")}, collection: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Name: "new name", Schema: schema.New("name", field.String()).WithMetadata("extension", "extension-2")}, - wantErr: func(err error) { - assert.ErrorIs(t, err, collections.ErrAlreadyExists) - assert.Equal(t, "Коллекция с идентификатором 'coll' уже существует. Удалите ее или "+ - "вызовите установку расширения с флагом Force", errors.GetDetail(err)) - }, + force: true, + update: true, }, { - name: "collection was created by user", + name: "User collection", exist: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String())}, collection: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Name: "new name", Schema: schema.New("name", field.String()).WithMetadata("extension", "extension-1")}, - wantErr: func(err error) { - assert.ErrorIs(t, err, collections.ErrAlreadyExists) - assert.Equal(t, "Коллекция с идентификатором 'coll' уже существует. Удалите ее или "+ - "вызовите установку расширения с флагом Force", errors.GetDetail(err)) - }, + update: false, + wantLogs: 1, }, { - name: "collection was created by user with force", + name: "User collection force", exist: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", - Schema: schema.New("name", field.String()).WithMetadata("extension", "extension-1")}, + Schema: schema.New("name", field.String())}, collection: &collections.Collection{ID: "coll", SpaceID: "sp", EnvID: "env", Name: "new name", Schema: schema.New("name", field.String()).WithMetadata("extension", "extension-1")}, - force: true, - wantErr: nil, + force: true, + update: true, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - set := setup.NewSetup(nil, "sp", "env", nil).WithForce(tt.force) - _, _, _, err := UpdateCollectionStrategy(set, tt.exist, tt.collection) - if tt.wantErr != nil { - tt.wantErr(err) - return - } + cfg := setup.NewConfig() + cfg.Collections.Add(tt.collection, UpdateExtensionCollections()) + + core, ob := observer.New(zap.DebugLevel) + logger := zap.New(core) + set := setup.NewSetup(nil, "sp", "env", logger).WithForce(tt.force) + _, update := cfg.Collections[0].UpdateFunc(set, tt.exist, tt.collection) + + assert.Equal(t, tt.update, update) + logs := ob.TakeAll() + assert.Len(t, logs, tt.wantLogs) + }) } } diff --git a/pkg/extension/service/extension.go b/pkg/extension/service/extension.go index 338dadc29ff89a46d3e054ce47c202ff7707f549..5d57e56c86190403cfe18a8084c902497280c993 100644 --- a/pkg/extension/service/extension.go +++ b/pkg/extension/service/extension.go @@ -98,7 +98,7 @@ func (s *Extension) setupExtensionClient(set *setup.Setup, spaceID string) { role := *s.role role.SpaceID = spaceID - set.AddRole(&role, setup.DeleteRoleIfRemove()) + set.AddRole(&role, setup.DeleteRoleIfRemoveFlag()) if s.client == nil { s.client = &clients.Client{} @@ -130,13 +130,13 @@ func (s *Extension) setupExtensionClient(set *setup.Setup, spaceID string) { } } - set.AddClient(&client, setup.OverwriteClient(), setup.DeleteClientIfRemove()) + set.AddClient(&client, setup.OverwriteClient(), setup.DeleteClientIfRemoveFlag()) } func (s *Extension) GetSetup(spaceID, envID string) (*setup.Setup, error) { set := s.setupFunc(spaceID, envID) - set.WithCollectionOptions( - func(c *collections.Collection) bool { return c.Schema != nil }, + set.Config.Collections.WithOptions( + func(c *collections.Collection) bool { return true }, setup.AddMetadata(extension.MetadataKey, s.GetName()), ) if set.HasErrors() { diff --git a/pkg/items/item.go b/pkg/items/item.go index 25693466a0ad80115cc7500cd256b3da395413b3..0f32b4c266f06e56ef5eb90783fdea8df5eb887c 100644 --- a/pkg/items/item.go +++ b/pkg/items/item.go @@ -145,6 +145,11 @@ func NewItem(spaceID, envID, collID, id string, data map[string]interface{}, tra } } +// GetID возвращает идентификатор записи +func (i *Item) GetID() string { + return i.ID +} + func (i *Item) Clone() *Item { itm := *i itm.Data = data.CloneMap(i.Data) diff --git a/pkg/roles/role.go b/pkg/roles/role.go index 579a9664ca87382c7c7533ecf502df3d6cb5c990..76520f8fbf353c027ae782efb1bf79a1bfa453f5 100644 --- a/pkg/roles/role.go +++ b/pkg/roles/role.go @@ -35,6 +35,11 @@ type Role struct { AllowManagement bool `json:"allow_management" bson:"allow_management"` } +// GetID возвращает идентификатор роли +func (r Role) GetID() string { + return r.ID +} + func (r Role) Clone() *Role { return &Role{ ID: r.ID, diff --git a/pkg/schema/field/field.go b/pkg/schema/field/field.go index 2645730fb33ba7c0913f9a32cd46018851d067f2..4fc9516f1d6b64ce49ee42ea527b84992477365b 100644 --- a/pkg/schema/field/field.go +++ b/pkg/schema/field/field.go @@ -123,6 +123,10 @@ func (f *Field) EnableState() { } func (f Field) GetType() Type { + if f.Params == nil { + return nil + } + return f.Params.Type() } diff --git a/pkg/schema/field/field_json.go b/pkg/schema/field/field_json.go index 170e3d161f87300955b06d21e8cf79e2140f9eb8..4d5dfcb80fc5d6758056753ea49b8e294c76aeae 100644 --- a/pkg/schema/field/field_json.go +++ b/pkg/schema/field/field_json.go @@ -70,6 +70,10 @@ func (f *Field) UnmarshalJSON(b []byte) error { } func (f *Field) MarshalJSON() ([]byte, error) { + if f.Params == nil { + return nil, errors.New("field parameters is nil") + } + j := jsonField{ FieldData: FieldData(*f), } diff --git a/pkg/setup/client.go b/pkg/setup/client.go index d465670e4ea063d8583277f7b743df1835501c6b..c40ad506fb1e2bee225cf6acd2930d7364bbb8b0 100644 --- a/pkg/setup/client.go +++ b/pkg/setup/client.go @@ -4,8 +4,9 @@ import ( "context" "strings" - "git.perx.ru/perxis/perxis-go/pkg/clients" "git.perx.ru/perxis/perxis-go/pkg/errors" + + "git.perx.ru/perxis/perxis-go/pkg/clients" "go.uber.org/zap" ) @@ -15,56 +16,32 @@ var ( ErrUninstallClients = errors.New("failed to uninstall clients") ) -type ClientsOption func(c *ClientConfig) -type UpdateClientFn func(s *Setup, exist, new *clients.Client) (*clients.Client, bool) -type DeleteClientFn func(s *Setup, client *clients.Client) bool - -type ClientConfig struct { - client *clients.Client - UpdateFn UpdateClientFn - DeleteFn DeleteClientFn -} - -func NewClientConfig(client *clients.Client, opt ...ClientsOption) ClientConfig { - c := ClientConfig{client: client} - - UpdateExistingClient()(&c) - DeleteClientIfRemove()(&c) - - for _, o := range opt { - o(&c) - } - - return c -} - -func OverwriteClient() ClientsOption { - return func(c *ClientConfig) { - c.UpdateFn = func(s *Setup, old, new *clients.Client) (*clients.Client, bool) { return new, true } - } -} +type ( + Client = Entity[ClientConf, *clients.Client] + Clients = EntityList[ClientConf, *clients.Client] + ClientOption = EntityOption[ClientConf, *clients.Client] + ClientConf struct{} +) -func KeepExistingClient() ClientsOption { - return func(c *ClientConfig) { - c.UpdateFn = func(s *Setup, old, new *clients.Client) (*clients.Client, bool) { return old, false } - } +func (ClientConf) Init(e *Entity[ClientConf, *clients.Client]) { + UpdateExistingClient()(e) + DeleteClientIfRemoveFlag()(e) } -func DeleteClient() ClientsOption { - return func(c *ClientConfig) { - c.DeleteFn = func(s *Setup, client *clients.Client) bool { return true } - } -} +var ( + OverwriteClient = Overwrite[ClientConf, *clients.Client] + KeepExistingClient = Keep[ClientConf, *clients.Client] + DeleteClient = Delete[ClientConf, *clients.Client] + DeleteClientIfRemoveFlag = DeleteIfRemoveFlag[ClientConf, *clients.Client] +) -func DeleteClientIfRemove() ClientsOption { - return func(c *ClientConfig) { - c.DeleteFn = func(s *Setup, client *clients.Client) bool { return s.IsRemove() } - } -} +///// -func UpdateExistingClient() ClientsOption { - return func(c *ClientConfig) { - c.UpdateFn = func(s *Setup, exist, client *clients.Client) (*clients.Client, bool) { +// UpdateExistingClient обновляет существующий клиент, если он существует +// и не содержит необходимых данных +func UpdateExistingClient() ClientOption { + return func(c *Client) { + c.UpdateFunc = func(s *Setup, exist, client *clients.Client) (*clients.Client, bool) { if exist.Name == "" { exist.Name = client.Name } @@ -93,61 +70,90 @@ func UpdateExistingClient() ClientsOption { } } -func (s *Setup) InstallClients(ctx context.Context) error { - if len(s.Clients) == 0 { - return nil - } +// +// Client Setup +// - s.logger.Debug("Install clients", zap.String("Space ID", s.SpaceID)) +// AddClient добавляет требования к настройке элементов в пространстве +func (s *Setup) AddClient(client *clients.Client, opt ...ClientOption) *Setup { + s.Config.Clients.Add(client, opt...) + return s +} - for _, c := range s.Clients { - err := s.InstallClient(ctx, c) - if err != nil { - s.logger.Error("Failed to install client", zap.String("Client ID", c.client.ID), zap.String("Client Name", c.client.Name), zap.Error(err)) - return errors.WithDetailf(errors.Wrap(err, "failed to install client"), "Возникла ошибка при настройке клиента %s(%s)", c.client.Name, c.client.ID) - } - } - return nil +// AddClients добавляет требования к настройке элементов в пространстве +func (s *Setup) AddClients(clients []*clients.Client, opt ...ClientOption) *Setup { + s.Config.Clients.AddMany(clients, opt...) + return s } -func (s *Setup) InstallClient(ctx context.Context, c ClientConfig) error { - client := c.client +// InstallClient устанавливает клиент в пространстве +func (s *Setup) InstallClient(ctx context.Context, c *Client) error { + client := c.Value(s) client.SpaceID = s.SpaceID if s.IsForce() { - _ = s.content.Clients.Delete(ctx, s.SpaceID, c.client.ID) - _, err := s.content.Clients.Create(ctx, c.client) + _ = s.content.Clients.Delete(ctx, s.SpaceID, client.ID) + _, err := s.content.Clients.Create(ctx, client) return err } - exist, err := s.content.Clients.Get(ctx, s.SpaceID, c.client.ID) + exist, err := s.content.Clients.Get(ctx, s.SpaceID, client.ID) if err != nil { if !strings.Contains(err.Error(), clients.ErrNotFound.Error()) { return err } - _, err = s.content.Clients.Create(ctx, c.client) + _, err = s.content.Clients.Create(ctx, client) return err } - if client, upd := c.UpdateFn(s, exist, c.client); upd { + if client, upd := c.UpdateFunc(s, exist, client); upd { return s.content.Clients.Update(ctx, client) } return nil } +// InstallClients устанавливает все клиенты в пространстве +func (s *Setup) InstallClients(ctx context.Context) error { + if len(s.Clients) == 0 { + return nil + } + + s.logger.Debug("Install clients", zap.String("Space ID", s.SpaceID)) + + for _, c := range s.Clients { + err := s.InstallClient(ctx, c) + if err != nil { + client := c.Value(s) + s.logger.Error("Failed to install client", zap.String("Client ID", client.ID), zap.String("Client Name", client.Name), zap.Error(err)) + return errors.WithDetailf(errors.Wrap(err, "failed to install client"), "Возникла ошибка при настройке клиента %s(%s)", client.Name, client.ID) + } + } + return nil +} + +// CheckClient проверяет наличие клиента в пространстве +func (s *Setup) CheckClient(ctx context.Context, c *Client) error { + client := c.Value(s) + _, err := s.content.Clients.Get(ctx, s.SpaceID, client.ID) + return err +} + +// CheckClients проверяет наличие всех клиентов в пространстве func (s *Setup) CheckClients(ctx context.Context) (err error) { if len(s.Clients) == 0 { return nil } - var errs []error s.logger.Debug("Check clients", zap.String("Space ID", s.SpaceID)) + + var errs []error for _, c := range s.Clients { - err := s.CheckClient(ctx, c.client) + err := s.CheckClient(ctx, c) if err != nil { - errs = append(errs, errors.WithDetailf(err, "Не найден клиент %s(%s)", c.client.Name, c.client.ID)) + client := c.Value(s) + errs = append(errs, errors.WithDetailf(err, "Не найден клиент %s(%s)", client.Name, client.ID)) } } @@ -158,11 +164,18 @@ func (s *Setup) CheckClients(ctx context.Context) (err error) { return nil } -func (s *Setup) CheckClient(ctx context.Context, client *clients.Client) error { - _, err := s.content.Clients.Get(ctx, s.SpaceID, client.ID) - return err +// UninstallClient удаляет клиент из пространства +func (s *Setup) UninstallClient(ctx context.Context, c *Client) error { + client := c.Value(s) + if c.DeleteFunc(s, client) { + if err := s.content.Clients.Delete(ctx, s.SpaceID, client.ID); err != nil && !strings.Contains(err.Error(), clients.ErrNotFound.Error()) { + return err + } + } + return nil } +// UninstallClients удаляет все клиенты из пространства func (s *Setup) UninstallClients(ctx context.Context) error { if len(s.Clients) == 0 { return nil @@ -172,19 +185,13 @@ func (s *Setup) UninstallClients(ctx context.Context) error { for _, c := range s.Clients { if err := s.UninstallClient(ctx, c); err != nil { - s.logger.Error("Failed to uninstall client", zap.String("Client ID", c.client.ID), zap.String("Client Name", c.client.Name), zap.Error(err)) - return errors.WithDetailf(errors.Wrap(err, "failed to uninstall client"), "Возникла ошибка при удалении клиента %s(%s)", c.client.Name, c.client.ID) + client := c.Value(s) + s.logger.Error("Failed to uninstall client", zap.String("Client ID", client.ID), + zap.String("Client Name", client.Name), zap.Error(err)) + return errors.WithDetailf(errors.Wrap(err, "failed to uninstall client"), + "Возникла ошибка при удалении клиента %s(%s)", client.Name, client.ID) } } return nil } - -func (s *Setup) UninstallClient(ctx context.Context, c ClientConfig) error { - if c.DeleteFn(s, c.client) { - if err := s.content.Clients.Delete(ctx, s.SpaceID, c.client.ID); err != nil && !strings.Contains(err.Error(), clients.ErrNotFound.Error()) { - return err - } - } - return nil -} diff --git a/pkg/setup/client_test.go b/pkg/setup/client_test.go index d345756a2b0b87e7ad10aebc1ee9ce0899df199c..6626ec18429955e063139fa24f729ac6d9476dcc 100644 --- a/pkg/setup/client_test.go +++ b/pkg/setup/client_test.go @@ -1,12 +1,10 @@ package setup import ( - "context" "testing" "git.perx.ru/perxis/perxis-go/pkg/clients" clientsMock "git.perx.ru/perxis/perxis-go/pkg/clients/mocks" - "git.perx.ru/perxis/perxis-go/pkg/content" "git.perx.ru/perxis/perxis-go/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -89,9 +87,9 @@ func TestSetup_InstallClients(t *testing.T) { tt.clientsCall(c) } - s := NewSetup(&content.Content{Clients: c}, "sp", "env", nil) - s.AddClients(tt.clients) - tt.wantErr(t, s.InstallClients(context.Background())) + //s := NewSetup(&content.Content{Clients: c}, "sp", "env", nil) + //s.AddClients(tt.clients) + //tt.wantErr(t, s.InstallClients(context.Background())) }) } } diff --git a/pkg/setup/collection.go b/pkg/setup/collection.go index 2d3656eaebd339dd9e064cef57e2ff6e501b1e2b..e597acfcabefd70a9a8abb3d2fed64c11fb274d2 100644 --- a/pkg/setup/collection.go +++ b/pkg/setup/collection.go @@ -4,8 +4,9 @@ import ( "context" "strings" - "git.perx.ru/perxis/perxis-go/pkg/collections" "git.perx.ru/perxis/perxis-go/pkg/data" + + "git.perx.ru/perxis/perxis-go/pkg/collections" "git.perx.ru/perxis/perxis-go/pkg/errors" "go.uber.org/zap" ) @@ -16,112 +17,145 @@ var ( 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) -type DeleteCollectionFn func(s *Setup, col *collections.Collection) (bool, error) - -type CollectionConfig struct { - collection *collections.Collection - UpdateFn UpdateCollectionFn - DeleteFn DeleteCollectionFn - SkipMigration bool -} +// +// Collection Configuration +// -func NewCollectionConfig(collection *collections.Collection, opt ...CollectionsOption) (c CollectionConfig, err error) { - collection = collection.Clone() +type ( + Collection = Entity[CollectionConf, *collections.Collection] + Collections = EntityList[CollectionConf, *collections.Collection] + CollectionOption = EntityOption[CollectionConf, *collections.Collection] + CollectionFilter = FilterFunc[*collections.Collection] - if collection.Schema != nil { - // приведение внутренних типов схемы, чтобы избежать возможного несоответствия типов при - // сравнивании схем (`[]interface{}/[]string`, `int/int64`, etc.) - err = collection.Schema.ConvertTypes() - if err != nil { - return - } + CollectionConf struct { + SkipMigration bool } +) - c = CollectionConfig{collection: collection} - - DefaultUpdateCollectionStrategy()(&c) - DeleteCollectionIfRemove()(&c) +var ( + OverwriteCollection = Overwrite[CollectionConf, *collections.Collection] + KeepCollection = Keep[CollectionConf, *collections.Collection] + DeleteCollection = Delete[CollectionConf, *collections.Collection] + DeleteCollectionIfRemoveFlag = DeleteIfRemoveFlag[CollectionConf, *collections.Collection] +) - for _, o := range opt { - o(&c) +// Init инициализирует конфигурацию коллекции +func (CollectionConf) Init(e *Entity[CollectionConf, *collections.Collection]) { + // Сформированная вручную схема может иметь разные типы данных для одних и тех же полей + // (`[]interface{}/[]string`, `int/int64`, etc.), что может привести к ошибкам + // при сравнении схем. Поэтому приводим все типы данных к одному типу. + if e.value.Schema != nil { + if err := e.value.Schema.ConvertTypes(); err != nil { + panic(err) + } } - return c, nil + // Устанавливаем стратегии обновления и удаления коллекции по умолчанию + UpdateExistingCollection()(e) + DeleteCollectionIfRemoveFlag()(e) } -func AddMetadata(key, value string) CollectionsOption { - return func(c *CollectionConfig) { - c.collection.Schema.WithMetadata(key, value) - } +func IsSchemaUpdateRequired(old, new *collections.Collection) bool { + return !new.IsView() && (old == nil || !old.Schema.Equal(new.Schema)) } -func SkipMigration() CollectionsOption { - return func(c *CollectionConfig) { - c.SkipMigration = true +// AddMetadata добавляет метаданные к коллекции +func AddMetadata(key, value string) CollectionOption { + 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 OverwriteCollection() CollectionsOption { - return func(c *CollectionConfig) { - c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool, error) { - update := new.Name != old.Name || new.IsSingle() != old.IsSingle() || new.IsSystem() != old.IsSystem() || - new.IsNoData() != old.IsNoData() || new.Hidden != old.Hidden || !new.View.Equal(old.View) || !data.ElementsMatch(old.Tags, new.Tags) - - return new, update, !old.Schema.Equal(new.Schema), nil - } +// SkipMigration пропускает миграцию коллекции +func SkipMigration() CollectionOption { + return func(e *Collection) { + e.Conf.SkipMigration = true } } -func KeepExistingCollection() CollectionsOption { - return func(c *CollectionConfig) { - c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool, error) { - return old, false, false, nil +// UpdateExistingCollection обновляет существующую коллекцию +func UpdateExistingCollection() CollectionOption { + return func(e *Collection) { + e.UpdateFunc = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool) { + // Копируем теги из старой коллекции в новую + if len(old.Tags) > 0 { + 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 + } + + return new, update || IsSchemaUpdateRequired(old, new) } } } -func DeleteCollection() CollectionsOption { - return func(c *CollectionConfig) { - c.DeleteFn = func(s *Setup, collection *collections.Collection) (bool, error) { return true, nil } - } -} +// +// Collection Setup +// -func DeleteCollectionIfRemove() CollectionsOption { - return func(c *CollectionConfig) { - c.DeleteFn = func(s *Setup, collection *collections.Collection) (bool, error) { return s.IsRemove(), nil } - } +// AddCollection добавляет требование к настройке коллекции в пространстве (runtime) +func (s *Setup) AddCollection(collection *collections.Collection, opt ...CollectionOption) *Setup { + s.Config.Collections.Add(collection, opt...) + return s } -func DefaultUpdateCollectionStrategyFn(_ *Setup, exist, collection *collections.Collection) (*collections.Collection, bool, bool, error) { - if len(exist.Tags) > 0 { - collection.Tags = data.SetFromSlice(append(exist.Tags, collection.Tags...)) +// AddCollections добавляет несколько требований к настройке коллекций в пространстве (runtime) +func (s *Setup) AddCollections(collections []*collections.Collection, opt ...CollectionOption) *Setup { + for _, c := range collections { + s.AddCollection(c, opt...) } + return s +} - var update, setSchema bool - update = collection.Name != exist.Name || collection.IsSingle() != exist.IsSingle() || collection.IsSystem() != exist.IsSystem() || - collection.IsNoData() != exist.IsNoData() || collection.Hidden != exist.Hidden || collection.IsView() != exist.IsView() && data.ElementsMatch(exist.Tags, collection.Tags) +func (s *Setup) InstallCollection(ctx context.Context, c *Collection) (updateSchema bool, err error) { + collection := c.Value(s) + collection.SpaceID, collection.EnvID = s.SpaceID, s.EnvironmentID - if exist.View != nil && collection.View != nil { - update = update && *exist.View == *collection.View + 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 } - setSchema = !collection.IsView() && !exist.Schema.Equal(collection.Schema) - - return collection, update, setSchema, nil -} + if exist == nil { + if _, err = s.content.Collections.Create(ctx, collection); err != nil { + return false, err + } + } else { + var updateCollection bool + collection, updateCollection = c.UpdateFunc(s, exist, collection) + if !updateCollection { + return false, nil + } -func DefaultUpdateCollectionStrategy() CollectionsOption { - return func(c *CollectionConfig) { - c.UpdateFn = DefaultUpdateCollectionStrategyFn + // TODO: Проверить, что коллекция изменилась + // TODO: Тест на сравнение схем + // Замена возможного алиаса окружения на реального 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) { - c.UpdateFn = fn + // Проверяем, нужно ли обновить схему коллекции + if IsSchemaUpdateRequired(exist, collection) { + 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) { @@ -136,14 +170,16 @@ func (s *Setup) InstallCollections(ctx context.Context) (err error) { for _, c := range s.Collections { setSchema, err = s.InstallCollection(ctx, c) if err != nil { + collection := c.Value(s) s.logger.Error("Failed to install collection", - zap.String("Collection ID", c.collection.ID), - zap.String("Collection Name", c.collection.Name), + zap.String("Collection ID", collection.ID), + zap.String("Collection Name", collection.Name), 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 } } @@ -164,46 +200,6 @@ func (s *Setup) InstallCollections(ctx context.Context) (err error) { 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 { if len(s.Collections) == 0 { return nil @@ -213,8 +209,9 @@ func (s *Setup) CheckCollections(ctx context.Context) error { var errs []error for _, c := range s.Collections { - if err := s.CheckCollection(ctx, c); err != nil { - errs = append(errs, errors.WithDetailf(err, "Не найдена коллекция %s(%s)", c.collection.ID, c.collection.Name)) + collection := c.Value(s) + 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 { return nil } -func (s *Setup) CheckCollection(ctx context.Context, c CollectionConfig) (err error) { - _, err = s.content.Collections.Get(ctx, s.SpaceID, s.EnvironmentID, c.collection.ID) +func (s *Setup) CheckCollection(ctx context.Context, c *collections.Collection) (err error) { + _, err = s.content.Collections.Get(ctx, s.SpaceID, s.EnvironmentID, c.ID) return err } @@ -239,28 +236,30 @@ func (s *Setup) UninstallCollections(ctx context.Context) error { for _, c := range s.Collections { if err := s.UninstallCollection(ctx, c); err != nil { + collection := c.Value(s) s.logger.Error("Failed to uninstall collection", - zap.String("Collection ID", c.collection.ID), - zap.String("Collection Name", c.collection.Name), + zap.String("Collection ID", collection.ID), + zap.String("Collection Name", collection.Name), 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 } -func (s *Setup) UninstallCollection(ctx context.Context, c CollectionConfig) error { - ok, err := c.DeleteFn(s, c.collection) - if err != nil { - return err - } - 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()) { +func (s *Setup) UninstallCollection(ctx context.Context, c *Collection) error { + collection := c.Value(s) + deleteCollection := c.DeleteFunc(s, collection) + if deleteCollection { + if err := s.content.Collections.Delete(ctx, s.SpaceID, s.EnvironmentID, collection.ID); err != nil && !strings.Contains(err.Error(), collections.ErrNotFound.Error()) { return err } - s.removeItems(c.collection.ID) // после удаления коллекции нет смысла удалять ее элементы + + // TODO: Проверить, в чем смысл происходящего + //s.removeItems(c.collection.ID) // после удаления коллекции нет смысла удалять ее элементы } return nil } diff --git a/pkg/setup/collection_test.go b/pkg/setup/collection_test.go index 788314ca8aa15aa9e4bba63ae6c090f0e5662e69..f1d9e4da1182938249192f8ef410ceecc81c743a 100644 --- a/pkg/setup/collection_test.go +++ b/pkg/setup/collection_test.go @@ -4,20 +4,69 @@ import ( "context" "testing" - "git.perx.ru/perxis/perxis-go/pkg/collections" 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/environments" envmocks "git.perx.ru/perxis/perxis-go/pkg/environments/mocks" "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/field" "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 { name string collections []*collections.Collection @@ -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())}}, collectionsCall: func(svc *mockscollections.Collections) { 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) { assert.Error(t, err) @@ -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) - }) - } -} diff --git a/pkg/setup/config.go b/pkg/setup/config.go index bdf47aed1cf76ec5d0bc83f08f19aa12cad9f073..d7f68a90409ab338f8ff191e2f91bf80ddc0b130 100644 --- a/pkg/setup/config.go +++ b/pkg/setup/config.go @@ -1,29 +1,32 @@ package setup import ( - "errors" "io/fs" - "git.perx.ru/perxis/perxis-go" - - "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" + "github.com/pkg/errors" ) type Config struct { - Roles []RoleConfig - Clients []ClientConfig - Collections []CollectionConfig - Items []ItemConfig + Collections Collections + Items Items + Roles Roles + Clients Clients } func NewConfig() *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 из файловой системы // Файлы должны быть расположены в директории со следующей структурой: // - collections/ - директория с файлами конфигурации коллекций @@ -31,26 +34,30 @@ func NewConfig() *Config { // - items/ - директория с файлами конфигурации элементов // - roles/ - директория с файлами конфигурации ролей 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 _, 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 } } 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 } } 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 } } 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 } } @@ -66,240 +73,210 @@ func (cfg *Config) MustLoad(fsys fs.FS) *Config { 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 загружает элементы из указанной файловой системы -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 -} +//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 ...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 +//} diff --git a/pkg/setup/config_test.go b/pkg/setup/config_test.go index 7e5c158929e7c5eaac92c13842828325f54d583a..5e7f114c4f04350e2389e8ee9b72db37adae5a63 100644 --- a/pkg/setup/config_test.go +++ b/pkg/setup/config_test.go @@ -1,71 +1,20 @@ package setup import ( + "os" "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" ) -func TestConfig_WithCollectionOptions(t *testing.T) { - 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"}}, - }} +// TODO - // применяем опцию OverwriteCollection к конфигурации и дополнительно добавляем фильтр (опция применится только к коллекции с id 1) - config.WithCollectionOptions(func(c *collections.Collection) bool { return c.ID == "1" }, OverwriteCollection()) - assert.NotNil(t, config.Collections[0].UpdateFn, "должна быть выполнена OverwriteCollection и установлена UpdateFn для коллекции ID:1") - assert.Nil(t, config.Collections[1].UpdateFn) - }) -} - -func TestConfig_WithItemsOptions(t *testing.T) { - 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) - }) +func TestConfig_Load(t *testing.T) { + cfg := NewConfig() + cfg, err := cfg.Load(os.DirFS("../../assets/tests/setup")) + 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") + assert.Equal(t, cfg.Collections[2].Value(nil).ID, "collection_c") } diff --git a/pkg/setup/entity.go b/pkg/setup/entity.go new file mode 100644 index 0000000000000000000000000000000000000000..69cd2c8d5ef4224bbc270c0c8cbb8df1fdfa1bf1 --- /dev/null +++ b/pkg/setup/entity.go @@ -0,0 +1,179 @@ +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() } + } +} diff --git a/pkg/setup/entity_test.go b/pkg/setup/entity_test.go new file mode 100644 index 0000000000000000000000000000000000000000..fd02d186a89504001954b4eeb041a3a21a1c64ce --- /dev/null +++ b/pkg/setup/entity_test.go @@ -0,0 +1,47 @@ +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) +} diff --git a/pkg/setup/item.go b/pkg/setup/item.go index af08ea951ad51da0a7cb7057191288d4997faea4..adf79935a7e55c212687907f0118112adfcc97ff 100644 --- a/pkg/setup/item.go +++ b/pkg/setup/item.go @@ -5,10 +5,11 @@ import ( "reflect" "strings" + "go.uber.org/zap" + "git.perx.ru/perxis/perxis-go/pkg/collections" "git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/items" - "go.uber.org/zap" ) var ( @@ -18,59 +19,60 @@ var ( ErrItemsNotFound = errors.New("item not found") ) -type ItemsOption func(c *ItemConfig) -type PublishItemFn func(s *Setup, item *items.Item) (*items.Item, bool) -type UpdateItemFn func(s *Setup, exist, new *items.Item) (*items.Item, bool) -type DeleteItemFn func(s *Setup, col *items.Item) bool - -type ItemConfig struct { - item *items.Item - PublishFn PublishItemFn - UpdateFn UpdateItemFn - DeleteFn DeleteItemFn - - // Если запись загружена из файла, необходимо выполнить Decode перед установкой - encoded bool -} +type ( + Item = Entity[ItemConf, *items.Item] + Items = EntityList[ItemConf, *items.Item] + ItemOption = EntityOption[ItemConf, *items.Item] -func NewItemConfig(item *items.Item, opt ...ItemsOption) ItemConfig { - c := ItemConfig{item: item} - - PublishItem()(&c) - KeepExistingItem()(&c) - DeleteItemIfRemove()(&c) - - for _, o := range opt { - o(&c) + ItemConf struct { + PublishFunc func(s *Setup, item *items.Item) (*items.Item, bool) + encoded bool // Если запись загружена из файла, необходимо выполнить Decode перед установкой } +) - return c +var ( + NewItem = NewEntity[ItemConf, *items.Item] + OverwriteItem = Overwrite[ItemConf, *items.Item] + KeepExistingItem = Keep[ItemConf, *items.Item] + DeleteItem = Delete[ItemConf, *items.Item] + DeleteItemIfRemoveFlag = DeleteIfRemoveFlag[ItemConf, *items.Item] +) + +func (ItemConf) Init(e *Entity[ItemConf, *items.Item]) { + PublishItem()(e) + KeepExistingItem()(e) + DeleteItemIfRemoveFlag()(e) } -func PrepareItems(handler func(*items.Item)) ItemsOption { - return func(c *ItemConfig) { - handler(c.item) +// OverwriteItem перезаписывает элемент +func PublishItem() ItemOption { + return func(c *Item) { + c.Conf.PublishFunc = func(s *Setup, item *items.Item) (*items.Item, bool) { return item, true } } } -func OverwriteItem() ItemsOption { - return func(c *ItemConfig) { - c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) { return new, true } +// DraftItem не публикует элемент, сохраняет его в черновике +func DraftItem() ItemOption { + return func(c *Item) { + c.Conf.PublishFunc = func(s *Setup, item *items.Item) (*items.Item, bool) { return item, false } } } -func DecodeItem() ItemsOption { - return func(c *ItemConfig) { - c.encoded = true +// DecodeItem декодирует элемент перед установкой +func DecodeItem() ItemOption { + return func(c *Item) { + c.Conf.encoded = true } } -func OverwriteFields(fields ...string) ItemsOption { - return func(c *ItemConfig) { - c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) { - +// OverwriteFields разрешает перезаписывать указанные поля элемента, в противном случае считается что элемент не изменился +func OverwriteFields(fields ...string) ItemOption { + return func(c *Item) { + c.UpdateFunc = func(s *Setup, old, new *items.Item) (*items.Item, bool) { var changed bool + for _, field := range fields { + // Пропускаем системные поля if items.IsSystemField(field) { continue } @@ -79,8 +81,8 @@ func OverwriteFields(fields ...string) ItemsOption { if err != nil { continue } - oldValue, err := old.Get(field) + if err != nil || newValue != oldValue { changed = true if err = old.Set(field, newValue); err != nil { @@ -94,9 +96,10 @@ func OverwriteFields(fields ...string) ItemsOption { } } -func KeepFields(fields ...string) ItemsOption { - return func(c *ItemConfig) { - c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) { +// KeepFields сохраняет указанные поля элемента +func KeepFields(fields ...string) ItemOption { + return func(c *Item) { + c.UpdateFunc = func(s *Setup, old, new *items.Item) (*items.Item, bool) { for _, field := range fields { if items.IsSystemField(field) { @@ -121,50 +124,74 @@ func KeepFields(fields ...string) ItemsOption { } } -func KeepExistingItem() ItemsOption { - return func(c *ItemConfig) { - c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) { return old, false } - } -} +// +// Item Setup +// -func DeleteItem() ItemsOption { - return func(c *ItemConfig) { - c.DeleteFn = func(s *Setup, item *items.Item) bool { return true } - } +// AddItem добавляет требования к настройке элементов в пространстве +func (s *Setup) AddItem(item *items.Item, opt ...ItemOption) *Setup { + s.Config.Items.Add(item, opt...) + return s } -func DeleteItemIfRemove() ItemsOption { - return func(c *ItemConfig) { - c.DeleteFn = func(s *Setup, item *items.Item) bool { return s.IsRemove() } - } +// AddItems добавляет требования к настройке элементов в пространстве +func (s *Setup) AddItems(items []*items.Item, opt ...ItemOption) *Setup { + s.Config.Items.AddMany(items, opt...) + return s } -func PublishItem() ItemsOption { - return func(c *ItemConfig) { - c.PublishFn = func(s *Setup, item *items.Item) (*items.Item, bool) { return item, true } +// InstallItem настраивает элемент +func (s *Setup) InstallItem(ctx context.Context, exists map[string]*items.Item, c *Item) error { + 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 KeepDraft() ItemsOption { - return func(c *ItemConfig) { - c.PublishFn = func(s *Setup, item *items.Item) (*items.Item, bool) { return item, false } + // Если элемент существует, обновляем его + if item, changed := c.UpdateFunc(s, exist, item); changed { + 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 { + return errors.Wrap(err, "unpublish item") + } + return nil } + + return nil } +// InstallItems устанавливает все элементы func (s *Setup) InstallItems(ctx context.Context) (err error) { if len(s.Items) == 0 { 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 + for i, c := range itms { - if !c.encoded { + // Пропускаем элементы, которые не требуют декодирования + if !c.Conf.encoded { continue } + // Получаем коллекцию и схему для декодирования элемента if coll == nil { coll, err = s.content.Collections.Get(ctx, s.SpaceID, s.EnvironmentID, collID) if err != nil { @@ -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 { return err } - itms[i] = ItemConfig{ - item: decoded, - PublishFn: c.PublishFn, - UpdateFn: c.UpdateFn, - DeleteFn: c.DeleteFn, - encoded: false, - } + + itms[i].value = decoded } - exists, err := s.getExisting(ctx, collID, itms) + // Получаем существующие элементы + exists, err := s.getItems(ctx, collID, itms) if err != nil { return err } + + // Устанавливаем элементы for _, c := range itms { if err := s.InstallItem(ctx, exists, c); err != nil { - s.logger.Error("Failed to install item", - zap.String("ID", c.item.ID), - zap.String("Collection", c.item.CollectionID), - zap.Error(err), - ) - return errors.WithDetailf(errors.Wrap(err, "failed to install item"), "Возникла ошибка при добавлении элемента %s(%s)", c.item.ID, c.item.CollectionID) + item := c.Value(s) + s.logger.Error("Failed to install item", zap.String("ID", item.ID), + zap.String("Collection", item.CollectionID), zap.Error(err)) + return errors.WithDetailf(errors.Wrap(err, "failed to install item"), + "Возникла ошибка при добавлении элемента %s(%s)", item.ID, item.CollectionID) } } } @@ -204,35 +229,15 @@ func (s *Setup) InstallItems(ctx context.Context) (err error) { return nil } -func (s *Setup) InstallItem(ctx context.Context, exists map[string]*items.Item, c ItemConfig) error { - item := c.item - item.SpaceID, item.EnvID = s.SpaceID, s.EnvironmentID - - exist, ok := exists[item.ID] - if !ok { - if item, publish := c.PublishFn(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 - } - - 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") +// UninstallItem удаляет элемент +func (s *Setup) UninstallItem(ctx context.Context, c *Item) error { + item := c.Value(s) + if c.DeleteFunc(s, item) { + err := s.content.Items.Delete(ctx, &items.Item{SpaceID: s.SpaceID, EnvID: s.EnvironmentID, CollectionID: item.CollectionID, ID: item.ID}) + if err != nil && !strings.Contains(err.Error(), items.ErrNotFound.Error()) { + return err } - return nil - } - return nil } @@ -245,28 +250,18 @@ func (s *Setup) UninstallItems(ctx context.Context) error { for _, c := range s.Items { if err := s.UninstallItem(ctx, c); err != nil { - s.logger.Error("Failed to uninstall item", - zap.String("Item", c.item.ID), - zap.String("Item", c.item.CollectionID), - zap.Error(err), + item := c.Value(s) + s.logger.Error("Failed to uninstall item", zap.String("Item", item.ID), + zap.String("Item", item.CollectionID), 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 -} - func (s *Setup) CheckItems(ctx context.Context) error { if len(s.Items) == 0 { return nil @@ -275,15 +270,17 @@ func (s *Setup) CheckItems(ctx context.Context) error { var errs []error s.logger.Debug("Check items", zap.Int("Items", len(s.Items))) - for col, itms := range s.groupByCollection() { - exists, err := s.getExisting(ctx, col, itms) + for col, itms := range groupByCollection(s.Items) { + exists, err := s.getItems(ctx, col, itms) if err != nil { return err } for _, c := range itms { - if _, ok := exists[c.item.ID]; !ok { - errs = append(errs, errors.WithDetailf(errors.New("not found"), "Не найден элемент %s(%s)", c.item.ID, c.item.CollectionID)) + item := c.Value(s) + 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 { return nil } -func (s *Setup) removeItems(collID string) { - itms := make([]ItemConfig, 0, len(s.Items)) - for _, i := range s.Items { - if i.item.CollectionID != collID { - itms = append(itms, i) - } - } - s.Items = itms -} - -func (s *Setup) groupByCollection() map[string][]ItemConfig { - itemsByColl := map[string][]ItemConfig{} - for _, i := range s.Items { - cfg, ok := itemsByColl[i.item.CollectionID] +// +//func (s *Setup) removeItems(collID string) { +// itms := make([]ItemConfig, 0, len(s.Items)) +// for _, i := range s.Items { +// if i.item.CollectionID != collID { +// itms = append(itms, i) +// } +// } +// s.Items = itms +//} + +func groupByCollection(itms Items) map[string]Items { + res := map[string]Items{} + for _, conf := range itms { + collectionID := conf.value.CollectionID + l, ok := res[collectionID] if !ok { - itemsByColl[i.item.CollectionID] = []ItemConfig{i} - continue + res[collectionID] = make(Items, 0, 1) } - 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( ctx, s.SpaceID, s.EnvironmentID, collID, - &items.Filter{ID: getItemIds(configs)}, + &items.Filter{ID: confs.GetIDs()}, &items.FindOptions{Regular: true, Hidden: true, Templates: true}, ) + if err != nil { - s.logger.Error("Failed to find existing items", - zap.String("Collection", collID), - zap.Error(err), - ) - return nil, errors.WithDetailf(errors.Wrap(err, "failed to find existing items"), "Возникла ошибка при поиске элементов в коллекции %s", collID) + s.logger.Error("Failed to find existing items", zap.String("Collection", collID), zap.Error(err)) + return nil, errors.WithDetailf(errors.Wrap(err, "failed to find existing items"), + "Возникла ошибка при получении элементов в коллекции %s", collID) } exists := make(map[string]*items.Item, len(itms)) @@ -341,11 +338,3 @@ func (s *Setup) getExisting(ctx context.Context, collID string, configs []ItemCo } return exists, nil } - -func getItemIds(items []ItemConfig) []string { - var ids []string - for _, i := range items { - ids = append(ids, i.item.ID) - } - return ids -} diff --git a/pkg/setup/item_test.go b/pkg/setup/item_test.go index 2d2e1a7705a80be6d4eb783af5ad81daf431e886..0a128625e7871d20aad0bceff7561197e97c01ea 100644 --- a/pkg/setup/item_test.go +++ b/pkg/setup/item_test.go @@ -6,10 +6,11 @@ import ( "git.perx.ru/perxis/perxis-go/pkg/content" "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" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + + "git.perx.ru/perxis/perxis-go/pkg/items" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -82,17 +83,16 @@ func TestItem_OverwriteFields(t *testing.T) { changed: false, }, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - c := ItemConfig{item: test.new} - OverwriteFields(test.fields...)(&c) - - got, changed := c.UpdateFn(nil, test.old, test.new) - require.Equal(t, test.changed, changed) - if !test.changed { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewItem(tt.new) + OverwriteFields(tt.fields...)(c) + got, changed := c.UpdateFunc(nil, tt.old, tt.new) + require.Equal(t, tt.changed, changed) + if !tt.changed { return } - assert.Equal(t, test.want, got) + assert.Equal(t, tt.want, got) }) } @@ -172,24 +172,22 @@ func TestItem_KeepFields(t *testing.T) { changed: true, }, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - c := ItemConfig{item: test.new} - KeepFields(test.fields...)(&c) - - got, changed := c.UpdateFn(nil, test.old, test.new) - require.Equal(t, test.changed, changed) - if !test.changed { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewItem(tt.new) + KeepFields(tt.fields...)(c) + got, changed := c.UpdateFunc(nil, tt.old, tt.new) + require.Equal(t, tt.changed, changed) + if !tt.changed { return } - assert.Equal(t, test.want, got) + assert.Equal(t, tt.want, got) }) } } func TestSetup_InstallItems(t *testing.T) { - tests := []struct { name string items []*items.Item @@ -279,7 +277,6 @@ func TestSetup_InstallItems(t *testing.T) { } func TestSetup_CreateDraft(t *testing.T) { - tests := []struct { name string items []*items.Item @@ -322,14 +319,13 @@ func TestSetup_CreateDraft(t *testing.T) { } 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())) }) } } func TestSetup_UpdateDraft(t *testing.T) { - tests := []struct { name string items []*items.Item @@ -358,7 +354,7 @@ func TestSetup_UpdateDraft(t *testing.T) { } 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())) }) } diff --git a/pkg/setup/role.go b/pkg/setup/role.go index ac6e641a97c6160cf29c5353f0176ef110a9bab3..2498e8456c474331ce648dbf48caaccf16718bb3 100644 --- a/pkg/setup/role.go +++ b/pkg/setup/role.go @@ -4,11 +4,12 @@ import ( "context" "strings" + "go.uber.org/zap" + "git.perx.ru/perxis/perxis-go/pkg/data" "git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/permission" "git.perx.ru/perxis/perxis-go/pkg/roles" - "go.uber.org/zap" ) var ( @@ -17,67 +18,28 @@ var ( ErrUninstallRoles = errors.New("failed to uninstall role") ) -type RolesOption func(c *RoleConfig) -type UpdateRoleFn func(s *Setup, exist, new *roles.Role) (*roles.Role, bool) -type DeleteRoleFn func(s *Setup, role *roles.Role) bool - -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 { - return func(c *RoleConfig) { - c.UpdateFn = func(s *Setup, old, new *roles.Role) (*roles.Role, bool) { return old, false } - } -} - -func DeleteRole() RolesOption { - return func(c *RoleConfig) { - c.DeleteFn = func(s *Setup, role *roles.Role) bool { return true } - } -} +type Role = Entity[RoleConf, *roles.Role] +type Roles = EntityList[RoleConf, *roles.Role] +type RoleOption = EntityOption[RoleConf, *roles.Role] +type RoleConf struct{} -func DeleteRoleIfRemove() RolesOption { - return func(c *RoleConfig) { - c.DeleteFn = func(s *Setup, role *roles.Role) bool { return s.IsRemove() } - } +// Init инициализирует конфигурацию коллекции +func (RoleConf) Init(e *Entity[RoleConf, *roles.Role]) { + DefaultRoleUpdateFunc()(e) + DeleteRoleIfRemoveFlag()(e) } -func UpdateExistingRole() RolesOption { - return func(c *RoleConfig) { - c.UpdateFn = func(s *Setup, exist, new *roles.Role) (*roles.Role, bool) { +var ( + NewRole = NewEntity[RoleConf, *roles.Role] + OverwriteRole = Overwrite[RoleConf, *roles.Role] + KeepExistingRole = Keep[RoleConf, *roles.Role] + DeleteRole = Delete[RoleConf, *roles.Role] + DeleteRoleIfRemoveFlag = DeleteIfRemoveFlag[RoleConf, *roles.Role] +) - // если передан флаг force, то обновляем все поля роли на переданные +func DefaultRoleUpdateFunc() RoleOption { + return func(c *Role) { + c.UpdateFunc = func(s *Setup, exist, new *roles.Role) (*roles.Role, bool) { if s.IsForce() { return new, true } @@ -105,68 +67,74 @@ func UpdateExistingRole() RolesOption { } } -func (s *Setup) InstallRoles(ctx context.Context) error { - if len(s.Roles) == 0 { - return nil - } - - s.logger.Debug("Install role", zap.String("Space ID", s.SpaceID), zap.Int("Roles", len(s.Roles))) +// +// Role Setup +// - for _, c := range s.Roles { - if err := s.InstallRole(ctx, c); err != nil { - s.logger.Error("Failed to install role", zap.String("Role ID", c.role.ID), zap.Error(err)) - return errors.Wrap(errors.WithDetailf(err, "Возникла ошибка при настройке роли %s(%s)", c.role.ID, c.role.Description), "failed to install role") - } - } +// AddRoles добавляет требования к настройке ролей в пространстве +func (s *Setup) AddRoles(roles []*roles.Role, opt ...RoleOption) *Setup { + s.Config.Roles.AddMany(roles, opt...) + 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 { - role := c.role +// InstallRole устанавливает роль +func (s *Setup) InstallRole(ctx context.Context, c *Role) error { + role := c.Value(s) 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) } exist, err := s.content.Roles.Get(ctx, s.SpaceID, role.ID) + + // Если роль не найдена, создаем новую if err != nil { if !strings.Contains(err.Error(), roles.ErrNotFound.Error()) { return err } - _, err = s.content.Roles.Create(ctx, role) 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 nil } -func (s *Setup) UninstallRoles(ctx context.Context) error { +// InstallRoles устанавливает все роли +func (s *Setup) InstallRoles(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))) + s.logger.Debug("Install roles", 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 { - s.logger.Error("Failed to uninstall role", zap.String("Role ID", c.role.ID), zap.Error(err)) - return errors.WithDetailf(errors.Wrap(err, "failed to uninstall role"), "Возникла ошибка при удалении роли %s(%s)", c.role.ID, c.role.Description) + if err := s.InstallRole(ctx, c); err != nil { + role := c.Value(s) + 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 } -func (s *Setup) UninstallRole(ctx context.Context, c RoleConfig) error { - if c.DeleteFn(s, c.role) { - err := s.content.Roles.Delete(ctx, s.SpaceID, c.role.ID) +// UninstallRole удаляет роль +func (s *Setup) UninstallRole(ctx context.Context, c *Role) error { + 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()) { return err } @@ -174,6 +142,34 @@ func (s *Setup) UninstallRole(ctx context.Context, c RoleConfig) error { 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 { if len(s.Roles) == 0 { return nil @@ -183,8 +179,9 @@ func (s *Setup) CheckRoles(ctx context.Context) error { var errs []error for _, c := range s.Roles { - if err := s.CheckRole(ctx, c.role); err != nil { - errs = append(errs, errors.WithDetailf(err, "Не найдена роль %s(%s)", c.role.ID, c.role.Description)) + if err := s.CheckRole(ctx, c); err != nil { + 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 { 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 -} diff --git a/pkg/setup/setup.go b/pkg/setup/setup.go index 9b6473a556e063518a78bf4e30ac1f844c658991..8855269638d1901b553ee4a6ecc6ed2c488f3d17 100644 --- a/pkg/setup/setup.go +++ b/pkg/setup/setup.go @@ -4,12 +4,8 @@ import ( "context" "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/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" "go.uber.org/zap" ) @@ -52,13 +48,20 @@ func NewSetup(content *content.Content, spaceID, environmentID string, logger *z content: content, logger: logger, 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.Config = cfg + setup.Config = config.Clone() return &setup } @@ -171,7 +174,6 @@ func (s *Setup) Uninstall(ctx context.Context) error { return nil } -// Deprecated: use Config func (s *Setup) Load(fsys fs.FS) *Setup { var err error s.Config, err = s.Config.Load(fsys) @@ -180,63 +182,3 @@ func (s *Setup) Load(fsys fs.FS) *Setup { } 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 -} diff --git a/pkg/setup/setup_test.go b/pkg/setup/setup_test.go index 604c2f6b29bae32756e2149ca572ce3b54920936..ff73b745deb0fcec807fa0c0f28b2ba5a0dd7258 100644 --- a/pkg/setup/setup_test.go +++ b/pkg/setup/setup_test.go @@ -2,26 +2,27 @@ package setup import ( "context" + "errors" "testing" - "git.perx.ru/perxis/perxis-go/pkg/clients" 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" - "git.perx.ru/perxis/perxis-go/pkg/content" "git.perx.ru/perxis/perxis-go/pkg/data" 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" - "git.perx.ru/perxis/perxis-go/pkg/roles" 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/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "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" ) @@ -78,13 +79,14 @@ func getActions() []*items.Item { } func newSetup(content *content.Content, t *testing.T) *Setup { - logger := zaptest.NewLogger(t, zaptest.WrapOptions()) - setup := NewSetup(content, spaceID, envID, logger) - setup.AddCollections(getCollections()) - setup.AddRoles(getRoles()) - setup.AddClients(getClients()) - setup.AddItems(getActions(), OverwriteItem()) + config := NewConfig() + config.Collections.AddMany(getCollections()) + config.Roles.AddMany(getRoles()) + config.Clients.AddMany(getClients()) + config.Items.AddMany(getActions(), OverwriteItem()) + logger := zaptest.NewLogger(t, zaptest.WrapOptions()) + setup := NewSetup(content, spaceID, envID, logger).WithConfig(config) return setup } @@ -101,10 +103,8 @@ func TestSetupInstall(t *testing.T) { spcMock.On("Get", mock.Anything, mock.Anything).Return(sps, nil).Once() setup := NewSetup(&content.Content{Spaces: spcMock}, spaceID, envID, logger) err := setup.Install(context.Background()) - require.NoError(t, err) spcMock.AssertExpectations(t) - }) t.Run("Success, no force", func(t *testing.T) { @@ -116,23 +116,19 @@ func TestSetupInstall(t *testing.T) { collsMock := &collectionMock.Collections{} for _, collection := range getCollections() { collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID). - Return(nil, collections.ErrNotFound). - Once() + Return(nil, collections.ErrNotFound).Once() collsMock.On("Create", mock.Anything, collection). - Return(collection, nil). - Once() + Return(collection, nil).Once() collsMock.On("SetSchema", mock.Anything, spaceID, envID, collection.ID, collection.Schema). - Return(nil). - Once() + Return(nil).Once() } rMock := &rolesMock.Roles{} for _, role := range getRoles() { rMock.On("Get", mock.Anything, spaceID, role.ID). - Return(nil, roles.ErrNotFound). - Once() + Return(nil, roles.ErrNotFound).Once() rMock.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { create := args[1].(*roles.Role)