package shared import ( "fmt" "strings" "time" "galaxy/user/internal/domain/common" "golang.org/x/text/language" ) // NormalizeString trims surrounding Unicode whitespace from value. func NormalizeString(value string) string { return strings.TrimSpace(value) } // ParseEmail trims value and validates it as one exact normalized e-mail // subject used by the auth-facing contract. func ParseEmail(value string) (common.Email, error) { email := common.Email(NormalizeString(value)) if err := email.Validate(); err != nil { return "", InvalidRequest(err.Error()) } return email, nil } // ParseUserID trims value and validates it as one stable user identifier. func ParseUserID(value string) (common.UserID, error) { userID := common.UserID(NormalizeString(value)) if err := userID.Validate(); err != nil { return "", InvalidRequest(err.Error()) } return userID, nil } // ParseRaceName trims value and validates it as one exact stored race name. func ParseRaceName(value string) (common.RaceName, error) { raceName := common.RaceName(NormalizeString(value)) if err := raceName.Validate(); err != nil { return "", InvalidRequest(err.Error()) } return raceName, nil } // ParseReasonCode trims value and validates it as one machine-readable reason // code. func ParseReasonCode(value string) (common.ReasonCode, error) { reasonCode := common.ReasonCode(NormalizeString(value)) if err := reasonCode.Validate(); err != nil { return "", InvalidRequest(err.Error()) } return reasonCode, nil } // ParseLanguageTag trims value and validates it against the current Stage 03 // boundary and BCP 47 semantics, returning the canonical tag form. func ParseLanguageTag(value string) (common.LanguageTag, error) { languageTag := common.LanguageTag(NormalizeString(value)) if err := languageTag.Validate(); err != nil { return "", InvalidRequest(err.Error()) } parsedTag, err := language.Parse(languageTag.String()) if err != nil { return "", InvalidRequest("language tag must be a valid BCP 47 language tag") } canonicalTag := common.LanguageTag(parsedTag.String()) if err := canonicalTag.Validate(); err != nil { return "", InvalidRequest(err.Error()) } return canonicalTag, nil } // ParseTimeZoneName trims value and validates it against the current Stage 03 // boundary and IANA time-zone semantics. func ParseTimeZoneName(value string) (common.TimeZoneName, error) { timeZoneName := common.TimeZoneName(NormalizeString(value)) if err := timeZoneName.Validate(); err != nil { return "", InvalidRequest(err.Error()) } if _, err := time.LoadLocation(timeZoneName.String()); err != nil { return "", InvalidRequest("time zone name must be a valid IANA time zone name") } return timeZoneName, nil } // ParseRegistrationPreferredLanguage trims value, validates it as one create- // only BCP 47 registration language tag, and returns the canonical tag form. func ParseRegistrationPreferredLanguage(value string) (common.LanguageTag, error) { languageTag, err := ParseLanguageTag(value) if err != nil { return "", reframeFieldError("registration_context.preferred_language", "language tag", err) } return languageTag, nil } // ParseRegistrationTimeZoneName trims value and validates it as one create- // only IANA registration time-zone name. func ParseRegistrationTimeZoneName(value string) (common.TimeZoneName, error) { timeZoneName, err := ParseTimeZoneName(value) if err != nil { return "", reframeFieldError("registration_context.time_zone", "time zone name", err) } return timeZoneName, nil } func reframeFieldError(fieldName string, valueName string, err error) error { if err == nil { return nil } message := err.Error() prefix := valueName + " " if strings.HasPrefix(message, prefix) { message = fieldName + " " + strings.TrimPrefix(message, prefix) } else { message = fmt.Sprintf("%s: %s", fieldName, message) } return InvalidRequest(message) }