129 lines
3.2 KiB
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
|
|
}
|