aurganize-backend/backend/pkg/slug/slug.go

129 lines
3.2 KiB
Go

package slug
import (
"crypto/rand"
"regexp"
"strconv"
"strings"
"time"
"unicode"
"github.com/rs/zerolog/log"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
)
func Generate(text string) string {
log.Debug().
Str("service", "slugService").
Str("action", "generate_slug_started").
Str("input_text", text).
Msg("starting slug generation")
text = strings.ToLower(text)
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
text, _, _ = transform.String(t, text)
text = strings.Replace(text, " ", "-", -1)
text = strings.Replace(text, "_", "-", -1)
reg := regexp.MustCompile(`[^a-z0-9-]+`)
text = reg.ReplaceAllString(text, "")
reg = regexp.MustCompile(`-+`)
text = reg.ReplaceAllString(text, "-")
text = strings.Trim(text, "-")
if len(text) > 100 {
log.Debug().
Str("service", "slugService").
Str("action", "slug_truncated").
Int("original_length", len(text)).
Msg("slug exceeded 100 characters and was truncated")
text = text[:100]
text = strings.TrimRight(text, "-")
}
if text == "" {
log.Warn().
Str("service", "slugService").
Str("action", "empty_slug_generated").
Str("input_text", text).
Msg("slug is empty after sanitization; generating random fallback")
text = "tenant" + generateRandomString(8)
}
return text
}
func GenerateUnique(base string, exists func(string) bool) string {
log.Debug().
Str("service", "slugService").
Str("action", "generate_unique_slug_started").
Str("base", base).
Msg("generating unique slug")
slug := Generate(base)
if !exists(slug) {
log.Debug().
Str("service", "slugService").
Str("action", "unique_slug_available").
Str("slug", slug).
Msg("slug is available without modification")
return slug
}
for i := 2; i < 1000; i++ {
candidate := slug + "-" + strconv.Itoa(i)
if !exists(candidate) {
log.Info().
Str("service", "slugService").
Str("action", "unique_slug_generated_with_suffix").
Str("slug", candidate).
Int("attempt", i-1).
Msg("slug collision resolved with numeric suffix")
return candidate
}
}
fallback := slug + "-" + strconv.Itoa(int(time.Now().Unix()))
log.Warn().
Str("service", "slugService").
Str("action", "unique_slug_fallback_timestamp").
Str("slug", fallback).
Msg("exhausted 1000 attempts; using timestamp fallback")
return fallback
}
func generateRandomString(length int) string {
log.Debug().
Str("service", "slugService").
Str("action", "generate_random_string").
Int("length", length).
Msg("generating random slug string")
const charset = "abcdefghijklmopqrstuvwxyz0123456789"
randBytes := make([]byte, length)
if _, err := rand.Read(randBytes); err != nil {
log.Error().
Str("service", "slugService").
Str("action", "rand_read_failed").
Err(err).
Msg("falling back to time-based random string")
return strconv.FormatInt(time.Now().UnixNano(), 36)[:length]
}
result := make([]byte, length)
for i := 0; i < length; i++ {
result[i] = charset[randBytes[i]%byte(len(charset))]
}
str := string(result)
log.Debug().
Str("service", "slugService").
Str("action", "random_string_generated").
Str("value", str).
Msg("random slug string generated")
return str
}