103 lines
3.2 KiB
Go
103 lines
3.2 KiB
Go
package racename
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"galaxy/util"
|
|
|
|
confusables "github.com/disciplinedware/go-confusables"
|
|
"golang.org/x/text/cases"
|
|
)
|
|
|
|
// confusableSkeletoner abstracts the underlying TR39 confusable-skeleton
|
|
// computer so tests may substitute a deterministic stub.
|
|
type confusableSkeletoner interface {
|
|
Skeleton(string) string
|
|
}
|
|
|
|
// Policy produces canonical uniqueness keys and validates user-supplied race
|
|
// names under the Race Name Directory rules: Unicode case folding, explicit
|
|
// ASCII anti-fraud digit-to-letter mappings, and a TR39 confusable skeleton.
|
|
type Policy struct {
|
|
caseFolder cases.Caser
|
|
skeletoner confusableSkeletoner
|
|
}
|
|
|
|
// antiFraudReplacer collapses the frozen ASCII anti-fraud digit-to-letter
|
|
// pairs so `P1lot` and `Pilot` canonicalize together.
|
|
var antiFraudReplacer = strings.NewReplacer(
|
|
"1", "i",
|
|
"0", "o",
|
|
"8", "b",
|
|
)
|
|
|
|
// NewPolicy returns the default race-name canonicalization policy.
|
|
func NewPolicy() (*Policy, error) {
|
|
policy := &Policy{
|
|
caseFolder: cases.Fold(),
|
|
skeletoner: confusables.Default(),
|
|
}
|
|
if policy.skeletoner == nil {
|
|
return nil, fmt.Errorf("new race-name policy: nil confusable skeletoner")
|
|
}
|
|
|
|
return policy, nil
|
|
}
|
|
|
|
// Canonical returns the stable uniqueness key for raceName.
|
|
//
|
|
// raceName is expected to be non-empty; surrounding whitespace is trimmed
|
|
// before canonicalization so callers that preserve original casing pass the
|
|
// untrimmed display form directly.
|
|
func (policy *Policy) Canonical(raceName string) (CanonicalKey, error) {
|
|
switch {
|
|
case policy == nil:
|
|
return "", fmt.Errorf("canonicalize race name: nil policy")
|
|
case policy.skeletoner == nil:
|
|
return "", fmt.Errorf("canonicalize race name: nil confusable skeletoner")
|
|
}
|
|
trimmed := strings.TrimSpace(raceName)
|
|
if trimmed == "" {
|
|
return "", fmt.Errorf("canonicalize race name: race name must not be empty")
|
|
}
|
|
|
|
folded := policy.caseFolder.String(trimmed)
|
|
antiFraudMapped := antiFraudReplacer.Replace(folded)
|
|
key := CanonicalKey(policy.skeletoner.Skeleton(antiFraudMapped))
|
|
if err := key.Validate(); err != nil {
|
|
return "", fmt.Errorf("canonicalize race name: %w", err)
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
// ValidateName reports whether raceName is structurally valid for use in the
|
|
// Race Name Directory. It delegates to galaxy/util.ValidateTypeName and
|
|
// returns the trimmed canonical display value on success.
|
|
func ValidateName(raceName string) (string, error) {
|
|
trimmed, ok := util.ValidateTypeName(raceName)
|
|
if !ok {
|
|
return "", fmt.Errorf("race name is invalid")
|
|
}
|
|
|
|
return trimmed, nil
|
|
}
|
|
|
|
// Canonicalize validates raceName as a Race Name Directory display value
|
|
// and returns its canonical uniqueness key. It composes ValidateName with
|
|
// the Canonical pipeline so every RND write and lookup shares a single
|
|
// entry point for both character-set and confusable-pair policy.
|
|
//
|
|
// Invalid raceName values surface the error returned by ValidateName;
|
|
// callers at the RaceNameDirectory port boundary map these to
|
|
// ports.ErrInvalidName.
|
|
func (policy *Policy) Canonicalize(raceName string) (CanonicalKey, error) {
|
|
trimmed, err := ValidateName(raceName)
|
|
if err != nil {
|
|
return "", fmt.Errorf("canonicalize race name: %w", err)
|
|
}
|
|
|
|
return policy.Canonical(trimmed)
|
|
}
|