Skip to content
Snippets Groups Projects
Commit 31581af8 authored by Semyon Krestyaninov's avatar Semyon Krestyaninov :dog2:
Browse files

Merge branch 'master' into feature/PRXS-3218-HideTemplateActions

parents 82a3dcf5 2fcf77f2
No related branches found
No related tags found
No related merge requests found
......@@ -101,16 +101,27 @@ type Collection struct {
}
// GetID возвращает идентификатор коллекции
func (c Collection) GetID() string {
func (c *Collection) GetID() string {
return c.ID
}
func (c Collection) GetSpaceID() string {
func (c *Collection) GetSpaceID() string {
return c.SpaceID
}
// Equal сравнивает две коллекции, за исключением Schema, Access, StateInfo и Config
func (c Collection) Equal(other *Collection) bool {
// Deprecated: используйте метод IsEqual
func (c *Collection) Equal(other *Collection) bool {
return c.IsEqual(other)
}
func (c *Collection) IsEqual(other *Collection) bool {
if c == other {
return true
}
if c == nil || other == nil {
return false
}
if c.ID != other.ID ||
c.SpaceID != other.SpaceID ||
c.EnvID != other.EnvID ||
......@@ -132,6 +143,10 @@ func (c Collection) Equal(other *Collection) bool {
return true
}
func IsEqual(c1, c2 *Collection) bool {
return c1.IsEqual(c2)
}
type View struct {
SpaceID string `json:"space_id" bson:"space_id"` // SpaceID оригинальной коллекции
EnvID string `json:"environment_id" bson:"environment_id"` // EnvID оригинальной коллекции
......@@ -185,7 +200,7 @@ const (
StateChanged
)
func (c Collection) Clone() *Collection {
func (c *Collection) Clone() *Collection {
clone := &Collection{
ID: c.ID,
SpaceID: c.SpaceID,
......@@ -294,3 +309,116 @@ func RuleFromAccess(access *Access) *permission.Rule {
Hidden: access.Hidden,
}
}
// Merge объединяет две коллекции, возвращая новую коллекцию
// с объединенными данными. Если данные второй коллекции не пустые,
// то данные из второй коллекции перезаписывают данные из первой.
func (c *Collection) Merge(other *Collection) *Collection {
if c == nil {
if other == nil {
return nil
}
return other.Clone()
}
if other == nil {
return c.Clone()
}
merged := c.Clone()
merged.mergeScalarFields(other)
merged.mergePointerFields(other)
merged.mergeComplexFields(other)
return merged
}
// mergeScalarFields копирует скалярные поля из other коллекции в текущую коллекцию.
func (c *Collection) mergeScalarFields(other *Collection) {
if other.ID != "" {
c.ID = other.ID
}
if other.Name != "" {
c.Name = other.Name
}
if other.SpaceID != "" {
c.SpaceID = other.SpaceID
}
if other.EnvID != "" {
c.EnvID = other.EnvID
}
if other.Hidden {
c.Hidden = other.Hidden
}
if other.NoPublish {
c.NoPublish = other.NoPublish
}
if other.NoArchive {
c.NoArchive = other.NoArchive
}
if other.NoRevisions {
c.NoRevisions = other.NoRevisions
}
if other.MaxRevisions > 0 {
c.MaxRevisions = other.MaxRevisions
}
if other.RevisionTTL > 0 {
c.RevisionTTL = other.RevisionTTL
}
}
// mergePointerFields копирует указатели из other коллекции в текущую коллекцию.
func (c *Collection) mergePointerFields(other *Collection) {
if other.NoData != nil {
c.NoData = other.NoData
}
if other.Single != nil {
single := *other.Single
c.Single = &single
}
if other.System != nil {
system := *other.System
c.System = &system
}
if other.StateInfo != nil {
info := *other.StateInfo
c.StateInfo = &info
}
if other.View != nil {
view := *other.View
c.View = &view
}
if other.Config != nil {
cfg := *other.Config
c.Config = &cfg
}
}
// mergeComplexFields копирует сложные поля из other коллекции в текущую коллекцию.
func (c *Collection) mergeComplexFields(other *Collection) {
if other.Schema != nil {
c.Schema = other.Schema.Clone(false)
}
if other.Access != nil {
c.Access = other.Access.Clone()
}
if other.Tags != nil {
tags := make([]string, len(other.Tags))
copy(tags, other.Tags)
c.Tags = tags
}
if len(other.Translations) > 0 {
translations := make(map[string]map[string]string, len(other.Translations))
for k, v := range other.Translations {
translations[k] = maps.Clone(v)
}
c.Translations = translations
}
}
// Merge объединяет две коллекции, возвращая новую коллекцию
// с объединенными данными. Если данные второй коллекции не пустые,
// то данные из второй коллекции перезаписывают данные из первой.
func Merge(c1, c2 *Collection) *Collection {
return c1.Merge(c2)
}
......@@ -2,7 +2,9 @@ package collections
import (
"testing"
"time"
"git.perx.ru/perxis/perxis-go/pkg/schema"
"github.com/stretchr/testify/require"
)
......@@ -100,3 +102,225 @@ func TestView_Equal(t *testing.T) {
})
}
}
func Test_Merge(t *testing.T) {
boolTrue := true
boolFalse := false
// Create schema objects with Title field
schema1 := schema.New()
schema1.Title = "schema1"
schema2 := schema.New()
schema2.Title = "schema2"
testCases := []struct {
name string
c1 *Collection
c2 *Collection
want *Collection
}{
{
name: "Both collections are nil",
c1: nil,
c2: nil,
want: nil,
},
{
name: "First collection is nil",
c1: nil,
c2: &Collection{
ID: "id2",
Name: "name2",
},
want: &Collection{
ID: "id2",
Name: "name2",
},
},
{
name: "Second collection is nil",
c1: &Collection{
ID: "id1",
Name: "name1",
},
c2: nil,
want: &Collection{
ID: "id1",
Name: "name1",
},
},
{
name: "Merge primitive fields",
c1: &Collection{
ID: "id1",
SpaceID: "space1",
EnvID: "env1",
Name: "name1",
Hidden: false,
NoPublish: false,
NoArchive: false,
NoRevisions: false,
MaxRevisions: 5,
RevisionTTL: time.Hour,
},
c2: &Collection{
ID: "id2",
SpaceID: "space2",
EnvID: "env2",
Name: "name2",
Hidden: true,
NoPublish: true,
NoArchive: true,
NoRevisions: true,
MaxRevisions: 10,
RevisionTTL: time.Hour * 2,
},
want: &Collection{
ID: "id2",
SpaceID: "space2",
EnvID: "env2",
Name: "name2",
Hidden: true,
NoPublish: true,
NoArchive: true,
NoRevisions: true,
MaxRevisions: 10,
RevisionTTL: time.Hour * 2,
},
},
{
name: "Merge pointer fields",
c1: &Collection{
NoData: &boolFalse,
Single: &boolFalse,
System: &boolFalse,
},
c2: &Collection{
NoData: &boolTrue,
Single: &boolTrue,
System: &boolTrue,
},
want: &Collection{
NoData: &boolTrue,
Single: &boolTrue,
System: &boolTrue,
},
},
{
name: "Merge complex fields",
c1: &Collection{
Schema: schema1,
View: &View{
SpaceID: "space1",
EnvID: "env1",
CollectionID: "collection1",
Filter: "filter1",
},
Tags: []string{"tag1", "tag2"},
Translations: map[string]map[string]string{
"en": {
"key1": "value1",
},
},
},
c2: &Collection{
Schema: schema2,
View: &View{
SpaceID: "space2",
EnvID: "env2",
CollectionID: "collection2",
Filter: "filter2",
},
Tags: []string{"tag3", "tag4"},
Translations: map[string]map[string]string{
"fr": {
"key2": "value2",
},
},
},
want: &Collection{
Schema: schema2,
View: &View{
SpaceID: "space2",
EnvID: "env2",
CollectionID: "collection2",
Filter: "filter2",
},
Tags: []string{"tag3", "tag4"},
Translations: map[string]map[string]string{
"fr": {
"key2": "value2",
},
},
},
},
{
name: "Merge with empty second collection",
c1: &Collection{
ID: "id1",
SpaceID: "space1",
EnvID: "env1",
Name: "name1",
NoData: &boolTrue,
Single: &boolTrue,
System: &boolTrue,
Schema: schema1,
Tags: []string{"tag1", "tag2"},
},
c2: &Collection{},
want: &Collection{
ID: "id1",
SpaceID: "space1",
EnvID: "env1",
Name: "name1",
NoData: &boolTrue,
Single: &boolTrue,
System: &boolTrue,
Schema: schema1,
Tags: []string{"tag1", "tag2"},
},
},
{
name: "Merge with partial second collection",
c1: &Collection{
ID: "id1",
SpaceID: "space1",
EnvID: "env1",
Name: "name1",
NoData: &boolFalse,
Single: &boolFalse,
System: &boolFalse,
Schema: schema1,
Tags: []string{"tag1", "tag2"},
},
c2: &Collection{
Name: "name2",
NoData: &boolTrue,
Schema: schema2,
},
want: &Collection{
ID: "id1",
SpaceID: "space1",
EnvID: "env1",
Name: "name2",
NoData: &boolTrue,
Single: &boolFalse,
System: &boolFalse,
Schema: schema2,
Tags: []string{"tag1", "tag2"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := Merge(tc.c1, tc.c2)
require.True(t, IsEqual(result, tc.want))
if result != nil && tc.want != nil {
require.True(t, schema.IsEqual(result.Schema, tc.want.Schema))
}
})
}
}
......@@ -11,6 +11,7 @@ import (
"git.perx.ru/perxis/perxis-go/pkg/schema"
"git.perx.ru/perxis/perxis-go/pkg/schema/field"
"git.perx.ru/perxis/perxis-go/pkg/schema/localizer"
"git.perx.ru/perxis/perxis-go/pkg/schema/walk"
pb "git.perx.ru/perxis/perxis-go/proto/items"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
......@@ -662,3 +663,31 @@ func GetItemIDs(arr []*Item) []string {
}
return res
}
func MergeItemData(ctx context.Context, sch *schema.Schema, origData, updData map[string]any) (map[string]any, error) {
if origData == nil {
return updData, nil
}
w := walk.NewWalker(sch, &walk.WalkConfig{})
w.DefaultFn = func(c *walk.WalkContext) error {
if c.Src == nil || c.Dst != nil {
return nil
}
c.Dst = c.Src
c.Changed = true
return nil
}
res, _, err := w.DataWalk(ctx, updData, origData)
if err != nil {
return nil, err
}
v, ok := res.(map[string]any)
if !ok {
return nil, fmt.Errorf("expected map[string]interface{}, got %[1]T, %[1]v", res)
}
return v, nil
}
......@@ -264,3 +264,245 @@ func TestItem_Encode_Decode(t *testing.T) {
})
}
}
func Test_mergeItemData(t *testing.T) {
tests := []struct {
name string
schema *schema.Schema
origData map[string]any
updData map[string]any
want map[string]interface{}
wantErr bool
}{
{
name: "merge with non-nil original data",
schema: schema.New(
"field1", field.String(),
"field2", field.String(),
"field3", field.String(),
),
origData: map[string]interface{}{
"field1": "value1",
"field2": "value2",
},
updData: map[string]interface{}{
"field2": "new_value2",
"field3": "value3",
},
want: map[string]interface{}{
"field1": "value1",
"field2": "new_value2",
"field3": "value3",
},
wantErr: false,
},
{
name: "merge with nil original data",
schema: schema.New(
"field1", field.String(),
),
origData: nil,
updData: map[string]interface{}{
"field1": "value1",
},
want: map[string]interface{}{
"field1": "value1",
},
wantErr: false,
},
{
name: "merge with empty original data",
schema: schema.New(
"field1", field.String(),
),
origData: map[string]interface{}{},
updData: map[string]interface{}{
"field1": "value1",
},
want: map[string]interface{}{
"field1": "value1",
},
wantErr: false,
},
{
name: "merge with schema fields",
schema: schema.New(
"field1", field.String(),
"field2", field.String(),
"field3", field.String(),
),
origData: map[string]interface{}{
"field1": "value1",
"field2": "value2",
},
updData: map[string]interface{}{
"field2": "new_value2",
"field3": "value3",
},
want: map[string]interface{}{
"field1": "value1",
"field2": "new_value2",
"field3": "value3",
},
wantErr: false,
},
{
name: "merge with extra fields not in schema",
schema: schema.New(
"field1", field.String(),
"field2", field.String(),
),
origData: map[string]interface{}{
"field1": "value1",
"extra_field": "extra_value",
},
updData: map[string]interface{}{
"field2": "value2",
"another_extra": "another_value",
},
want: map[string]interface{}{
"field1": "value1",
"field2": "value2",
},
wantErr: false,
},
{
name: "merge with different field types",
schema: schema.New(
"string_field", field.String(),
"number_field", field.Number(field.NumberFormatInt),
"bool_field", field.String(),
),
origData: map[string]interface{}{
"string_field": "old_value",
"number_field": 42,
},
updData: map[string]interface{}{
"string_field": "new_value",
"bool_field": "true",
},
want: map[string]interface{}{
"string_field": "new_value",
"number_field": 42,
"bool_field": "true",
},
wantErr: false,
},
{
name: "merge with nested schema",
schema: schema.New(
"user", field.Object(
"name", field.String(),
"age", field.Number(field.NumberFormatInt),
"active", field.Bool(),
),
"metadata", field.Object(
"created_at", field.String(),
"updated_at", field.String(),
),
),
origData: map[string]interface{}{
"user": map[string]interface{}{
"name": "John",
"age": 30,
"active": true,
},
"metadata": map[string]interface{}{
"created_at": "2024-01-01",
},
},
updData: map[string]interface{}{
"user": map[string]interface{}{
"name": "John Doe",
},
"metadata": map[string]interface{}{
"updated_at": "2024-03-20",
},
},
want: map[string]interface{}{
"user": map[string]interface{}{
"name": "John Doe",
"age": 30,
"active": true,
},
"metadata": map[string]interface{}{
"created_at": "2024-01-01",
"updated_at": "2024-03-20",
},
},
wantErr: false,
},
{
name: "merge with array fields",
schema: schema.New(
"tags", field.Array(field.String()),
"numbers", field.Array(field.Number(field.NumberFormatInt)),
"mixed", field.Array(field.String()),
),
origData: map[string]interface{}{
"tags": []interface{}{"tag1", "tag2"},
"numbers": []interface{}{1, 2, 3},
},
updData: map[string]interface{}{
"tags": []interface{}{"tag3", "tag4"},
"mixed": []interface{}{"value1", "value2"},
},
want: map[string]interface{}{
"tags": []interface{}{"tag3", "tag4"},
"numbers": []interface{}{1, 2, 3},
"mixed": []interface{}{"value1", "value2"},
},
wantErr: false,
},
{
name: "merge with required fields",
schema: schema.New(
"required_field", field.String(),
"optional_field", field.String(),
),
origData: map[string]interface{}{
"required_field": "original",
"optional_field": "optional",
},
updData: map[string]interface{}{
"required_field": "updated",
},
want: map[string]interface{}{
"required_field": "updated",
"optional_field": "optional",
},
wantErr: false,
},
{
name: "merge with validation rules",
schema: schema.New(
"email", field.String(),
"age", field.Number(field.NumberFormatInt),
),
origData: map[string]interface{}{
"email": "test@example.com",
"age": 25,
},
updData: map[string]interface{}{
"email": "new@example.com",
"age": 30,
},
want: map[string]interface{}{
"email": "new@example.com",
"age": 30,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := MergeItemData(context.Background(), tt.schema, tt.origData, tt.updData)
if (err != nil) != tt.wantErr {
t.Errorf("mergeItemData() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, tt.want, got)
})
}
}
......@@ -48,14 +48,24 @@ func (s *Schema) ClearState() *Schema {
return s
}
func (s *Schema) Equal(sch *Schema) bool {
if s == sch {
// Equal compares two schemas for equality.
// Deprecated: use `IsEqual` method instead.
func (s *Schema) Equal(other *Schema) bool {
return s.IsEqual(other)
}
func (s *Schema) IsEqual(other *Schema) bool {
if s == other {
return true
}
if s == nil || sch == nil {
if s == nil || other == nil {
return false
}
return reflect.DeepEqual(s.Field, sch.Field)
return reflect.DeepEqual(s.Field, other.Field)
}
func IsEqual(s1, s2 *Schema) bool {
return s1.IsEqual(s2)
}
func (s Schema) WithIncludes(includes ...interface{}) *Schema {
......@@ -78,9 +88,9 @@ func (s Schema) SetMetadata(md map[string]string) *Schema {
return &s
}
func (f Schema) SetSingleLocale(r bool) *Schema {
f.SingleLocale = r
return &f
func (s Schema) SetSingleLocale(r bool) *Schema {
s.SingleLocale = r
return &s
}
func (s *Schema) ConvertTypes() error {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment