diff --git a/pkg/items/item.go b/pkg/items/item.go index dc56fe63284be6885cf4560578df0bf77729e359..92ef2494981c12e7e28e05c474ceb0db09e3c8be 100644 --- a/pkg/items/item.go +++ b/pkg/items/item.go @@ -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 +} diff --git a/pkg/items/item_test.go b/pkg/items/item_test.go index 05baa60a3c1e8eaae9534fe9026d7456d78ab36c..1344444e685ee025e9b7e13a4bc2d781eb36a9de 100644 --- a/pkg/items/item_test.go +++ b/pkg/items/item_test.go @@ -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) + }) + } +}