validation.go
package utils
import (
"fmt"
"regexp"
"time"
)
// ID length constants matching nanoid generation
const (
ShortIDLen = 12 // dataset, location, cluster, pattern, species, filter, call_type
)
// Sample rate reasonable bounds for audio recording
const (
MinSampleRate = 1000 // 1 kHz - below this is unlikely to be real audio
MaxSampleRate = 500000 // 500 kHz - well above bat detectors (~250kHz)
)
// Max string lengths from schema
const (
MaxNameLen = 140 // location.name, cluster.name
MaxDatasetNameLen = 255 // dataset.name
MaxDescriptionLen = 255 // all description fields
MaxPathLen = 255 // cluster.path
MaxFileNameLen = 255 // file.file_name
MaxTimezoneLen = 40 // location.timezone_id
)
// ID format regex - alphanumeric characters (nanoid uses A-Za-z0-9_)
var shortIDRegex = regexp.MustCompile(`^[A-Za-z0-9_-]{12}$`)
// ValidateShortID validates 12-character nanoid format
func ValidateShortID(id, fieldName string) error {
if id == "" {
return fmt.Errorf("%s cannot be empty", fieldName)
}
if len(id) != ShortIDLen {
return fmt.Errorf("%s must be exactly %d characters (got %d)", fieldName, ShortIDLen, len(id))
}
if !shortIDRegex.MatchString(id) {
return fmt.Errorf("%s has invalid format (expected alphanumeric nanoid)", fieldName)
}
return nil
}
// ValidateOptionalShortID validates short ID if provided (non-empty)
func ValidateOptionalShortID(id *string, fieldName string) error {
if id == nil || *id == "" {
return nil
}
return ValidateShortID(*id, fieldName)
}
// ValidateStringLength validates string length constraint
func ValidateStringLength(value, fieldName string, maxLen int) error {
if len(value) > maxLen {
return fmt.Errorf("%s must be %d characters or less (got %d)", fieldName, maxLen, len(value))
}
return nil
}
// ValidateOptionalStringLength validates string length if provided
func ValidateOptionalStringLength(value *string, fieldName string, maxLen int) error {
if value == nil || *value == "" {
return nil
}
return ValidateStringLength(*value, fieldName, maxLen)
}
// ValidateRange validates numeric range constraint (inclusive)
func ValidateRange[T int | float64](value T, fieldName string, min, max T) error {
if value < min || value > max {
return fmt.Errorf("%s must be between %v and %v (got %v)", fieldName, min, max, value)
}
return nil
}
// ValidatePositive validates positive number (> 0)
func ValidatePositive[T int | float64](value T, fieldName string) error {
if value <= 0 {
return fmt.Errorf("%s must be positive (got %v)", fieldName, value)
}
return nil
}
// ValidateNonNegative validates non-negative number (>= 0)
func ValidateNonNegative[T int | float64](value T, fieldName string) error {
if value < 0 {
return fmt.Errorf("%s must be non-negative (got %v)", fieldName, value)
}
return nil
}
// ValidateSampleRate validates audio sample rate is in reasonable range
func ValidateSampleRate(rate int) error {
return ValidateRange(rate, "sample_rate", MinSampleRate, MaxSampleRate)
}
// ValidateTimezone validates IANA timezone ID
func ValidateTimezone(tzID string) error {
if _, err := time.LoadLocation(tzID); err != nil {
return fmt.Errorf("invalid timezone_id '%s': %w", tzID, err)
}
return nil
}