From 9bebc1247dbb43d501614c1ca73eb18b643c3d9e Mon Sep 17 00:00:00 2001
From: Leonard Lyubich <leonard@nspcc.ru>
Date: Fri, 13 Nov 2020 13:38:49 +0300
Subject: [PATCH] [#168] accounting: Implement binary/JSON encoders/decoders on
 Decimal

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
---
 pkg/accounting/decimal.go      | 32 ++++++++++++++++++++++++++++++++
 pkg/accounting/decimal_test.go | 26 ++++++++++++++++++++++++++
 v2/accounting/json.go          | 26 ++++++++++++++++++++++++++
 v2/accounting/json_test.go     | 20 ++++++++++++++++++++
 v2/accounting/marshal.go       | 31 ++++++++++++++++++++++---------
 v2/accounting/marshal_test.go  | 29 +++++++++++++----------------
 6 files changed, 139 insertions(+), 25 deletions(-)
 create mode 100644 v2/accounting/json.go
 create mode 100644 v2/accounting/json_test.go

diff --git a/pkg/accounting/decimal.go b/pkg/accounting/decimal.go
index b6e275f..07b11d6 100644
--- a/pkg/accounting/decimal.go
+++ b/pkg/accounting/decimal.go
@@ -40,3 +40,35 @@ func (d *Decimal) SetPrecision(p uint32) {
 	(*accounting.Decimal)(d).
 		SetPrecision(p)
 }
+
+// Marshal marshals Decimal into a protobuf binary form.
+//
+// Buffer is allocated when the argument is empty.
+// Otherwise, the first buffer is used.
+func (d *Decimal) Marshal(b ...[]byte) ([]byte, error) {
+	var buf []byte
+	if len(b) > 0 {
+		buf = b[0]
+	}
+
+	return (*accounting.Decimal)(d).
+		StableMarshal(buf)
+}
+
+// Unmarshal unmarshals protobuf binary representation of Decimal.
+func (d *Decimal) Unmarshal(data []byte) error {
+	return (*accounting.Decimal)(d).
+		Unmarshal(data)
+}
+
+// MarshalJSON encodes Decimal to protobuf JSON format.
+func (d *Decimal) MarshalJSON() ([]byte, error) {
+	return (*accounting.Decimal)(d).
+		MarshalJSON()
+}
+
+// UnmarshalJSON decodes Decimal from protobuf JSON format.
+func (d *Decimal) UnmarshalJSON(data []byte) error {
+	return (*accounting.Decimal)(d).
+		UnmarshalJSON(data)
+}
diff --git a/pkg/accounting/decimal_test.go b/pkg/accounting/decimal_test.go
index e02a92f..a15334b 100644
--- a/pkg/accounting/decimal_test.go
+++ b/pkg/accounting/decimal_test.go
@@ -23,3 +23,29 @@ func TestDecimal_Precision(t *testing.T) {
 
 	require.Equal(t, p, d.Precision())
 }
+
+func TestDecimalEncoding(t *testing.T) {
+	d := NewDecimal()
+	d.SetValue(1)
+	d.SetPrecision(2)
+
+	t.Run("binary", func(t *testing.T) {
+		data, err := d.Marshal()
+		require.NoError(t, err)
+
+		d2 := NewDecimal()
+		require.NoError(t, d2.Unmarshal(data))
+
+		require.Equal(t, d, d2)
+	})
+
+	t.Run("json", func(t *testing.T) {
+		data, err := d.MarshalJSON()
+		require.NoError(t, err)
+
+		d2 := NewDecimal()
+		require.NoError(t, d2.UnmarshalJSON(data))
+
+		require.Equal(t, d, d2)
+	})
+}
diff --git a/v2/accounting/json.go b/v2/accounting/json.go
new file mode 100644
index 0000000..322ca5c
--- /dev/null
+++ b/v2/accounting/json.go
@@ -0,0 +1,26 @@
+package accounting
+
+import (
+	accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting/grpc"
+	"google.golang.org/protobuf/encoding/protojson"
+)
+
+func (d *Decimal) MarshalJSON() ([]byte, error) {
+	return protojson.MarshalOptions{
+		EmitUnpopulated: true,
+	}.Marshal(
+		DecimalToGRPCMessage(d),
+	)
+}
+
+func (d *Decimal) UnmarshalJSON(data []byte) error {
+	msg := new(accounting.Decimal)
+
+	if err := protojson.Unmarshal(data, msg); err != nil {
+		return err
+	}
+
+	*d = *DecimalFromGRPCMessage(msg)
+
+	return nil
+}
diff --git a/v2/accounting/json_test.go b/v2/accounting/json_test.go
new file mode 100644
index 0000000..1145e05
--- /dev/null
+++ b/v2/accounting/json_test.go
@@ -0,0 +1,20 @@
+package accounting_test
+
+import (
+	"testing"
+
+	"github.com/nspcc-dev/neofs-api-go/v2/accounting"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDecimalJSON(t *testing.T) {
+	i := generateDecimal(10)
+
+	data, err := i.MarshalJSON()
+	require.NoError(t, err)
+
+	i2 := new(accounting.Decimal)
+	require.NoError(t, i2.UnmarshalJSON(data))
+
+	require.Equal(t, i, i2)
+}
diff --git a/v2/accounting/marshal.go b/v2/accounting/marshal.go
index 83fd0e7..684599f 100644
--- a/v2/accounting/marshal.go
+++ b/v2/accounting/marshal.go
@@ -1,7 +1,9 @@
 package accounting
 
 import (
-	"github.com/nspcc-dev/neofs-api-go/util/proto"
+	protoutil "github.com/nspcc-dev/neofs-api-go/util/proto"
+	accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting/grpc"
+	"google.golang.org/protobuf/proto"
 )
 
 const (
@@ -27,14 +29,14 @@ func (d *Decimal) StableMarshal(buf []byte) ([]byte, error) {
 		err       error
 	)
 
-	n, err = proto.Int64Marshal(decimalValueField, buf[offset:], d.val)
+	n, err = protoutil.Int64Marshal(decimalValueField, buf[offset:], d.val)
 	if err != nil {
 		return nil, err
 	}
 
 	offset += n
 
-	n, err = proto.UInt32Marshal(decimalPrecisionField, buf[offset:], d.prec)
+	n, err = protoutil.UInt32Marshal(decimalPrecisionField, buf[offset:], d.prec)
 	if err != nil {
 		return nil, err
 	}
@@ -47,12 +49,23 @@ func (d *Decimal) StableSize() (size int) {
 		return 0
 	}
 
-	size += proto.Int64Size(decimalValueField, d.val)
-	size += proto.UInt32Size(decimalPrecisionField, d.prec)
+	size += protoutil.Int64Size(decimalValueField, d.val)
+	size += protoutil.UInt32Size(decimalPrecisionField, d.prec)
 
 	return size
 }
 
+func (d *Decimal) Unmarshal(data []byte) error {
+	m := new(accounting.Decimal)
+	if err := proto.Unmarshal(data, m); err != nil {
+		return err
+	}
+
+	*d = *DecimalFromGRPCMessage(m)
+
+	return nil
+}
+
 func (b *BalanceRequestBody) StableMarshal(buf []byte) ([]byte, error) {
 	if b == nil {
 		return []byte{}, nil
@@ -62,7 +75,7 @@ func (b *BalanceRequestBody) StableMarshal(buf []byte) ([]byte, error) {
 		buf = make([]byte, b.StableSize())
 	}
 
-	_, err := proto.NestedStructureMarshal(balanceReqBodyOwnerField, buf, b.ownerID)
+	_, err := protoutil.NestedStructureMarshal(balanceReqBodyOwnerField, buf, b.ownerID)
 	if err != nil {
 		return nil, err
 	}
@@ -75,7 +88,7 @@ func (b *BalanceRequestBody) StableSize() (size int) {
 		return 0
 	}
 
-	size = proto.NestedStructureSize(balanceReqBodyOwnerField, b.ownerID)
+	size = protoutil.NestedStructureSize(balanceReqBodyOwnerField, b.ownerID)
 
 	return size
 }
@@ -89,7 +102,7 @@ func (br *BalanceResponseBody) StableMarshal(buf []byte) ([]byte, error) {
 		buf = make([]byte, br.StableSize())
 	}
 
-	_, err := proto.NestedStructureMarshal(balanceRespBodyDecimalField, buf, br.bal)
+	_, err := protoutil.NestedStructureMarshal(balanceRespBodyDecimalField, buf, br.bal)
 	if err != nil {
 		return nil, err
 	}
@@ -102,7 +115,7 @@ func (br *BalanceResponseBody) StableSize() (size int) {
 		return 0
 	}
 
-	size = proto.NestedStructureSize(balanceRespBodyDecimalField, br.bal)
+	size = protoutil.NestedStructureSize(balanceRespBodyDecimalField, br.bal)
 
 	return size
 }
diff --git a/v2/accounting/marshal_test.go b/v2/accounting/marshal_test.go
index 4b42304..6ace77f 100644
--- a/v2/accounting/marshal_test.go
+++ b/v2/accounting/marshal_test.go
@@ -10,22 +10,6 @@ import (
 	goproto "google.golang.org/protobuf/proto"
 )
 
-func TestDecimal_StableMarshal(t *testing.T) {
-	decimalFrom := generateDecimal(888)
-	transport := new(grpc.Decimal)
-
-	t.Run("non empty", func(t *testing.T) {
-		wire, err := decimalFrom.StableMarshal(nil)
-		require.NoError(t, err)
-
-		err = goproto.Unmarshal(wire, transport)
-		require.NoError(t, err)
-
-		decimalTo := accounting.DecimalFromGRPCMessage(transport)
-		require.Equal(t, decimalFrom, decimalTo)
-	})
-}
-
 func TestBalanceRequestBody_StableMarshal(t *testing.T) {
 	requestBodyFrom := generateBalanceRequestBody("Owner ID")
 	transport := new(grpc.BalanceRequest_Body)
@@ -82,3 +66,16 @@ func generateBalanceResponseBody(val int64) *accounting.BalanceResponseBody {
 
 	return response
 }
+
+func TestDecimalMarshal(t *testing.T) {
+	d := generateDecimal(3)
+
+	data, err := d.StableMarshal(nil)
+	require.NoError(t, err)
+
+	d2 := new(accounting.Decimal)
+
+	require.NoError(t, d2.Unmarshal(data))
+
+	require.Equal(t, d, d2)
+}