From 16d93c251a3f2df96ddddacae04f6a23df74fd2a Mon Sep 17 00:00:00 2001
From: Alex Petraky <petraky@perx.ru>
Date: Mon, 28 Apr 2025 20:07:40 +0000
Subject: [PATCH] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?=
 =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F?=
 =?UTF-8?q?=20MergeItemData=20=D0=B2=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=20It?=
 =?UTF-8?q?ems,=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D0=B0=D1=8F=20=D0=B2?=
 =?UTF-8?q?=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D1=8F=D0=B5=D1=82=20=D1=81=D0=BB?=
 =?UTF-8?q?=D0=B8=D1=8F=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?=
 =?UTF-8?q?=D1=85=20=D0=B4=D0=B2=D1=83=D1=85=20=D0=BE=D0=B1=D1=8A=D0=B5?=
 =?UTF-8?q?=D0=BA=D1=82=D0=BE=D0=B2=20Item=20=D1=81=20=D1=83=D1=87=D0=B5?=
 =?UTF-8?q?=D1=82=D0=BE=D0=BC=20=D1=81=D1=85=D0=B5=D0=BC=D1=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Issue: #3191
---
 pkg/items/item.go      |  29 +++++
 pkg/items/item_test.go | 242 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 271 insertions(+)

diff --git a/pkg/items/item.go b/pkg/items/item.go
index dc56fe63..92ef2494 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 05baa60a..1344444e 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)
+		})
+	}
+}
-- 
GitLab