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 } }