Files
RenewCMS/internal/domain/author/authorDomain.go
T

181 lines
4.3 KiB
Go

package author
import (
"RenewCMS/internal/domain/slug"
"errors"
"net/url"
"strings"
"unicode/utf8"
)
var (
ErrAuthorNotFound = errors.New("author not found")
ErrSlugTaken = errors.New("an author with that slug already exists")
ErrAuthorHasArticles = errors.New("the author is still credited on articles")
ErrUserAlreadyLinked = errors.New("the user already has an author")
ErrAuthorAlreadyLinked = errors.New("the author is already linked to a user")
ErrAuthorRights = errors.New("only an owner or admin can change this author")
ErrBioTooLong = errors.New("the bio is too long")
ErrInvalidURL = errors.New("invalid url")
ErrTooManyLinks = errors.New("too many social links")
ErrInvalidSlug = errors.New("invalid slug")
ErrInvalidReassign = errors.New("invalid reassign destination")
ErrInvalidSocialType = errors.New("invalid social link type")
)
const (
MinNameRunes = 1
MaxNameRunes = 100
MaxSlugRunes = 100
MaxBioRunes = 280
MaxURLLength = 2048
MaxLinks = 5
FallbackSlug = "author"
SocialX = "x"
SocialLinkedIn = "linkedin"
SocialGitHub = "github"
SocialMastodon = "mastodon"
SocialCustom = "custom"
)
type SocialLink struct {
Type string `json:"type"`
URL string `json:"url"`
}
type Author struct {
ID uint32 `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Bio string `json:"bio"`
AvatarID *uint32 `json:"avatar_id"`
Website *string `json:"website"`
SocialLinks []SocialLink `json:"social_links"`
UserID *uint32 `json:"user_id"`
IsLinked bool `json:"is_linked"`
ArticleCount int `json:"article_count"`
UserEmail *string `json:"user_email,omitempty"`
}
type Byline struct {
ID uint32 `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Bio string `json:"bio"`
AvatarID *uint32 `json:"avatar_id"`
Website *string `json:"website"`
SocialLinks []SocialLink `json:"social_links"`
}
func (a Author) Byline() Byline {
links := a.SocialLinks
if links == nil {
links = []SocialLink{}
}
return Byline{ID: a.ID, Name: a.Name, Slug: a.Slug, Bio: a.Bio, AvatarID: a.AvatarID, Website: a.Website, SocialLinks: links}
}
func BylineIDs(bylines []Byline) []uint32 {
ids := make([]uint32, 0, len(bylines))
for _, byline := range bylines {
ids = append(ids, byline.ID)
}
return ids
}
type Nullable[T any] struct {
Set bool
Value *T
}
type Patch struct {
Name *string
Slug *string
Bio *string
AvatarID Nullable[uint32]
Website Nullable[string]
SocialLinks *[]SocialLink
UserID Nullable[uint32]
}
type ListQuery struct {
Q string
Page int
Size int
}
type Page struct {
Items []Author
Total int
}
func IsValidSlug(candidate string) bool {
return slug.IsValid(candidate)
}
func IsValidName(name string) bool {
length := utf8.RuneCountInString(strings.TrimSpace(name))
return length >= MinNameRunes && length <= MaxNameRunes
}
func ValidateBio(bio string) error {
if utf8.RuneCountInString(bio) > MaxBioRunes {
return ErrBioTooLong
}
return nil
}
func ValidateURL(raw string) error {
if raw == "" || len(raw) > MaxURLLength {
return ErrInvalidURL
}
parsed, err := url.Parse(raw)
if err != nil || parsed.Scheme != "https" || parsed.Hostname() == "" {
return ErrInvalidURL
}
return nil
}
func NormalizeWebsite(website *string) (*string, error) {
if website == nil || *website == "" {
return nil, nil
}
if err := ValidateURL(*website); err != nil {
return nil, err
}
return website, nil
}
func IsValidSocialType(linkType string) bool {
switch linkType {
case SocialX, SocialLinkedIn, SocialGitHub, SocialMastodon, SocialCustom:
return true
}
return false
}
func ValidateLinks(links []SocialLink) error {
if len(links) > MaxLinks {
return ErrTooManyLinks
}
for _, link := range links {
if !IsValidSocialType(link.Type) {
return ErrInvalidSocialType
}
if err := ValidateURL(link.URL); err != nil {
return err
}
}
return nil
}
func GenerateSlug(name string) string {
return slug.Generate(name, FallbackSlug)
}
func SlugWithSuffix(base string, attempt int) string {
return slug.WithSuffix(base, attempt)
}