Spaces:
Configuration error
Configuration error
| 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 | |
| } | |
| } | |