api-qobiltu-dev / pkg /validation /custom_rules.go
lifedebugger's picture
Deploy files from GitHub repository
1cea019
package validation
import (
"context"
"fmt"
"reflect"
"regexp"
"strings"
"sync"
"time"
v10 "github.com/go-playground/validator/v10"
"gorm.io/gorm"
)
type ValidatorOptionSource interface {
GetValidOptions(key string) ([]string, error)
GetValidKeys() []string
Refresh() error
StartAutoRefresh(ctx context.Context, interval time.Duration)
HasKey(key string) bool
}
// --------------------
// DBOptionSource with Safe Handling
// --------------------
type DBOptionSource struct {
db *gorm.DB
options map[string][]string
mu sync.RWMutex
lastUpdate time.Time
expiry time.Duration
stopChan chan struct{}
}
func NewDBOptionSource(db *gorm.DB, expiry time.Duration) (*DBOptionSource, error) {
source := &DBOptionSource{
db: db,
expiry: expiry,
stopChan: make(chan struct{}),
}
if err := source.Refresh(); err != nil {
return nil, fmt.Errorf("failed to initialize DB option source: %w", err)
}
return source, nil
}
func (s *DBOptionSource) Refresh() error {
s.mu.Lock()
defer s.mu.Unlock()
// Buat session baru tanpa transaction
tx := s.db.Session(&gorm.Session{SkipDefaultTransaction: true})
var results []struct {
Slug string `gorm:"column:slug"`
Value string `gorm:"column:value"`
}
err := tx.Raw(`
SELECT
c.option_slug AS slug,
v.option_value AS value
FROM option_categories c
JOIN option_values v ON c.id = v.option_category_id
ORDER BY c.id, v.id
`).Scan(&results).Error
if err != nil {
return fmt.Errorf("failed to refresh options: %w", err)
}
newOptions := make(map[string][]string)
for _, r := range results {
newOptions[r.Slug] = append(newOptions[r.Slug], r.Value)
}
s.options = newOptions
s.lastUpdate = time.Now()
fmt.Println("options refreshed")
return nil
}
func (s *DBOptionSource) StartAutoRefresh(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
go func() {
for {
select {
case <-ticker.C:
s.mu.Lock()
needsRefresh := time.Since(s.lastUpdate) > s.expiry
s.mu.Unlock()
if needsRefresh {
if err := s.Refresh(); err != nil {
fmt.Printf("failed to auto-refresh options: %v\n", err)
}
}
case <-ctx.Done():
ticker.Stop()
return
case <-s.stopChan:
ticker.Stop()
return
}
}
}()
}
func (s *DBOptionSource) StopAutoRefresh() {
close(s.stopChan)
}
func (s *DBOptionSource) HasKey(key string) bool {
s.mu.RLock()
defer s.mu.RUnlock()
_, exists := s.options[key]
return exists
}
func (s *DBOptionSource) GetValidOptions(key string) ([]string, error) {
s.mu.RLock()
needsRefresh := time.Since(s.lastUpdate) > s.expiry
s.mu.RUnlock()
if needsRefresh {
if err := s.Refresh(); err != nil {
return nil, fmt.Errorf("failed to refresh options: %w", err)
}
}
s.mu.RLock()
defer s.mu.RUnlock()
options, exists := s.options[key]
if !exists {
return nil, nil // Return nil instead of error for missing keys
}
copied := make([]string, len(options))
copy(copied, options)
return copied, nil
}
func (s *DBOptionSource) GetValidKeys() []string {
s.mu.RLock()
defer s.mu.RUnlock()
keys := make([]string, 0, len(s.options))
for k := range s.options {
keys = append(keys, k)
}
return keys
}
// --------------------
// Validator with Safe Rule Handling
// --------------------
type Validator struct {
source ValidatorOptionSource
validate *v10.Validate
}
func NewValidator(source ValidatorOptionSource) *Validator {
validate := v10.New()
return &Validator{
source: source,
validate: validate,
}
}
func (v *Validator) RegisterAllCustomRules() error {
// First register static validation rules
staticRules := map[string]func(v10.FieldLevel) bool{
"password": v.validatePassword,
"phone-number": v.validatePhoneNumber,
}
for name, fn := range staticRules {
if err := v.validate.RegisterValidation(name, fn); err != nil {
return fmt.Errorf("failed to register %s validation: %w", name, err)
}
}
// Then register dynamic option rules
for _, key := range v.source.GetValidKeys() {
if !v.source.HasKey(key) {
continue // Skip if key doesn't exist
}
if err := v.validate.RegisterValidation(key, v.createOptionRule(key)); err != nil {
return fmt.Errorf("failed to register validation for %s: %w", key, err)
}
}
return nil
}
func (v *Validator) Validate(input interface{}) error {
if input == nil {
return nil
}
val := reflect.ValueOf(input)
if val.Kind() == reflect.Ptr {
if val.IsNil() {
return nil
}
val = val.Elem() // Dereference the pointer
}
err := v.validate.Struct(input)
if err == nil {
return nil
}
// Filter out errors for nil/empty fields
if ve, ok := err.(v10.ValidationErrors); ok {
var filteredErrors v10.ValidationErrors
for _, fe := range ve {
fieldValue := val.FieldByName(fe.StructField())
if !fieldValue.IsValid() {
continue
}
if !isEmpty(fieldValue) {
filteredErrors = append(filteredErrors, fe)
} else {
fmt.Printf("Ignoring validation error for empty field: %s\n", fe.Field())
}
}
if len(filteredErrors) > 0 {
return filteredErrors
}
return nil
}
return err
}
// isEmpty checks if a value is nil or empty
func isEmpty(v reflect.Value) bool {
switch v.Kind() {
case reflect.String:
return v.Len() == 0
case reflect.Ptr, reflect.Interface:
return v.IsNil()
case reflect.Slice, reflect.Map, reflect.Array:
return v.Len() == 0
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Struct:
if t, ok := v.Interface().(time.Time); ok {
return t.IsZero()
}
// Consider non-time structs as non-empty
return false
default:
return false
}
}
// createOptionRule remains the same as previous version
func (v *Validator) createOptionRule(key string) func(v10.FieldLevel) bool {
return func(fl v10.FieldLevel) bool {
field := fl.Field()
if isEmpty(field) {
return true
}
value := field.String()
validOptions, err := v.source.GetValidOptions(key)
if err != nil || validOptions == nil {
return true
}
for _, opt := range validOptions {
if opt == value {
return true
}
}
return false
}
}
func (v *Validator) validatePassword(fl v10.FieldLevel) bool {
password := fl.Field().String()
return len(password) >= 8 &&
strings.ContainsAny(password, "abcdefghijklmnopqrstuvwxyz") &&
strings.ContainsAny(password, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") &&
strings.ContainsAny(password, "0123456789")
}
func (v *Validator) validatePhoneNumber(fl v10.FieldLevel) bool {
phone := NormalizePhoneNumber(fl.Field().String())
return strings.HasPrefix(phone, "+62")
}
func NormalizePhoneNumber(input string) string {
re := regexp.MustCompile(`[^0-9\+]`)
input = re.ReplaceAllString(input, "")
switch {
case strings.HasPrefix(input, "0"):
return "+62" + input[1:]
case strings.HasPrefix(input, "62") && !strings.HasPrefix(input, "+62"):
return "+" + input
case strings.HasPrefix(input, "8"):
return "+62" + input
default:
return input
}
}