From c15843217f93af7221d78447603c8a4457b61354 Mon Sep 17 00:00:00 2001 From: Semyon Krestyaninov <krestyaninov@perx.ru> Date: Mon, 9 Jun 2025 15:52:07 +0300 Subject: [PATCH] add ToMap and FromMap for item --- pkg/items/item.go | 100 ++++++++++++++---- pkg/items/item_test.go | 235 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 313 insertions(+), 22 deletions(-) diff --git a/pkg/items/item.go b/pkg/items/item.go index b211b6d7..86af196a 100644 --- a/pkg/items/item.go +++ b/pkg/items/item.go @@ -13,6 +13,7 @@ import ( "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" + "github.com/mitchellh/mapstructure" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -169,29 +170,84 @@ func (i *Item) Clone() *Item { return &itm } -func (i *Item) ToMap() map[string]interface{} { - return map[string]interface{}{ - "id": i.ID, - "space_id": i.SpaceID, - "env_id": i.EnvID, - "collection_id": i.CollectionID, - "state": i.State, - "created_rev_at": i.CreatedRevAt, - "created_by": i.CreatedBy, - "created_at": i.CreatedAt, - "updated_at": i.UpdatedAt, - "updated_by": i.UpdatedBy, - "revision_id": i.RevisionID, - "revision_description": i.RevisionDescription, - "data": i.Data, - "locale_id": i.LocaleID, - "translations": i.Translations, - "translations_ids": i.TranslationsIDs, - "deleted": i.Deleted, - "hidden": i.Hidden, - "template": i.Template, - "search_score": i.SearchScore, +// ToMap конвертирует переданный Item в map[string]any, кодируя данные согласно схеме. +// Вычисляемые поля удаляются из результата. +func ToMap(item *Item, sch *schema.Schema) (map[string]any, error) { + if item == nil { + return nil, errors.New("item must not be nil") + } + if sch == nil { + return nil, errors.New("schema must not be nil") + } + + output := make(map[string]any) + + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + TagName: "json", + Result: &output, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to create decoder") + } + + item, err = item.Encode(context.Background(), sch) + if err != nil { + return nil, errors.Wrap(err, "failed to encode item by schema") + } + + err = decoder.Decode(item) + if err != nil { + return nil, errors.Wrap(err, "failed to encode item") } + + // Кодируем системные поля со временем + output["createdRevAt"] = item.CreatedRevAt.Format(time.RFC3339) + output["createdAt"] = item.CreatedAt.Format(time.RFC3339) + output["updatedAt"] = item.UpdatedAt.Format(time.RFC3339) + + // Удаляем вычисляемые поля + delete(output, "permissions") + delete(output, "searchScore") + + return output, nil +} + +// FromMap конвертирует переданный map[string]any в Item, декодируя данные согласно схеме. +// Вычисляемые поля игнорируются при декодировании. +func FromMap(input map[string]any, sch *schema.Schema) (*Item, error) { + if len(input) == 0 { + return nil, errors.New("input map must not be nil") + } + if sch == nil { + return nil, errors.New("schema must not be nil") + } + + item := &Item{} + + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + DecodeHook: mapstructure.StringToTimeHookFunc(time.RFC3339), + TagName: "json", + Result: item, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to create decoder") + } + + // Удаляем вычисляемые поля + delete(input, "permissions") + delete(input, "searchScore") + + err = decoder.Decode(input) + if err != nil { + return nil, errors.Wrap(err, "failed to decode") + } + + item, err = item.Decode(context.Background(), sch) + if err != nil { + return nil, errors.Wrap(err, "failed to decode item by schema") + } + + return item, nil } // SetData устанавливает перевод в нужное поле записи diff --git a/pkg/items/item_test.go b/pkg/items/item_test.go index 1344444e..44e96946 100644 --- a/pkg/items/item_test.go +++ b/pkg/items/item_test.go @@ -506,3 +506,238 @@ func Test_mergeItemData(t *testing.T) { }) } } + +func TestToMap(t *testing.T) { + sch := schema.New( + "str", field.String(), + "num", field.Number(field.NumberFormatFloat), + "embedded", field.Object( + "now", field.Time(), + ), + ) + + tests := []struct { + name string + input *Item + sch *schema.Schema + want map[string]any + assertErr assert.ErrorAssertionFunc + }{ + { + name: "nil item", + input: nil, + sch: nil, + want: nil, + assertErr: assert.Error, + }, + { + name: "nil schema", + input: &Item{}, + sch: nil, + want: nil, + assertErr: assert.Error, + }, + { + name: "simple", + input: &Item{ + ID: "item_id", + SpaceID: "space_id", + EnvID: "env_id", + CollectionID: "coll_id", + State: StatePublished, + CreatedRevAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC), + CreatedBy: "created_by", + CreatedAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC), + UpdatedBy: "updated_by", + Data: map[string]any{ + "str": "string", + "num": 2.7, + "embedded": map[string]any{ + "now": time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC), + }, + }, + LocaleID: "locale_id", + Translations: map[string]map[string]any{ + "ru": { + "str": "строка", + "num": 3.14, + }, + }, + TranslationsIDs: []string{"ru"}, + RevisionID: "rev_id", + RevisionDescription: "rev_desc", + Permissions: &Permissions{ + Edit: true, + Archive: false, + Publish: true, + SoftDelete: false, + HardDelete: false, + }, + SearchScore: 123.0, + Deleted: false, + Hidden: true, + Template: true, + }, + sch: sch, + want: map[string]any{ + "id": "item_id", + "spaceId": "space_id", + "envId": "env_id", + "collectionId": "coll_id", + "state": StatePublished, + "createdRevAt": "2024-08-12T00:00:00Z", + "createdBy": "created_by", + "createdAt": "2024-08-12T00:00:00Z", + "updatedAt": "2024-08-12T00:00:00Z", + "updatedBy": "updated_by", + "data": map[string]any{ + "str": "string", + "num": 2.7, + "embedded": map[string]any{ + "now": "2024-08-12T00:00:00Z", + }, + }, + "localeId": "locale_id", + "translations": map[string]map[string]any{ + "ru": { + "str": "строка", + "num": 3.14, + }, + }, + "translationsIds": []string{"ru"}, + "revId": "rev_id", + "revDescription": "rev_desc", + "deleted": false, + "hidden": true, + "template": true, + }, + assertErr: assert.NoError, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := ToMap(tc.input, tc.sch) + tc.assertErr(t, err) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestFromMap(t *testing.T) { + sch := schema.New( + "str", field.String(), + "num", field.Number(field.NumberFormatFloat), + "embedded", field.Object( + "now", field.Time(), + ), + ) + + tests := []struct { + name string + input map[string]any + want *Item + sch *schema.Schema + assertErr assert.ErrorAssertionFunc + }{ + { + name: "nil item", + input: nil, + sch: nil, + want: nil, + assertErr: assert.Error, + }, + { + name: "empty item", + input: map[string]any{}, + sch: nil, + want: nil, + assertErr: assert.Error, + }, + { + name: "nil schema", + input: map[string]any{}, + sch: nil, + assertErr: assert.Error, + }, + { + name: "simple", + input: map[string]any{ + "id": "item_id", + "spaceId": "space_id", + "envId": "env_id", + "collectionId": "coll_id", + "state": StatePublished, + "createdRevAt": "2024-08-12T00:00:00Z", + "createdBy": "created_by", + "createdAt": "2024-08-12T00:00:00Z", + "updatedAt": "2024-08-12T00:00:00Z", + "updatedBy": "updated_by", + "data": map[string]any{ + "str": "string", + "num": 2.7, + "embedded": map[string]any{ + "now": "2024-08-12T00:00:00Z", + }, + }, + "localeId": "locale_id", + "translations": map[string]map[string]any{ + "ru": { + "str": "строка", + "num": 3.14, + }, + }, + "translationsIds": []string{"ru"}, + "revId": "rev_id", + "revDescription": "rev_desc", + "deleted": false, + "hidden": true, + "template": true, + }, + sch: sch, + want: &Item{ + ID: "item_id", + SpaceID: "space_id", + EnvID: "env_id", + CollectionID: "coll_id", + State: StatePublished, + CreatedRevAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC), + CreatedBy: "created_by", + CreatedAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC), + UpdatedBy: "updated_by", + Data: map[string]any{ + "str": "string", + "num": 2.7, + "embedded": map[string]any{ + "now": time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC), + }, + }, + LocaleID: "locale_id", + Translations: map[string]map[string]any{ + "ru": { + "str": "строка", + "num": 3.14, + }, + }, + TranslationsIDs: []string{"ru"}, + RevisionID: "rev_id", + RevisionDescription: "rev_desc", + Permissions: nil, + SearchScore: 0.0, + Deleted: false, + Hidden: true, + Template: true, + }, + assertErr: assert.NoError, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := FromMap(tc.input, sch) + tc.assertErr(t, err) + assert.Equal(t, tc.want, got) + }) + } +} -- GitLab