feat: errors - i18n (#111)
continuous-integration/drone/push Build is passing Details

Reviewed-on: #111
This commit is contained in:
Dawid Wysokiński 2023-06-27 12:00:35 +00:00
parent 0cc1f1637a
commit 6415b162de
18 changed files with 242 additions and 305 deletions

1
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/bwmarrin/discordgo v0.27.1
github.com/cenkalti/backoff/v4 v4.2.1
github.com/google/uuid v1.3.0
github.com/hashicorp/golang-lru/v2 v2.0.4
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa
github.com/kelseyhightower/envconfig v1.4.0
github.com/nicksnyder/go-i18n/v2 v2.2.1

2
go.sum
View File

@ -39,6 +39,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.4 h1:7GHuZcgid37q8o5i3QI9KMT4nCWQQ3Kx3Ov6bb9MfK0=
github.com/hashicorp/golang-lru/v2 v2.0.4/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw=

View File

@ -592,12 +592,14 @@ func (c *groupCommand) handle(s *discordgo.Session, i *discordgo.InteractionCrea
}
func (c *groupCommand) handleCreate(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
locale := string(i.Locale)
params, err := c.optionsToCreateGroupParams(s, i.GuildID, i.ApplicationCommandData().Options[0].Options)
if err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
Content: c.localizer.LocalizeError(locale, err),
},
})
return
@ -608,14 +610,14 @@ func (c *groupCommand) handleCreate(ctx context.Context, s *discordgo.Session, i
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
successMessageID := "cmd.group.create.success"
successMsg, err := c.localizer.Localize(string(i.Locale), &i18n.LocalizeConfig{
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{
MessageID: successMessageID,
TemplateData: map[string]any{
"GroupID": group.ID,
@ -673,7 +675,7 @@ func (c *groupCommand) handleList(ctx context.Context, s *discordgo.Session, i *
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
Content: c.localizer.LocalizeError(locale, err),
},
})
return
@ -735,12 +737,14 @@ func (c *groupCommand) handleList(ctx context.Context, s *discordgo.Session, i *
}
func (c *groupCommand) handleDetails(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
locale := string(i.Locale)
g, err := c.groupSvc.GetWithTribes(ctx, i.ApplicationCommandData().Options[0].Options[0].StringValue(), i.GuildID)
if err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
Content: c.localizer.LocalizeError(locale, err),
},
})
return
@ -767,7 +771,7 @@ func (c *groupCommand) handleDetails(ctx context.Context, s *discordgo.Session,
}
descriptionMessageID := "cmd.group.details.embed.description"
description, err := c.localizer.Localize(string(i.Locale), &i18n.LocalizeConfig{
description, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{
MessageID: descriptionMessageID,
TemplateData: map[string]any{
"ServerKey": g.ServerKey,
@ -782,7 +786,6 @@ func (c *groupCommand) handleDetails(ctx context.Context, s *discordgo.Session,
c.logger.Error("no message with the specified identifier", zap.String("id", descriptionMessageID), zap.Error(err))
return
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
@ -812,6 +815,8 @@ func (c *groupCommand) handleSet(ctx context.Context, s *discordgo.Session, i *d
}
func (c *groupCommand) handleSetChannelGains(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
locale := string(i.Locale)
group := ""
channel := ""
for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options {
@ -831,14 +836,14 @@ func (c *groupCommand) handleSetChannelGains(ctx context.Context, s *discordgo.S
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
successMessageID := "cmd.group.set.channel-gains.success"
successMsg, err := c.localizer.Localize(string(i.Locale), &i18n.LocalizeConfig{MessageID: successMessageID})
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
return
@ -853,6 +858,8 @@ func (c *groupCommand) handleSetChannelGains(ctx context.Context, s *discordgo.S
}
func (c *groupCommand) handleSetChannelLosses(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
locale := string(i.Locale)
group := ""
channel := ""
for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options {
@ -872,14 +879,14 @@ func (c *groupCommand) handleSetChannelLosses(ctx context.Context, s *discordgo.
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
successMessageID := "cmd.group.set.channel-losses.success"
successMsg, err := c.localizer.Localize(string(i.Locale), &i18n.LocalizeConfig{MessageID: successMessageID})
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
return
@ -894,6 +901,8 @@ func (c *groupCommand) handleSetChannelLosses(ctx context.Context, s *discordgo.
}
func (c *groupCommand) handleSetInternals(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
locale := string(i.Locale)
group := ""
internals := false
for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options {
@ -913,14 +922,14 @@ func (c *groupCommand) handleSetInternals(ctx context.Context, s *discordgo.Sess
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
successMessageID := "cmd.group.set.internals.success"
successMsg, err := c.localizer.Localize(string(i.Locale), &i18n.LocalizeConfig{MessageID: successMessageID})
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
return
@ -935,6 +944,8 @@ func (c *groupCommand) handleSetInternals(ctx context.Context, s *discordgo.Sess
}
func (c *groupCommand) handleSetBarbarians(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
locale := string(i.Locale)
group := ""
barbarians := false
for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options {
@ -954,14 +965,14 @@ func (c *groupCommand) handleSetBarbarians(ctx context.Context, s *discordgo.Ses
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
successMessageID := "cmd.group.set.barbarians.success"
successMsg, err := c.localizer.Localize(string(i.Locale), &i18n.LocalizeConfig{MessageID: successMessageID})
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
return
@ -985,6 +996,8 @@ func (c *groupCommand) handleTribe(ctx context.Context, s *discordgo.Session, i
}
func (c *groupCommand) handleTribeAdd(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
locale := string(i.Locale)
group := ""
tag := ""
for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options {
@ -1004,14 +1017,14 @@ func (c *groupCommand) handleTribeAdd(ctx context.Context, s *discordgo.Session,
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
successMessageID := "cmd.group.tribe.add.success"
successMsg, err := c.localizer.Localize(string(i.Locale), &i18n.LocalizeConfig{MessageID: successMessageID})
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
return
@ -1026,6 +1039,8 @@ func (c *groupCommand) handleTribeAdd(ctx context.Context, s *discordgo.Session,
}
func (c *groupCommand) handleTribeRemove(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
locale := string(i.Locale)
group := ""
tag := ""
for _, opt := range i.ApplicationCommandData().Options[0].Options[0].Options {
@ -1045,14 +1060,14 @@ func (c *groupCommand) handleTribeRemove(ctx context.Context, s *discordgo.Sessi
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
successMessageID := "cmd.group.tribe.remove.success"
successMsg, err := c.localizer.Localize(string(i.Locale), &i18n.LocalizeConfig{MessageID: successMessageID})
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
return
@ -1067,19 +1082,21 @@ func (c *groupCommand) handleTribeRemove(ctx context.Context, s *discordgo.Sessi
}
func (c *groupCommand) handleDelete(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
locale := string(i.Locale)
group := i.ApplicationCommandData().Options[0].Options[0].StringValue()
if err := c.groupSvc.Delete(ctx, group, i.GuildID); err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: messageFromError(err),
Content: c.localizer.LocalizeError(locale, err),
},
})
return
}
successMessageID := "cmd.group.delete.success"
successMsg, err := c.localizer.Localize(string(i.Locale), &i18n.LocalizeConfig{MessageID: successMessageID})
successMsg, err := c.localizer.Localize(locale, &i18n.LocalizeConfig{MessageID: successMessageID})
if err != nil {
c.logger.Error("no message with the specified identifier", zap.String("id", successMessageID), zap.Error(err))
return

View File

@ -1,25 +1,14 @@
package discord
import (
"errors"
"fmt"
"time"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
)
const (
embedDescriptionCharLimit = 4096
)
func messageFromError(err error) string {
var userErr domain.Error
if !errors.As(err, &userErr) {
return "something went wrong, please try again later"
}
return userErr.UserError()
}
func formatTimestamp(t time.Time) string {
return t.Format(time.RFC3339)
}

View File

@ -1,4 +1,17 @@
{
"err.default": "Something went wrong. Try again later.",
"err.required": "{{ .Field }} can't be blank.",
"err.group-limit-reached": "The group limit has been reached ({{ .Current }}/{{ .Limit }}).",
"err.group-not-found": "Group (id={{ .ID }}) not found.",
"err.group-does-not-exist": "The group (id={{ .ID }}) doesn't exist.",
"err.monitor-already-exists": "The tribe has already been added to the group.",
"err.monitor-limit-reached": "The tribe limit has been reached ({{ .Current }}/{{ .Limit }}).",
"err.monitor-not-found": "Monitor (id={{ .ID }}) not found.",
"err.tw-server-does-not-exist": "The server (version code={{ .VersionCode }}, key={{ .Key }}) doesn't exist.",
"err.tw-server-is-closed": "The server (version code={{ .VersionCode }}, key={{ .Key }}) is closed.",
"err.tribe-not-found": "Tribe (tag={{ .Tag }}) not found.",
"err.tribe-does-not-exist": "The tribe (tag={{ .Tag }}) doesn't exist.",
"cmd.group.description": "Manages groups on this server",
"cmd.group.create.description": "Creates a new monitor group",

View File

@ -1,4 +1,17 @@
{
"err.default": "Coś poszło nie tak. Spróbuj ponownie później.",
"err.required": "{{ .Field }} nie może być pusty.",
"err.group-limit-reached": "Limit grup został osiągnięty ({{ .Current }}/{{ .Limit }}).",
"err.group-not-found": "Grupa (id={{ .ID }}) nie została znaleziona.",
"err.group-does-not-exist": "Grupa (id={{ .ID }}) nie istnieje.",
"err.monitor-already-exists": "Plemię zostało już dodane do tej grupy.",
"err.monitor-limit-reached": "Limit plemion został osiągnięty ({{ .Current }}/{{ .Limit }}).",
"err.monitor-not-found": "Monitor (id={{ .ID }}) nie została znaleziona.",
"err.tw-server-does-not-exist": "Serwer (kod wersji={{ .VersionCode }}, klucz={{ .Key }}) nie istnieje.",
"err.tw-server-is-closed": "Serwer (kod wersji={{ .VersionCode }}, klucz={{ .Key }}) jest zamknięty.",
"err.tribe-not-found": "Plemię (skrót={{ .Tag }}) nie zostało znalezione.",
"err.tribe-does-not-exist": "Plemię (skrót={{ .Tag }}) nie istnieje.",
"cmd.group.description": "Umożliwia zarządzanie grupami na tym serwerze",
"cmd.group.create.description": "Tworzy nową grupę",

View File

@ -1,15 +1,21 @@
package discordi18n
import (
"errors"
"fmt"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/bwmarrin/discordgo"
lru "github.com/hashicorp/golang-lru/v2"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
const localizerCacheSize = 128
type Localizer struct {
bundle *i18n.Bundle
cache *lru.Cache[string, *i18n.Localizer]
defaultLanguage language.Tag
}
@ -18,17 +24,35 @@ func NewLocalizer(defaultLanguage language.Tag) (*Localizer, error) {
if err != nil {
return nil, err
}
l, err := lru.New[string, *i18n.Localizer](localizerCacheSize)
if err != nil {
return nil, err
}
return &Localizer{
bundle: b,
cache: l,
}, nil
}
func (l *Localizer) Localize(lang string, lc *i18n.LocalizeConfig) (string, error) {
return i18n.NewLocalizer(l.bundle, lang, l.defaultLanguage.String()).Localize(lc)
return l.newLocalizer(lang).Localize(lc)
}
func (l *Localizer) MustLocalize(lang string, lc *i18n.LocalizeConfig) string {
return l.newLocalizer(lang).MustLocalize(lc)
}
func (l *Localizer) LocalizeWithTag(lang string, lc *i18n.LocalizeConfig) (string, language.Tag, error) {
return i18n.NewLocalizer(l.bundle, lang, l.defaultLanguage.String()).LocalizeWithTag(lc)
return l.newLocalizer(lang).LocalizeWithTag(lc)
}
func (l *Localizer) newLocalizer(lang string) *i18n.Localizer {
if loc, ok := l.cache.Get(lang); ok {
return loc
}
loc := i18n.NewLocalizer(l.bundle, lang, l.defaultLanguage.String())
_ = l.cache.Add(lang, loc)
return loc
}
func (l *Localizer) LocalizeDiscord(messageID string) (string, map[discordgo.Locale]string, error) {
@ -66,3 +90,25 @@ func (l *Localizer) LocalizeDiscordWithConfig(lc *i18n.LocalizeConfig) (string,
return defaultMsg, m, nil
}
func (l *Localizer) LocalizeError(lang string, errToLocalize error) string {
defaultErr, err := l.Localize(lang, &i18n.LocalizeConfig{MessageID: "err.default"})
if err != nil {
defaultErr = "Something went wrong. Try again later."
}
var translatableErr domain.TranslatableError
if !errors.As(errToLocalize, &translatableErr) {
return defaultErr
}
msgID := "err." + translatableErr.Slug()
msg, err := l.Localize(lang, &i18n.LocalizeConfig{
MessageID: msgID,
TemplateData: translatableErr.Params(),
})
if err != nil {
return defaultErr
}
return msg
}

View File

@ -5,43 +5,30 @@ import (
"fmt"
)
var (
ErrNothingToUpdate = errors.New("nothing to update")
ErrRequired = errors.New("cannot be blank")
)
var ErrNothingToUpdate = errors.New("nothing to update")
type ErrorCode uint8
const (
ErrorCodeUnknown ErrorCode = iota
ErrorCodeEntityNotFound
ErrorCodeValidationError
ErrorCodeAlreadyExists
)
type Error interface {
type TranslatableError interface {
error
UserError() string
Code() ErrorCode
Slug() string
Params() map[string]any
}
type ValidationError struct {
type RequiredError struct {
Field string
Err error
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Err)
var _ TranslatableError = RequiredError{}
func (e RequiredError) Error() string {
return fmt.Sprintf("%s can't be blank", e.Field)
}
func (e ValidationError) UserError() string {
return e.Error()
func (e RequiredError) Slug() string {
return "required"
}
func (e ValidationError) Code() ErrorCode {
return ErrorCodeValidationError
}
func (e ValidationError) Unwrap() error {
return e.Err
func (e RequiredError) Params() map[string]any {
return map[string]any{
"Field": e.Field,
}
}

View File

@ -1,24 +0,0 @@
package domain_test
import (
"errors"
"fmt"
"testing"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/stretchr/testify/assert"
)
func TestValidationError(t *testing.T) {
t.Parallel()
err := domain.ValidationError{
Field: "test",
Err: errors.New("err"),
}
var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("%s: %s", err.Field, err.Err.Error()), err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.ErrorIs(t, err, err.Err)
assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
}

View File

@ -12,7 +12,7 @@ type Group struct {
ChannelLosses string
Internals bool // Show conquers in the same group
Barbarians bool // Show barbarian conquers
ServerKey string // Tribal Wars server key
ServerKey string // TW server key (e.g. en130)
VersionCode string
CreatedAt time.Time
}
@ -44,24 +44,15 @@ func NewCreateGroupParams(
barbarians, internals bool,
) (CreateGroupParams, error) {
if serverID == "" {
return CreateGroupParams{}, ValidationError{
Field: "ServerID",
Err: ErrRequired,
}
return CreateGroupParams{}, RequiredError{Field: "ServerID"}
}
if versionCode == "" {
return CreateGroupParams{}, ValidationError{
Field: "VersionCode",
Err: ErrRequired,
}
return CreateGroupParams{}, RequiredError{Field: "VersionCode"}
}
if serverKey == "" {
return CreateGroupParams{}, ValidationError{
Field: "ServerKey",
Err: ErrRequired,
}
return CreateGroupParams{}, RequiredError{Field: "ServerKey"}
}
return CreateGroupParams{
@ -130,46 +121,59 @@ type GroupLimitReachedError struct {
Limit int // maximum number of groups
}
var _ TranslatableError = GroupLimitReachedError{}
func (e GroupLimitReachedError) Error() string {
return fmt.Sprintf("group limit has been reached (%d/%d)", e.Current, e.Limit)
}
func (e GroupLimitReachedError) UserError() string {
return e.Error()
func (e GroupLimitReachedError) Slug() string {
return "group-limit-reached"
}
func (e GroupLimitReachedError) Code() ErrorCode {
return ErrorCodeValidationError
func (e GroupLimitReachedError) Params() map[string]any {
return map[string]any{
"Current": e.Current,
"Limit": e.Limit,
}
}
type GroupNotFoundError struct {
ID string
}
var _ TranslatableError = GroupNotFoundError{}
func (e GroupNotFoundError) Error() string {
return fmt.Sprintf("group (id=%s) not found", e.ID)
}
func (e GroupNotFoundError) UserError() string {
return e.Error()
func (e GroupNotFoundError) Slug() string {
return "group-not-found"
}
func (e GroupNotFoundError) Code() ErrorCode {
return ErrorCodeEntityNotFound
func (e GroupNotFoundError) Params() map[string]any {
return map[string]any{
"ID": e.ID,
}
}
type GroupDoesNotExistError struct {
ID string
}
var _ TranslatableError = GroupDoesNotExistError{}
func (e GroupDoesNotExistError) Error() string {
return fmt.Sprintf("group (id=%s) doesn't exist", e.ID)
}
func (e GroupDoesNotExistError) UserError() string {
return e.Error()
func (e GroupDoesNotExistError) Slug() string {
return "group-does-not-exist"
}
func (e GroupDoesNotExistError) Code() ErrorCode {
return ErrorCodeValidationError
func (e GroupDoesNotExistError) Params() map[string]any {
return map[string]any{
"ID": e.ID,
}
}

View File

@ -1,11 +1,9 @@
package domain_test
import (
"fmt"
"testing"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
@ -38,29 +36,20 @@ func TestNewCreateGroupParams(t *testing.T) {
name: "ERR: ServerID cannot be blank",
versionCode: "",
serverID: "",
err: domain.ValidationError{
Field: "ServerID",
Err: domain.ErrRequired,
},
err: domain.RequiredError{Field: "ServerID"},
},
{
name: "ERR: VersionCode cannot be blank",
serverID: "1234",
versionCode: "",
err: domain.ValidationError{
Field: "VersionCode",
Err: domain.ErrRequired,
},
err: domain.RequiredError{Field: "VersionCode"},
},
{
name: "ERR: ServerKey cannot be blank",
serverID: "1234",
versionCode: "en",
serverKey: "",
err: domain.ValidationError{
Field: "ServerKey",
Err: domain.ErrRequired,
},
err: domain.RequiredError{Field: "ServerKey"},
},
}
@ -183,40 +172,3 @@ func TestUpdateGroupParams_IsZero(t *testing.T) {
})
}
}
func TestGroupLimitReachedError(t *testing.T) {
t.Parallel()
err := domain.GroupLimitReachedError{
Current: 10,
Limit: 10,
}
var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("group limit has been reached (%d/%d)", err.Current, err.Limit), err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
}
func TestGroupNotFoundError(t *testing.T) {
t.Parallel()
err := domain.GroupNotFoundError{
ID: uuid.NewString(),
}
var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("group (id=%s) not found", err.ID), err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code())
}
func TestGroupDoesNotExistError(t *testing.T) {
t.Parallel()
err := domain.GroupDoesNotExistError{
ID: uuid.NewString(),
}
var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("group (id=%s) doesn't exist", err.ID), err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
}

View File

@ -22,23 +22,30 @@ type MonitorAlreadyExistsError struct {
GroupID string
}
var _ TranslatableError = MonitorAlreadyExistsError{}
func (e MonitorAlreadyExistsError) Error() string {
return fmt.Sprintf("monitor (groupID=%s,tribeID=%d) already exists", e.GroupID, e.TribeID)
}
func (e MonitorAlreadyExistsError) UserError() string {
return e.Error()
func (e MonitorAlreadyExistsError) Slug() string {
return "monitor-already-exists"
}
func (e MonitorAlreadyExistsError) Code() ErrorCode {
return ErrorCodeAlreadyExists
func (e MonitorAlreadyExistsError) Params() map[string]any {
return map[string]any{
"TribeID": e.TribeID,
"GroupID": e.GroupID,
}
}
type MonitorLimitReachedError struct {
Current int // current number of groups
Limit int // maximum number of groups
Limit int // max number of groups
}
var _ TranslatableError = MonitorLimitReachedError{}
func (e MonitorLimitReachedError) Error() string {
return fmt.Sprintf("monitor limit has been reached (%d/%d)", e.Current, e.Limit)
}
@ -47,14 +54,23 @@ func (e MonitorLimitReachedError) UserError() string {
return e.Error()
}
func (e MonitorLimitReachedError) Code() ErrorCode {
return ErrorCodeValidationError
func (e MonitorLimitReachedError) Slug() string {
return "monitor-limit-reached"
}
func (e MonitorLimitReachedError) Params() map[string]any {
return map[string]any{
"Current": e.Current,
"Limit": e.Limit,
}
}
type MonitorNotFoundError struct {
ID string
}
var _ TranslatableError = MonitorNotFoundError{}
func (e MonitorNotFoundError) Error() string {
return fmt.Sprintf("monitor (id=%s) not found", e.ID)
}
@ -63,6 +79,12 @@ func (e MonitorNotFoundError) UserError() string {
return e.Error()
}
func (e MonitorNotFoundError) Code() ErrorCode {
return ErrorCodeEntityNotFound
func (e MonitorNotFoundError) Slug() string {
return "monitor-not-found"
}
func (e MonitorNotFoundError) Params() map[string]any {
return map[string]any{
"ID": e.ID,
}
}

View File

@ -1,48 +0,0 @@
package domain_test
import (
"fmt"
"testing"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestMonitorAlreadyExistsError(t *testing.T) {
t.Parallel()
err := domain.MonitorAlreadyExistsError{
GroupID: uuid.NewString(),
TribeID: 1234,
}
var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("monitor (groupID=%s,tribeID=%d) already exists", err.GroupID, err.TribeID), err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeAlreadyExists, err.Code())
}
func TestMonitorLimitReachedError(t *testing.T) {
t.Parallel()
err := domain.MonitorLimitReachedError{
Current: 10,
Limit: 10,
}
var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("monitor limit has been reached (%d/%d)", err.Current, err.Limit), err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
}
func TestMonitorNotFoundError(t *testing.T) {
t.Parallel()
err := domain.MonitorNotFoundError{
ID: uuid.NewString(),
}
var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("monitor (id=%s) not found", err.ID), err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code())
}

View File

@ -43,44 +43,56 @@ type Ennoblement struct {
CreatedAt time.Time
}
type ServerDoesNotExistError struct {
type TWServerDoesNotExistError struct {
VersionCode string
Key string
}
func (e ServerDoesNotExistError) Error() string {
var _ TranslatableError = TWServerDoesNotExistError{}
func (e TWServerDoesNotExistError) Error() string {
return fmt.Sprintf("server (versionCode=%s,key=%s) doesn't exist", e.VersionCode, e.Key)
}
func (e ServerDoesNotExistError) UserError() string {
return e.Error()
func (e TWServerDoesNotExistError) Slug() string {
return "tw-server-does-not-exist"
}
func (e ServerDoesNotExistError) Code() ErrorCode {
return ErrorCodeValidationError
func (e TWServerDoesNotExistError) Params() map[string]any {
return map[string]any{
"VersionCode": e.VersionCode,
"Key": e.Key,
}
}
type ServerIsClosedError struct {
type TWServerIsClosedError struct {
VersionCode string
Key string
}
func (e ServerIsClosedError) Error() string {
var _ TranslatableError = TWServerIsClosedError{}
func (e TWServerIsClosedError) Error() string {
return fmt.Sprintf("server (versionCode=%s,key=%s) is closed", e.VersionCode, e.Key)
}
func (e ServerIsClosedError) UserError() string {
return e.Error()
func (e TWServerIsClosedError) Slug() string {
return "tw-server-is-closed"
}
func (e ServerIsClosedError) Code() ErrorCode {
return ErrorCodeValidationError
func (e TWServerIsClosedError) Params() map[string]any {
return map[string]any{
"VersionCode": e.VersionCode,
"Key": e.Key,
}
}
type TribeDoesNotExistError struct {
Tag string
}
var _ TranslatableError = TribeDoesNotExistError{}
func (e TribeDoesNotExistError) Error() string {
return fmt.Sprintf("tribe (tag=%s) doesn't exist", e.Tag)
}
@ -89,22 +101,32 @@ func (e TribeDoesNotExistError) UserError() string {
return e.Error()
}
func (e TribeDoesNotExistError) Code() ErrorCode {
return ErrorCodeValidationError
func (e TribeDoesNotExistError) Slug() string {
return "tribe-does-not-exist"
}
func (e TribeDoesNotExistError) Params() map[string]any {
return map[string]any{
"Tag": e.Tag,
}
}
type TribeNotFoundError struct {
Tag string
}
var _ TranslatableError = TribeNotFoundError{}
func (e TribeNotFoundError) Error() string {
return fmt.Sprintf("tribe (tag=%s) not found", e.Tag)
}
func (e TribeNotFoundError) UserError() string {
return e.Error()
func (e TribeNotFoundError) Slug() string {
return "tribe-not-found"
}
func (e TribeNotFoundError) Code() ErrorCode {
return ErrorCodeEntityNotFound
func (e TribeNotFoundError) Params() map[string]any {
return map[string]any{
"Tag": e.Tag,
}
}

View File

@ -1,59 +0,0 @@
package domain_test
import (
"fmt"
"testing"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/stretchr/testify/assert"
)
func TestServerDoesNotExistError(t *testing.T) {
t.Parallel()
err := domain.ServerDoesNotExistError{
VersionCode: "pl",
Key: "pl151",
}
var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("server (versionCode=%s,key=%s) doesn't exist", err.VersionCode, err.Key), err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
}
func TestServerIsClosedError(t *testing.T) {
t.Parallel()
err := domain.ServerIsClosedError{
VersionCode: "pl",
Key: "pl151",
}
var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("server (versionCode=%s,key=%s) is closed", err.VersionCode, err.Key), err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
}
func TestTribeDoesNotExistError(t *testing.T) {
t.Parallel()
err := domain.TribeDoesNotExistError{
Tag: "*TAG*",
}
var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("tribe (tag=%s) doesn't exist", err.Tag), err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
}
func TestTribeNotFoundError(t *testing.T) {
t.Parallel()
err := domain.TribeNotFoundError{
Tag: "*TAG*",
}
var _ domain.Error = err
assert.Equal(t, fmt.Sprintf("tribe (tag=%s) not found", err.Tag), err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code())
}

View File

@ -78,14 +78,14 @@ func (g *Group) checkTWServer(ctx context.Context, versionCode, serverKey string
if !errors.As(err, &apiErr) || apiErr.Code != twhelp.ErrorCodeEntityNotFound {
return fmt.Errorf("TWHelpClient.GetServer: %w", err)
}
return domain.ServerDoesNotExistError{
return domain.TWServerDoesNotExistError{
VersionCode: versionCode,
Key: serverKey,
}
}
if !server.Open {
return domain.ServerIsClosedError{
return domain.TWServerIsClosedError{
VersionCode: versionCode,
Key: serverKey,
}
@ -406,7 +406,7 @@ func (g *Group) listEnnoblements(ctx context.Context, groups []domain.GroupWithM
group.ServerKey,
twhelp.ListEnnoblementsQueryParams{
Since: since,
Sort: []twhelp.ListEnnoblementsSort{twhelp.ListEnnoblementsSortCreatedAtASC},
Sort: []twhelp.EnnoblementSort{twhelp.EnnoblementSortCreatedAtASC},
},
)
if err != nil {

View File

@ -111,7 +111,7 @@ func TestGroup_Create(t *testing.T) {
params := newParams(t)
g, err := service.NewGroup(repo, client, zap.NewNop(), 1, 1).Create(context.Background(), params)
assert.ErrorIs(t, err, domain.ServerDoesNotExistError{
assert.ErrorIs(t, err, domain.TWServerDoesNotExistError{
VersionCode: params.VersionCode(),
Key: params.ServerKey(),
})
@ -135,7 +135,7 @@ func TestGroup_Create(t *testing.T) {
params := newParams(t)
g, err := service.NewGroup(repo, client, zap.NewNop(), 1, 1).Create(context.Background(), params)
assert.ErrorIs(t, err, domain.ServerIsClosedError{
assert.ErrorIs(t, err, domain.TWServerIsClosedError{
VersionCode: params.VersionCode(),
Key: params.ServerKey(),
})

View File

@ -163,13 +163,13 @@ func (c *Client) GetTribeByID(ctx context.Context, version, server string, id in
return resp.Data, nil
}
type ListEnnoblementsSort string
type EnnoblementSort string
const (
ListEnnoblementsSortCreatedAtASC ListEnnoblementsSort = "createdAt:ASC"
EnnoblementSortCreatedAtASC EnnoblementSort = "createdAt:ASC"
)
func (s ListEnnoblementsSort) String() string {
func (s EnnoblementSort) String() string {
return string(s)
}
@ -177,7 +177,7 @@ type ListEnnoblementsQueryParams struct {
Limit int32
Offset int32
Since time.Time
Sort []ListEnnoblementsSort
Sort []EnnoblementSort
}
func (c *Client) ListEnnoblements(