package local import ( "fmt" "strings" "galaxy/user/internal/domain/account" "galaxy/user/internal/domain/common" "galaxy/user/internal/ports" confusables "github.com/disciplinedware/go-confusables" "golang.org/x/text/cases" ) type confusableSkeletoner interface { Skeleton(string) string } type raceNamePolicy struct { caseFolder cases.Caser skeletoner confusableSkeletoner } var raceNameAntiFraudReplacer = strings.NewReplacer( "1", "i", "0", "o", "8", "b", ) // NewRaceNamePolicy returns the local Stage 06 race-name canonicalization // policy backed by Unicode case folding, explicit ASCII anti-fraud mappings, // and a TR39 confusable skeleton. func NewRaceNamePolicy() (ports.RaceNamePolicy, error) { policy := &raceNamePolicy{ caseFolder: cases.Fold(), skeletoner: confusables.Default(), } if policy.skeletoner == nil { return nil, fmt.Errorf("new race-name policy: nil confusable skeletoner") } return policy, nil } // CanonicalKey returns the stable uniqueness key for raceName. func (policy *raceNamePolicy) CanonicalKey(raceName common.RaceName) (account.RaceNameCanonicalKey, 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") } if err := raceName.Validate(); err != nil { return "", fmt.Errorf("canonicalize race name: %w", err) } folded := policy.caseFolder.String(raceName.String()) antiFraudMapped := raceNameAntiFraudReplacer.Replace(folded) key := account.RaceNameCanonicalKey(policy.skeletoner.Skeleton(antiFraudMapped)) if err := key.Validate(); err != nil { return "", fmt.Errorf("canonicalize race name: %w", err) } return key, nil }