diff --git a/netmap/policy.go b/netmap/policy.go index a5ef032a..5e23a9eb 100644 --- a/netmap/policy.go +++ b/netmap/policy.go @@ -36,13 +36,31 @@ func (p *PlacementPolicy) ToV2() *netmap.PlacementPolicy { // SubnetID returns subnet to select nodes from. func (p *PlacementPolicy) SubnetID() *subnetid.ID { - return (*subnetid.ID)( - (*netmap.PlacementPolicy)(p).GetSubnetID()) + idv2 := (*netmap.PlacementPolicy)(p).GetSubnetID() + if idv2 == nil { + return nil + } + + var id subnetid.ID + + err := id.ReadFromV2(*idv2) + if err != nil { + panic(err) // will disappear after netmap package refactor + } + + return &id } // SetSubnetID sets subnet to select nodes from. func (p *PlacementPolicy) SetSubnetID(subnet *subnetid.ID) { - (*netmap.PlacementPolicy)(p).SetSubnetID((*refs.SubnetID)(subnet)) + var idv2 *refs.SubnetID + + if subnet != nil { + idv2 = new(refs.SubnetID) + subnet.WriteToV2(idv2) + } + + (*netmap.PlacementPolicy)(p).SetSubnetID(idv2) } // Replicas returns list of object replica descriptors. diff --git a/netmap/subnet.go b/netmap/subnet.go index b61a504e..25582469 100644 --- a/netmap/subnet.go +++ b/netmap/subnet.go @@ -2,6 +2,7 @@ package netmap import ( "errors" + "fmt" "github.com/nspcc-dev/neofs-api-go/v2/netmap" "github.com/nspcc-dev/neofs-api-go/v2/refs" @@ -48,9 +49,12 @@ func (i *NodeInfo) IterateSubnets(f func(subnetid.ID) error) error { var id subnetid.ID return netmap.IterateSubnets((*netmap.NodeInfo)(i), func(idv2 refs.SubnetID) error { - id.FromV2(idv2) + err := id.ReadFromV2(idv2) + if err != nil { + return fmt.Errorf("invalid subnet: %w", err) + } - err := f(id) + err = f(id) if errors.Is(err, ErrRemoveSubnet) { return netmap.ErrRemoveSubnet } @@ -66,7 +70,7 @@ var errAbortSubnetIter = errors.New("abort subnet iterator") // Function is NPE-safe: nil NodeInfo always belongs to zero subnet only. func BelongsToSubnet(node *NodeInfo, id subnetid.ID) bool { err := node.IterateSubnets(func(id_ subnetid.ID) error { - if id.Equals(&id_) { + if id.Equals(id_) { return errAbortSubnetIter } diff --git a/netmap/subnet_test.go b/netmap/subnet_test.go index 0dd3600d..7e1150b5 100644 --- a/netmap/subnet_test.go +++ b/netmap/subnet_test.go @@ -12,7 +12,7 @@ func TestNodeInfoSubnets(t *testing.T) { t.Run("enter subnet", func(t *testing.T) { var id subnetid.ID - id.SetNumber(13) + id.SetNumeric(13) var node netmap.NodeInfo @@ -35,8 +35,8 @@ func TestNodeInfoSubnets(t *testing.T) { t.Run("not last", func(t *testing.T) { var id, idrm subnetid.ID - id.SetNumber(13) - idrm.SetNumber(23) + id.SetNumeric(13) + idrm.SetNumeric(23) var node netmap.NodeInfo @@ -44,7 +44,7 @@ func TestNodeInfoSubnets(t *testing.T) { node.EnterSubnet(idrm) err := node.IterateSubnets(func(id subnetid.ID) error { - if subnetid.IsZero(id) || id.Equals(&idrm) { + if subnetid.IsZero(id) || id.Equals(idrm) { return netmap.ErrRemoveSubnet } @@ -100,7 +100,7 @@ func TestEnterSubnet(t *testing.T) { node.ExitSubnet(id) require.False(t, netmap.BelongsToSubnet(&node, id)) - id.SetNumber(10) + id.SetNumeric(10) node.EnterSubnet(id) require.True(t, netmap.BelongsToSubnet(&node, id)) require.False(t, netmap.BelongsToSubnet(&node, subnetid.ID{})) @@ -113,8 +113,8 @@ func TestEnterSubnet(t *testing.T) { func TestBelongsToSubnet(t *testing.T) { var id, idMiss, idZero subnetid.ID - id.SetNumber(13) - idMiss.SetNumber(23) + id.SetNumeric(13) + idMiss.SetNumeric(23) var node netmap.NodeInfo diff --git a/subnet/doc.go b/subnet/doc.go new file mode 100644 index 00000000..f68816fb --- /dev/null +++ b/subnet/doc.go @@ -0,0 +1,10 @@ +/* +Package subnet collects functionality related to the NeoFS subnets. + +Subnet of a particular NeoFS network consists of a subset of the storage nodes +of that network. Subnet of the whole network is called zero. Info type acts as +a subnet descriptor. Each subnet is owned by the user who created it. Information +about all subnets is stored in the Subnet contract of the NeoFS Sidechain. + +*/ +package subnet diff --git a/subnet/id/doc.go b/subnet/id/doc.go new file mode 100644 index 00000000..e8ee2cb9 --- /dev/null +++ b/subnet/id/doc.go @@ -0,0 +1,10 @@ +/* +Package subnetid provides primitives to work with subnet identification in NeoFS. + +ID type is used for global subnet identity inside the NeoFS network. + +Using package types in an application is recommended to potentially work with +different protocol versions with which these types are compatible. + +*/ +package subnetid diff --git a/subnet/id/id.go b/subnet/id/id.go index 9a198df2..8cfa4fa9 100644 --- a/subnet/id/id.go +++ b/subnet/id/id.go @@ -2,95 +2,104 @@ package subnetid import ( "fmt" + "strconv" "github.com/nspcc-dev/neofs-api-go/v2/refs" ) -// ID represents NeoFS subnet identifier. +// ID represents unique identifier of the subnet in the NeoFS network. // -// The type is compatible with the corresponding message from NeoFS API V2 protocol. +// ID is mutually compatible with github.com/nspcc-dev/neofs-api-go/v2/refs.SubnetID +// message. See ReadFromV2 / WriteToV2 methods. // -// Zero value and nil pointer is equivalent to zero subnet ID. -type ID refs.SubnetID - -// FromV2 initializes ID from refs.SubnetID message structure. Must not be called on nil. -// -// Note: nil refs.SubnetID corresponds to zero ID value or nil pointer to it. -func (x *ID) FromV2(msg refs.SubnetID) { - *x = ID(msg) +// Instances can be created using built-in var declaration. Zero value is +// equivalent to identifier of the zero subnet (whole NeoFS network). +type ID struct { + m refs.SubnetID } -// WriteToV2 writes ID to refs.SubnetID message structure. The message must not be nil. +// ReadFromV2 reads ID from the refs.SubnetID message. Checks if the +// message conforms to NeoFS API V2 protocol. // -// Note: nil ID corresponds to zero refs.SubnetID value or nil pointer to it. +// See also WriteToV2. +func (x *ID) ReadFromV2(msg refs.SubnetID) error { + x.m = msg + return nil +} + +// WriteToV2 writes ID to refs.SubnetID message structure. The message MUST NOT +// be nil. +// +// See also ReadFromV2. func (x ID) WriteToV2(msg *refs.SubnetID) { - *msg = refs.SubnetID(x) + *msg = x.m } -// Equals returns true iff both instances identify the same subnet. +// Equals defines a comparison relation between two ID instances. // -// Method is NPE-safe: nil pointer equals to pointer to zero value. -func (x *ID) Equals(x2 *ID) bool { - return (*refs.SubnetID)(x).GetValue() == (*refs.SubnetID)(x2).GetValue() +// Note that comparison using '==' operator is not recommended since it MAY result +// in loss of compatibility. +func (x ID) Equals(x2 ID) bool { + return x.m.GetValue() == x2.m.GetValue() } -// MarshalText encodes ID into text format according to particular NeoFS API protocol. -// Supported versions: -// * V2 (see refs.SubnetID type). +// EncodeToString encodes ID into NeoFS API protocol string (base10 encoding). // -// Implements encoding.TextMarshaler. -func (x *ID) MarshalText() ([]byte, error) { - return (*refs.SubnetID)(x).MarshalText() +// See also DecodeString. +func (x ID) EncodeToString() string { + return strconv.FormatUint(uint64(x.m.GetValue()), 10) } -// UnmarshalText decodes ID from the text according to particular NeoFS API protocol. -// Must not be called on nil. Supported versions: -// * V2 (see refs.SubnetID type). -// -// Implements encoding.TextUnmarshaler. -func (x *ID) UnmarshalText(text []byte) error { - return (*refs.SubnetID)(x).UnmarshalText(text) -} - -// String returns string representation of ID using MarshalText. -// Returns string with message on error. -// -// Implements fmt.Stringer. -func (x *ID) String() string { - text, err := x.MarshalText() +// DecodeString decodes string calculated using EncodeToString. Returns +// an error if s is malformed. +func (x *ID) DecodeString(s string) error { + num, err := strconv.ParseUint(s, 10, 32) if err != nil { - return fmt.Sprintf(" %v", err) + return fmt.Errorf("invalid numeric value: %w", err) } - return string(text) + x.m.SetValue(uint32(num)) + + return nil } -// Marshal encodes ID into a binary format of NeoFS API V2 protocol (Protocol Buffers with direct field order). -func (x *ID) Marshal() ([]byte, error) { - return (*refs.SubnetID)(x).StableMarshal(nil), nil -} - -// Unmarshal decodes ID from NeoFS API V2 binary format (see Marshal). Must not be called on nil. +// String implements fmt.Stringer. // -// Note: empty data corresponds to zero ID value or nil pointer to it. +// String is designed to be human-readable, and its format MAY differ between +// SDK versions. String MAY return same result as EncodeToString. String MUST NOT +// be used to encode ID into NeoFS protocol string. +func (x ID) String() string { + return "#" + strconv.FormatUint(uint64(x.m.GetValue()), 10) +} + +// Marshal encodes ID into a binary format of the NeoFS API protocol +// (Protocol Buffers with direct field order). +// +// See also Unmarshal. +func (x ID) Marshal() []byte { + return x.m.StableMarshal(nil) +} + +// Unmarshal decodes binary ID calculated using Marshal. Returns an error +// describing a format violation. func (x *ID) Unmarshal(data []byte) error { - return (*refs.SubnetID)(x).Unmarshal(data) + return x.m.Unmarshal(data) } -// SetNumber sets ID value in uint32 format. Must not be called on nil. -// By default, number is 0 which refers to zero subnet. -func (x *ID) SetNumber(num uint32) { - (*refs.SubnetID)(x).SetValue(num) +// SetNumeric sets ID value in numeric format. By default, number is 0 which +// refers to the zero subnet. +func (x *ID) SetNumeric(num uint32) { + x.m.SetValue(num) } -// IsZero returns true iff the ID refers to zero subnet. +// IsZero compares id with zero subnet ID. func IsZero(id ID) bool { - return id.Equals(nil) + return id.Equals(ID{}) } -// MakeZero makes ID to refer to zero subnet. Arg must not be nil (it is already zero). +// MakeZero makes ID to refer to zero subnet. // // Makes no sense to call on zero value (e.g. declared using var). func MakeZero(id *ID) { - id.SetNumber(0) + id.SetNumeric(0) } diff --git a/subnet/id/id_test.go b/subnet/id/id_test.go index ddd926bf..3515bbf6 100644 --- a/subnet/id/id_test.go +++ b/subnet/id/id_test.go @@ -14,26 +14,26 @@ func TestIsZero(t *testing.T) { require.True(t, subnetid.IsZero(id)) - id.SetNumber(13) + id.SetNumeric(13) require.False(t, subnetid.IsZero(id)) - id.SetNumber(0) + id.SetNumeric(0) require.True(t, subnetid.IsZero(id)) } -func TestID_FromV2(t *testing.T) { +func TestID_ReadFromV2(t *testing.T) { const num = 13 var id1 subnetid.ID - id1.SetNumber(num) + id1.SetNumeric(num) var idv2 refs.SubnetID idv2.SetValue(num) var id2 subnetid.ID - id2.FromV2(idv2) + require.NoError(t, id2.ReadFromV2(idv2)) - require.True(t, id1.Equals(&id2)) + require.True(t, id1.Equals(id2)) } func TestID_WriteToV2(t *testing.T) { @@ -47,7 +47,7 @@ func TestID_WriteToV2(t *testing.T) { id.WriteToV2(&idv2) require.Zero(t, idv2.GetValue()) - id.SetNumber(num) + id.SetNumeric(num) id.WriteToV2(&idv2) require.EqualValues(t, num, idv2.GetValue()) @@ -58,36 +58,30 @@ func TestID_Equals(t *testing.T) { var id1, id2, idOther, id0 subnetid.ID - id0.Equals(nil) + id0.Equals(subnetid.ID{}) - id1.SetNumber(num) - id2.SetNumber(num) - idOther.SetNumber(num + 1) + id1.SetNumeric(num) + id2.SetNumeric(num) + idOther.SetNumeric(num + 1) - require.True(t, id1.Equals(&id2)) - require.False(t, id1.Equals(&idOther)) - require.False(t, id2.Equals(&idOther)) + require.True(t, id1.Equals(id2)) + require.False(t, id1.Equals(idOther)) + require.False(t, id2.Equals(idOther)) } func TestSubnetIDEncoding(t *testing.T) { id := subnetidtest.ID() t.Run("binary", func(t *testing.T) { - data, err := id.Marshal() - require.NoError(t, err) - var id2 subnetid.ID - require.NoError(t, id2.Unmarshal(data)) + require.NoError(t, id2.Unmarshal(id.Marshal())) require.True(t, id2.Equals(id)) }) t.Run("text", func(t *testing.T) { - data, err := id.MarshalText() - require.NoError(t, err) - var id2 subnetid.ID - require.NoError(t, id2.UnmarshalText(data)) + require.NoError(t, id2.DecodeString(id.EncodeToString())) require.True(t, id2.Equals(id)) }) @@ -95,12 +89,12 @@ func TestSubnetIDEncoding(t *testing.T) { func TestMakeZero(t *testing.T) { var id subnetid.ID - - id.SetNumber(13) + id.SetNumeric(13) require.False(t, subnetid.IsZero(id)) subnetid.MakeZero(&id) require.True(t, subnetid.IsZero(id)) + require.Equal(t, subnetid.ID{}, id) } diff --git a/subnet/id/test/doc.go b/subnet/id/test/doc.go new file mode 100644 index 00000000..409f2b2b --- /dev/null +++ b/subnet/id/test/doc.go @@ -0,0 +1,13 @@ +/* +Package subnetidtest provides functions for convenient testing of subnetid package API. + +Note that importing the package into source files is highly discouraged. + +Random instance generation functions can be useful when testing expects any value, e.g.: + import subnetidtest "github.com/nspcc-dev/neofs-sdk-go/suibnet/id/test" + + value := subnetidtest.ID() + // test the value + +*/ +package subnetidtest diff --git a/subnet/id/test/id.go b/subnet/id/test/id.go index 7a8ba466..00245d3d 100644 --- a/subnet/id/test/id.go +++ b/subnet/id/test/id.go @@ -6,11 +6,8 @@ import ( subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id" ) -// ID generates and returns random subnetid.ID using math/rand.Uint32. -func ID() *subnetid.ID { - var id subnetid.ID - - id.SetNumber(rand.Uint32()) - - return &id +// ID generates and returns random subnetid.ID. +func ID() (x subnetid.ID) { + x.SetNumeric(rand.Uint32()) + return } diff --git a/subnet/subnet.go b/subnet/subnet.go index fd295ab8..f47d5544 100644 --- a/subnet/subnet.go +++ b/subnet/subnet.go @@ -1,6 +1,8 @@ package subnet import ( + "fmt" + "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-api-go/v2/subnet" subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id" @@ -9,102 +11,99 @@ import ( // Info represents information about NeoFS subnet. // -// The type is compatible with the corresponding message from NeoFS API V2 protocol. +// Instances can be created using built-in var declaration. +type Info struct { + id subnetid.ID + + owner user.ID +} + +// Marshal encodes Info into a binary format of the NeoFS API protocol +// (Protocol Buffers with direct field order). // -// Zero value and nil pointer to it represents zero subnet w/o an owner. -type Info subnet.Info +// See also Unmarshal. +func (x Info) Marshal() []byte { + var id refs.SubnetID + x.id.WriteToV2(&id) -// FromV2 initializes Info from subnet.Info message structure. Must not be called on nil. -func (x *Info) FromV2(msg subnet.Info) { - *x = Info(msg) + var owner refs.OwnerID + x.owner.WriteToV2(&owner) + + var m subnet.Info + m.SetID(&id) + m.SetOwner(&owner) + + return m.StableMarshal(nil) } -// WriteToV2 writes Info to subnet.Info message structure. The message must not be nil. -func (x Info) WriteToV2(msg *subnet.Info) { - *msg = subnet.Info(x) -} - -// Marshal encodes Info into a binary format of NeoFS API V2 protocol (Protocol Buffers with direct field order). -func (x *Info) Marshal() ([]byte, error) { - return (*subnet.Info)(x).StableMarshal(nil), nil -} - -// Unmarshal decodes Info from NeoFS API V2 binary format (see Marshal). Must not be called on nil. -// -// Note: empty data corresponds to zero Info value or nil pointer to it. +// Unmarshal decodes binary Info calculated using Marshal. Returns an error +// describing a format violation. func (x *Info) Unmarshal(data []byte) error { - return (*subnet.Info)(x).Unmarshal(data) + var m subnet.Info + + err := m.Unmarshal(data) + if err != nil { + return err + } + + id := m.ID() + if id != nil { + err = x.id.ReadFromV2(*id) + if err != nil { + return fmt.Errorf("invalid ID: %w", err) + } + } else { + subnetid.MakeZero(&x.id) + } + + owner := m.Owner() + if owner != nil { + err = x.owner.ReadFromV2(*owner) + if err != nil { + return fmt.Errorf("invalid owner: %w", err) + } + } else { + x.owner = user.ID{} + } + + return nil } // SetID sets the identifier of the subnet that Info describes. +// +// See also ID. func (x *Info) SetID(id subnetid.ID) { - infov2 := (*subnet.Info)(x) - - idv2 := infov2.ID() - if idv2 == nil { - idv2 = new(refs.SubnetID) - infov2.SetID(idv2) - } - - id.WriteToV2(idv2) + x.id = id } -// ReadID reads the identifier of the subnet that Info describes. Arg must not be nil. -func (x Info) ReadID(id *subnetid.ID) { - infov2 := (subnet.Info)(x) - - idv2 := infov2.ID() - if idv2 == nil { - subnetid.MakeZero(id) - return - } - - id.FromV2(*idv2) +// ID returns subnet identifier set using SetID. +// +// Zero Info refers to the zero subnet. +func (x Info) ID() subnetid.ID { + return x.id } -// SetOwner sets subnet owner ID. +// SetOwner sets identifier of the subnet owner. func (x *Info) SetOwner(id user.ID) { - infov2 := (*subnet.Info)(x) - - idv2 := infov2.Owner() - if idv2 == nil { - idv2 = new(refs.OwnerID) - infov2.SetOwner(idv2) - } - - id.WriteToV2(idv2) + x.owner = id } -// ReadOwner reads the identifier of the subnet that Info describes. -// Must be called only if owner is set (see HasOwner). Arg must not be nil. -func (x Info) ReadOwner(id *user.ID) { - infov2 := (subnet.Info)(x) - - id2 := infov2.Owner() - if id2 == nil { - *id = user.ID{} - return - } - - if ownerV2 := infov2.Owner(); ownerV2 != nil { - _ = id.ReadFromV2(*ownerV2) - } +// Owner returns subnet owner set using SetOwner. +// +// Zero Info has no owner which is incorrect according to the +// NeoFS API protocol. +func (x Info) Owner() user.ID { + return x.owner } -// IsOwner checks subnet ownership. -func IsOwner(info Info, id user.ID) bool { - var id2 user.ID - - info.ReadOwner(&id2) - - return id.Equals(id2) +// AssertOwnership checks if the given info describes the subnet owned by the +// given user. +func AssertOwnership(info Info, id user.ID) bool { + return id.Equals(info.Owner()) } -// IDEquals checks if ID refers to subnet that Info describes. -func IDEquals(info Info, id subnetid.ID) bool { - id2 := new(subnetid.ID) - - info.ReadID(id2) - - return id.Equals(id2) +// AssertReference checks if the given info describes the subnet referenced by +// the given id. +func AssertReference(info Info, id subnetid.ID) bool { + return id.Equals(info.ID()) } diff --git a/subnet/subnet_test.go b/subnet/subnet_test.go index 70590fde..ce4466a1 100644 --- a/subnet/subnet_test.go +++ b/subnet/subnet_test.go @@ -3,11 +3,10 @@ package subnet_test import ( "testing" - subnetv2 "github.com/nspcc-dev/neofs-api-go/v2/subnet" - subnettest "github.com/nspcc-dev/neofs-api-go/v2/subnet/test" . "github.com/nspcc-dev/neofs-sdk-go/subnet" subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id" - "github.com/nspcc-dev/neofs-sdk-go/user" + subnetidtest "github.com/nspcc-dev/neofs-sdk-go/subnet/id/test" + subnettest "github.com/nspcc-dev/neofs-sdk-go/subnet/test" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" ) @@ -15,55 +14,35 @@ import ( func TestInfoZero(t *testing.T) { var info Info - var id subnetid.ID - info.ReadID(&id) - - require.True(t, subnetid.IsZero(id)) + require.Zero(t, info.ID()) + require.True(t, subnetid.IsZero(info.ID())) } func TestInfo_SetID(t *testing.T) { - var ( - id subnetid.ID - info Info - ) - - id.SetNumber(222) + id := subnetidtest.ID() + var info Info info.SetID(id) - require.True(t, IDEquals(info, id)) + require.Equal(t, id, info.ID()) + require.True(t, AssertReference(info, id)) } func TestInfo_SetOwner(t *testing.T) { - var ( - id user.ID - info Info - ) - - id = *usertest.ID() - - require.False(t, IsOwner(info, id)) + id := *usertest.ID() + var info Info info.SetOwner(id) - require.True(t, IsOwner(info, id)) + require.Equal(t, id, info.Owner()) + require.True(t, AssertOwnership(info, id)) } -func TestInfo_WriteToV2(t *testing.T) { - var ( - infoTo, infoFrom Info +func TestInfo_Marshal(t *testing.T) { + info := subnettest.Info() - infoV2From, infoV2To subnetv2.Info - ) + var info2 Info + require.NoError(t, info2.Unmarshal(info.Marshal())) - infoV2From = *subnettest.GenerateSubnetInfo(false) - - infoFrom.FromV2(infoV2From) - - infoFrom.WriteToV2(&infoV2To) - - infoTo.FromV2(infoV2To) - - require.Equal(t, infoV2From, infoV2To) - require.Equal(t, infoFrom, infoTo) + require.Equal(t, info, info2) } diff --git a/subnet/test/doc.go b/subnet/test/doc.go new file mode 100644 index 00000000..e74db82b --- /dev/null +++ b/subnet/test/doc.go @@ -0,0 +1,13 @@ +/* +Package subnettest provides functions for convenient testing of subnet package API. + +Note that importing the package into source files is highly discouraged. + +Random instance generation functions can be useful when testing expects any value, e.g.: + import subnettest "github.com/nspcc-dev/neofs-sdk-go/suibnet/test" + + value := subnettest.Info() + // test the value + +*/ +package subnettest diff --git a/subnet/test/subnet.go b/subnet/test/subnet.go new file mode 100644 index 00000000..7884f5a4 --- /dev/null +++ b/subnet/test/subnet.go @@ -0,0 +1,14 @@ +package subnettest + +import ( + "github.com/nspcc-dev/neofs-sdk-go/subnet" + subnetidtest "github.com/nspcc-dev/neofs-sdk-go/subnet/id/test" + usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" +) + +// Info generates and returns random subnet.Info. +func Info() (x subnet.Info) { + x.SetID(subnetidtest.ID()) + x.SetOwner(*usertest.ID()) + return +}