[#316] ir/netmap: Validate LOCODE attributes of network map candidates

Define a structure for dealing with the geographic location of nodes.
Implement VerifyAndUpdate (with the same purpose as NodeValidator interface)
that checks LOCODE attribute and fills other attributes of the location.
Technically the entity is a wrapper over the NeoFS location database: it
maps the node LOCODE to the database record from which the new attributes
are generated.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
remotes/KirillovDenis/release/v0.21.1
Leonard Lyubich 2021-02-09 17:44:55 +03:00 committed by Leonard Lyubich
parent cbfaceb956
commit db703a5117
4 changed files with 245 additions and 0 deletions

View File

@ -0,0 +1,85 @@
package locode
import (
"github.com/nspcc-dev/neofs-api-go/pkg/netmap"
"github.com/nspcc-dev/neofs-node/pkg/util/locode"
"github.com/pkg/errors"
)
var errMissingLocode = errors.New("missing locode attribute")
var errMissingRequiredAttr = errors.New("missing required attribute in DB record")
// VerifyAndUpdate validates LOCODE attribute of n
// and adds a group of related attributes.
//
// If n does not contain LOCODE attribute or its value does not
// match the UN/LOCODE format, an error returns.
//
// New attributes are formed from the record of DB instance (Prm).
// If DB entry R was found w/o errors, then new attributes are:
// * CountryCode: R.CountryCode().String();
// * Country: R.CountryName();
// * CityCode: R.LocationCode().String();
// * City: Record.LocationName();
// * SubDivCode: R.SubDivCode();
// * SubDiv: R.SubDivName();
// * Continent: R.Continent().String().
//
// LOCODE attribute remains untouched.
func (v *Validator) VerifyAndUpdate(n *netmap.NodeInfo) error {
mAttr := uniqueAttributes(n.Attributes())
attrLocode, ok := mAttr[attrKeyLocode]
if !ok {
return errMissingLocode
}
lc, err := locode.FromString(attrLocode.Value())
if err != nil {
return errors.Wrap(err, "invalid locode value")
}
record, err := v.db.Get(lc)
if err != nil {
return errors.Wrap(err, "could not get locode record from DB")
}
for attrKey, attrDesc := range v.mAttr {
attrVal := attrDesc.converter(record)
if attrVal == "" {
if !attrDesc.optional {
return errMissingRequiredAttr
}
continue
}
a := netmap.NewNodeAttribute()
a.SetKey(attrKey)
a.SetValue(attrVal)
mAttr[attrKey] = a
}
as := n.Attributes()
as = as[:0]
for _, attr := range mAttr {
as = append(as, attr)
}
n.SetAttributes(as...)
return nil
}
func uniqueAttributes(as []*netmap.NodeAttribute) map[string]*netmap.NodeAttribute {
mAttr := make(map[string]*netmap.NodeAttribute, len(as))
for _, attr := range as {
mAttr[attr.Key()] = attr
}
return mAttr
}

View File

@ -0,0 +1,59 @@
package locode
import (
"github.com/nspcc-dev/neofs-node/pkg/util/locode"
locodedb "github.com/nspcc-dev/neofs-node/pkg/util/locode/db"
)
// Record is an interface of read-only
// NeoFS LOCODE database single entry.
type Record interface {
// Must return ISO 3166-1 alpha-2
// country code.
//
// Must not return nil.
CountryCode() *locodedb.CountryCode
// Must return English short country name
// officially used by the ISO 3166
// Maintenance Agency (ISO 3166/MA).
CountryName() string
// Must return UN/LOCODE 3-character code
// for the location (numerals 2-9 may also
// be used).
//
// Must not return nil.
LocationCode() *locodedb.LocationCode
// Must return name of the location which
// have been allocated a UN/LOCODE without
// diacritic sign.
LocationName() string
// Must return ISO 1-3 character alphabetic
// and/or numeric code for the administrative
// division of the country concerned.
SubDivCode() string
// Must return subdivision name.
SubDivName() string
// Must return existing continent where is
// the location.
//
// Must not return nil.
Continent() *locodedb.Continent
}
// DB is an interface of read-only
// NeoFS LOCODE database.
type DB interface {
// Must find the record that corresponds to
// LOCODE and provides the Record interface.
//
// Must return an error if Record is nil.
//
// LOCODE is always non-nil.
Get(*locode.LOCODE) (Record, error)
}

View File

@ -0,0 +1,34 @@
package locode
type attrDescriptor struct {
optional bool
converter func(Record) string
}
func countryCodeValue(r Record) (val string) {
return r.CountryCode().String()
}
func countryValue(r Record) string {
return r.CountryName()
}
func locationCodeValue(r Record) string {
return r.LocationCode().String()
}
func locationValue(r Record) string {
return r.LocationName()
}
func subDivCodeValue(r Record) string {
return r.SubDivCode()
}
func subDivValue(r Record) string {
return r.SubDivName()
}
func continentValue(r Record) string {
return r.Continent().String()
}

View File

@ -0,0 +1,67 @@
package locode
// Prm groups the required parameters of the Validator's constructor.
//
// All values must comply with the requirements imposed on them.
// Passing incorrect parameter values will result in constructor
// failure (error or panic depending on the implementation).
type Prm struct {
// NeoFS LOCODE database interface.
//
// Must not be nil.
DB DB
}
// Validator is an utility that verifies and updates
// node attributes associated with its geographical location
// (LOCODE).
//
// For correct operation, Validator must be created
// using the constructor (New) based on the required parameters
// and optional components. After successful creation,
// the Validator is immediately ready to work through API.
type Validator struct {
db DB
mAttr map[string]attrDescriptor
}
// TODO: define constants in API lib.
const (
attrKeyLocode = "Locode"
attrKeyCountryCode = "CountryCode"
attrKeyCountry = "Country"
attrKeyLocationCode = "CityCode"
attrKeyLocation = "City"
attrKeySubDivCode = "SubDivCode"
attrKeySubDiv = "SubDiv"
attrKeyContinent = "Continent"
)
// New creates a new instance of the Validator.
//
// Panics if at least one value of the parameters is invalid.
//
// The created Validator does not require additional
// initialization and is completely ready for work.
func New(prm Prm) *Validator {
return &Validator{
db: prm.DB,
mAttr: map[string]attrDescriptor{
attrKeyCountryCode: {converter: countryCodeValue},
attrKeyCountry: {converter: countryValue},
attrKeyLocationCode: {converter: locationCodeValue},
attrKeyLocation: {converter: locationValue},
attrKeySubDivCode: {converter: subDivCodeValue, optional: true},
attrKeySubDiv: {converter: subDivValue, optional: true},
attrKeyContinent: {converter: continentValue},
},
}
}