feat: game lobby service
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user