From b95b2da0bf0cf0d7be14cebecec3e89a58b22f58 Mon Sep 17 00:00:00 2001
From: ko_oler <kooler89@gmail.com>
Date: Thu, 1 Feb 2024 14:51:58 +0300
Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?=
 =?UTF-8?q?=D0=BD=20marshall/unmarshall=20=D0=B4=D0=BB=D1=8F=20=D0=B8?=
 =?UTF-8?q?=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 id/json.go      |  58 ++++++++++++++++-
 id/json_test.go | 163 ++++++++++++++++++++++++++++++++++++++++++++++++
 id/role.go      |   2 +-
 3 files changed, 221 insertions(+), 2 deletions(-)
 create mode 100644 id/json_test.go

diff --git a/id/json.go b/id/json.go
index 04fa39fa..9c468ceb 100644
--- a/id/json.go
+++ b/id/json.go
@@ -1,3 +1,59 @@
 package id
 
-// TODO: Сохранение/чтение в JSON (в строку)
+import (
+	"encoding/json"
+	"reflect"
+
+	jsoniter "github.com/json-iterator/go"
+)
+
+var knownImplementations = []ID{
+	&SpaceID{},
+	&EnvironmentID{},
+	&CollectionID{},
+	&ClientID{},
+	&RoleID{},
+	&SchemaID{},
+	&ItemID{},
+	&RevisionID{},
+	&FieldID{},
+	&OrganizationID{},
+	&ServiceID{},
+	&UserID{},
+}
+
+type JSONID struct {
+	Value ID `json:"value"`
+}
+
+func (t *JSONID) UnmarshalJSON(b []byte) error {
+	var data struct {
+		Type  string
+		Value json.RawMessage
+	}
+	if err := jsoniter.Unmarshal(b, &data); err != nil {
+		return err
+	}
+	for _, knownImplementation := range knownImplementations {
+		if knownImplementation.Type() == data.Type {
+			knownType := reflect.TypeOf(knownImplementation)
+			target := reflect.New(knownType)
+			if err := jsoniter.Unmarshal(data.Value, target.Interface()); err != nil {
+				return err
+			}
+			t.Value = target.Elem().Interface().(ID)
+			return nil
+		}
+	}
+	return nil
+}
+
+func (t *JSONID) MarshalJSON() ([]byte, error) {
+	return json.Marshal(struct {
+		Type  string
+		Value any
+	}{
+		Type:  reflect.TypeOf(t.Value).String(),
+		Value: t.Value,
+	})
+}
diff --git a/id/json_test.go b/id/json_test.go
new file mode 100644
index 00000000..7974a13d
--- /dev/null
+++ b/id/json_test.go
@@ -0,0 +1,163 @@
+package id
+
+import (
+	"testing"
+
+	jsoniter "github.com/json-iterator/go"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestJSONID_MarshalJSON(t *testing.T) {
+	tests := []struct {
+		name  string
+		Value ID
+		want  string
+	}{
+		{
+			name:  Organization,
+			Value: &OrganizationID{OrganizationID: "1"},
+			want:  "/orgs/1",
+		},
+		{
+			name:  User,
+			Value: &UserID{UserID: "1"},
+			want:  "/users/1",
+		},
+		{
+			name:  Service,
+			Value: &ServiceID{ServiceID: "1"},
+			want:  "/services/1",
+		},
+		{
+			name:  Space,
+			Value: &SpaceID{SpaceID: "1"},
+			want:  "/spaces/1",
+		},
+		{
+			name:  Environment,
+			Value: &EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}},
+			want:  "/spaces/1/envs/1",
+		},
+		{
+			name:  Client,
+			Value: &ClientID{ClientID: "1", SpaceID: SpaceID{SpaceID: "1"}},
+			want:  "/spaces/1/clients/1",
+		},
+		{
+			name:  Role,
+			Value: &RoleID{RoleID: "1", SpaceID: SpaceID{SpaceID: "1"}},
+			want:  "/spaces/1/roles/1",
+		},
+		{
+			name:  Collection,
+			Value: &CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}},
+			want:  "/spaces/1/envs/1/cols/1",
+		},
+		{
+			name:  Schema,
+			Value: &SchemaID{SchemaID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}},
+			want:  "/spaces/1/envs/1/schema/1",
+		},
+		{
+			name:  Item,
+			Value: &ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}},
+			want:  "/spaces/1/envs/1/cols/1/items/1",
+		},
+		{
+			name:  Revision,
+			Value: &RevisionID{RevisionID: "1", ItemID: ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}},
+			want:  "/spaces/1/envs/1/cols/1/items/1/revs/1",
+		},
+		{
+			name:  Field,
+			Value: &FieldID{FieldName: "1", ItemID: ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}},
+			want:  "/spaces/1/envs/1/cols/1/items/1/fields/1",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var j JSONID
+			j.Value = tt.Value
+			got, err := jsoniter.Marshal(&tt.Value)
+			require.NoError(t, err)
+			assert.Equalf(t, tt.want, string(got), "MarshalJSON()")
+		})
+	}
+}
+
+func TestJSONID_UnmarshalJSON(t *testing.T) {
+	tests := []struct {
+		name  string
+		Value ID
+		b     []byte
+	}{
+		{
+			name:  Organization,
+			Value: &OrganizationID{OrganizationID: "1"},
+			b:     []byte(`{"type": "organization", "value": "/orgs/1"}`),
+		},
+		{
+			name:  User,
+			Value: &UserID{UserID: "1"},
+			b:     []byte(`{"type": "user", "value": "/users/1"}`),
+		},
+		{
+			name:  Service,
+			Value: &ServiceID{ServiceID: "1"},
+			b:     []byte(`{"type": "service", "value": "/services/1"}`),
+		},
+		{
+			name:  Space,
+			Value: &SpaceID{SpaceID: "1"},
+			b:     []byte(`{"type": "space", "value": "/spaces/1"}`),
+		},
+		{
+			name:  Environment,
+			Value: &EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}},
+			b:     []byte(`{"type": "environment", "value": "/spaces/1/envs/1"}`),
+		},
+		{
+			name:  Client,
+			Value: &ClientID{ClientID: "1", SpaceID: SpaceID{SpaceID: "1"}},
+			b:     []byte(`{"type": "client", "value": "/spaces/1/clients/1"}`),
+		},
+		{
+			name:  Role,
+			Value: &RoleID{RoleID: "1", SpaceID: SpaceID{SpaceID: "1"}},
+			b:     []byte(`{"type": "role", "value": "/spaces/1/roles/1"}`),
+		},
+		{
+			name:  Collection,
+			Value: &CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}},
+			b:     []byte(`{"type": "collection", "value": "/spaces/1/envs/1/cols/1"}`),
+		},
+		{
+			name:  Schema,
+			Value: &SchemaID{SchemaID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}},
+			b:     []byte(`{"type": "schema", "value": "/spaces/1/envs/1/schema/1"}`),
+		},
+		{
+			name:  Item,
+			Value: &ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}},
+			b:     []byte(`{"type": "item", "value": "/spaces/1/envs/1/cols/1/items/1"}`),
+		},
+		{
+			name:  Revision,
+			Value: &RevisionID{RevisionID: "1", ItemID: ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}},
+			b:     []byte(`{"type": "revision", "value": "/spaces/1/envs/1/cols/1/items/1/revs/1"}`),
+		},
+		{
+			name:  Field,
+			Value: &FieldID{FieldName: "1", ItemID: ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}},
+			b:     []byte(`{"type": "field", "value": "/spaces/1/envs/1/cols/1/items/1/fields/1"}`),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var j JSONID
+			require.NoError(t, j.UnmarshalJSON(tt.b))
+			assert.Equal(t, j.Value, tt.Value)
+		})
+	}
+}
diff --git a/id/role.go b/id/role.go
index 273f5e1e..9dbfcd95 100644
--- a/id/role.go
+++ b/id/role.go
@@ -14,7 +14,7 @@ type RoleID struct {
 	RoleID string `json:"role_id"`
 }
 
-func (t *RoleID) Type() string { return Client }
+func (t *RoleID) Type() string { return Role }
 
 func (t *RoleID) String() string {
 	return Join(t.SpaceID.String(), RolesPrefix, t.RoleID)
-- 
GitLab