176 lines
4.2 KiB
Go
176 lines
4.2 KiB
Go
package shortservice
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"regexp"
|
|
|
|
"git.maronato.dev/maronato/goshort/internal/errs"
|
|
"git.maronato.dev/maronato/goshort/internal/storage"
|
|
"git.maronato.dev/maronato/goshort/internal/storage/models"
|
|
randomutil "git.maronato.dev/maronato/goshort/internal/util/random"
|
|
)
|
|
|
|
const (
|
|
// DefaultShortLength is the default length of the short URL.
|
|
DefaultShortLength = 5
|
|
// MinShortLength is the minimum length of the short URL.
|
|
MinShortLength = 4
|
|
// MaxShortLength is the maximum length of the short URL.
|
|
MaxShortLength = 20
|
|
)
|
|
|
|
type ShortService struct {
|
|
db storage.Storage
|
|
}
|
|
|
|
func NewShortService(db storage.Storage) *ShortService {
|
|
return &ShortService{db: db}
|
|
}
|
|
|
|
func (s *ShortService) FindShort(ctx context.Context, name string) (*models.Short, error) {
|
|
// Check if the short is valid
|
|
err := ShortNameIsValid(name)
|
|
if err != nil {
|
|
return &models.Short{}, fmt.Errorf("could not validate short: %w", err)
|
|
}
|
|
|
|
// Get the short from storage
|
|
short, err := s.db.FindShort(ctx, name)
|
|
if err != nil {
|
|
return short, fmt.Errorf("could not get short from storage: %w", err)
|
|
}
|
|
|
|
return short, nil
|
|
}
|
|
|
|
func (s *ShortService) FindShortByID(ctx context.Context, id string) (*models.Short, error) {
|
|
// Check if the ID is valid
|
|
if !models.LooksLikeID(id) {
|
|
return &models.Short{}, errs.ErrInvalidShort
|
|
}
|
|
|
|
// Get the short from storage
|
|
short, err := s.db.FindShortByID(ctx, id)
|
|
if err != nil {
|
|
return short, fmt.Errorf("could not get short from storage: %w", err)
|
|
}
|
|
|
|
return short, nil
|
|
}
|
|
|
|
// ShortenURL shortens a URL with a random short name.
|
|
func (s *ShortService) generateUnusedShortName(ctx context.Context) (string, error) {
|
|
// Generate a random short URL
|
|
for {
|
|
shortName := randomutil.GenerateRandomShort(DefaultShortLength)
|
|
|
|
if _, err := s.FindShort(ctx, shortName); err != nil {
|
|
if errors.Is(err, errs.ErrShortDoesNotExist) {
|
|
return shortName, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("failed to generate unused short name: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *ShortService) Shorten(ctx context.Context, short *models.Short) (*models.Short, error) {
|
|
// Check if the short is empty
|
|
if short.Name == "" {
|
|
// Generate a random short name
|
|
newName, err := s.generateUnusedShortName(ctx)
|
|
if err != nil {
|
|
return &models.Short{}, fmt.Errorf("failed to shorten URL: %w", err)
|
|
}
|
|
|
|
short.Name = newName
|
|
}
|
|
|
|
// make sure the short is valid
|
|
err := ShortIsValid(short)
|
|
if err != nil {
|
|
return &models.Short{}, fmt.Errorf("could not validate short: %w", err)
|
|
}
|
|
|
|
// Save the short in storage
|
|
newShort, err := s.db.CreateShort(ctx, short)
|
|
if err != nil {
|
|
return &models.Short{}, fmt.Errorf("could not save short in storage: %w", err)
|
|
}
|
|
|
|
return newShort, nil
|
|
}
|
|
|
|
func (s *ShortService) ListShorts(ctx context.Context, user *models.User) ([]*models.Short, error) {
|
|
// Find shorts
|
|
shorts, err := s.db.ListShorts(ctx, user)
|
|
if err != nil {
|
|
return shorts, fmt.Errorf("could not get shorts from storage: %w", err)
|
|
}
|
|
|
|
return shorts, nil
|
|
}
|
|
|
|
func (s *ShortService) DeleteShort(ctx context.Context, short *models.Short) error {
|
|
// Delete short
|
|
err := s.db.DeleteShort(ctx, short)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete short: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var shortRegex = regexp.MustCompile(
|
|
fmt.Sprintf("^%s$",
|
|
fmt.Sprintf("[a-zA-Z0-9_-]{%d,%d}", MinShortLength, MaxShortLength)),
|
|
)
|
|
|
|
func ShortNameIsValid(name string) error {
|
|
if !shortRegex.MatchString(name) {
|
|
return errs.Errorf(
|
|
fmt.Sprintf(
|
|
"short must use only letters, numbers, underscores and dashes, and be between %d and %d characters long",
|
|
MinShortLength,
|
|
MaxShortLength,
|
|
),
|
|
errs.ErrInvalidShort,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ShortURLIsValid(shortURL string) error {
|
|
parsedURL, err := url.ParseRequestURI(shortURL)
|
|
if err != nil {
|
|
return errs.Errorf("invalid URL", errs.ErrInvalidShort)
|
|
}
|
|
|
|
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
|
return errs.Errorf("invalid URL scheme", errs.ErrInvalidShort)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ShortIsValid checks if the short is valid.
|
|
func ShortIsValid(short *models.Short) error {
|
|
// Check if the short name is valid
|
|
err := ShortNameIsValid(short.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if the URL is valid
|
|
err = ShortURLIsValid(short.URL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|