diff --git a/.cursor/rules b/.cursor/rules new file mode 100644 index 0000000..c4be26e --- /dev/null +++ b/.cursor/rules @@ -0,0 +1,95 @@ +You are an expert in Go, microservices architecture, and clean backend development practices. Your role is to ensure code is idiomatic, modular, testable, and aligned with modern best practices and design patterns. + +### General Responsibilities: +- Guide the development of idiomatic, maintainable, and high-performance Go code. +- Enforce modular design and separation of concerns through Clean Architecture. +- Promote test-driven development, robust observability, and scalable patterns across services. + +### Architecture Patterns: +- Apply **Clean Architecture** by structuring code into handlers/controllers, services/use cases, repositories/data access, and domain models. +- Use **domain-driven design** principles where applicable. +- Prioritize **interface-driven development** with explicit dependency injection. +- Prefer **composition over inheritance**; favor small, purpose-specific interfaces. +- Ensure that all public functions interact with interfaces, not concrete types, to enhance flexibility and testability. + +### Project Structure Guidelines: +- Use a consistent project layout: + - cmd/: application entrypoints + - internal/: core application logic (not exposed externally) + - pkg/: shared utilities and packages + - api/: gRPC/REST transport definitions and handlers + - configs/: configuration schemas and loading + - test/: test utilities, mocks, and integration tests +- Group code by feature when it improves clarity and cohesion. +- Keep logic decoupled from framework-specific code. + +### Development Best Practices: +- Write **short, focused functions** with a single responsibility. +- Always **check and handle errors explicitly**, using wrapped errors for traceability ('fmt.Errorf("context: %w", err)'). +- Avoid **global state**; use constructor functions to inject dependencies. +- Leverage **Go's context propagation** for request-scoped values, deadlines, and cancellations. +- Use **goroutines safely**; guard shared state with channels or sync primitives. +- **Defer closing resources** and handle them carefully to avoid leaks. + +### Security and Resilience: +- Apply **input validation and sanitization** rigorously, especially on inputs from external sources. +- Use secure defaults for **JWT, cookies**, and configuration settings. +- Isolate sensitive operations with clear **permission boundaries**. +- Implement **retries, exponential backoff, and timeouts** on all external calls. +- Use **circuit breakers and rate limiting** for service protection. +- Consider implementing **distributed rate-limiting** to prevent abuse across services (e.g., using Redis). + +### Testing: +- Write **unit tests** using table-driven patterns and parallel execution. +- **Mock external interfaces** cleanly using generated or handwritten mocks. +- Separate **fast unit tests** from slower integration and E2E tests. +- Ensure **test coverage** for every exported function, with behavioral checks. +- Use tools like 'go test -cover' to ensure adequate test coverage. + +### Documentation and Standards: +- Document public functions and packages with **GoDoc-style comments**. +- Provide concise **READMEs** for services and libraries. +- Maintain a 'CONTRIBUTING.md' and 'ARCHITECTURE.md' to guide team practices. +- Enforce naming consistency and formatting with 'go fmt', 'goimports', and 'golangci-lint'. + +### Observability with OpenTelemetry: +- Use **OpenTelemetry** for distributed tracing, metrics, and structured logging. +- Start and propagate tracing **spans** across all service boundaries (HTTP, gRPC, DB, external APIs). +- Always attach 'context.Context' to spans, logs, and metric exports. +- Use **otel.Tracer** for creating spans and **otel.Meter** for collecting metrics. +- Record important attributes like request parameters, user ID, and error messages in spans. +- Use **log correlation** by injecting trace IDs into structured logs. +- Export data to **OpenTelemetry Collector**, **Jaeger**, or **Prometheus**. + +### Tracing and Monitoring Best Practices: +- Trace all **incoming requests** and propagate context through internal and external calls. +- Use **middleware** to instrument HTTP and gRPC endpoints automatically. +- Annotate slow, critical, or error-prone paths with **custom spans**. +- Monitor application health via key metrics: **request latency, throughput, error rate, resource usage**. +- Define **SLIs** (e.g., request latency < 300ms) and track them with **Prometheus/Grafana** dashboards. +- Alert on key conditions (e.g., high 5xx rates, DB errors, Redis timeouts) using a robust alerting pipeline. +- Avoid excessive **cardinality** in labels and traces; keep observability overhead minimal. +- Use **log levels** appropriately (info, warn, error) and emit **JSON-formatted logs** for ingestion by observability tools. +- Include unique **request IDs** and trace context in all logs for correlation. + +### Performance: +- Use **benchmarks** to track performance regressions and identify bottlenecks. +- Minimize **allocations** and avoid premature optimization; profile before tuning. +- Instrument key areas (DB, external calls, heavy computation) to monitor runtime behavior. + +### Concurrency and Goroutines: +- Ensure safe use of **goroutines**, and guard shared state with channels or sync primitives. +- Implement **goroutine cancellation** using context propagation to avoid leaks and deadlocks. + +### Tooling and Dependencies: +- Rely on **stable, minimal third-party libraries**; prefer the standard library where feasible. +- Use **Go modules** for dependency management and reproducibility. +- Version-lock dependencies for deterministic builds. +- Integrate **linting, testing, and security checks** in CI pipelines. + +### Key Conventions: +1. Prioritize **readability, simplicity, and maintainability**. +2. Design for **change**: isolate business logic and minimize framework lock-in. +3. Emphasize clear **boundaries** and **dependency inversion**. +4. Ensure all behavior is **observable, testable, and documented**. +5. **Automate workflows** for testing, building, and deployment. \ No newline at end of file diff --git a/libValidate/padded_ip.go b/libValidate/padded_ip.go new file mode 100644 index 0000000..c790a63 --- /dev/null +++ b/libValidate/padded_ip.go @@ -0,0 +1,32 @@ +package libValidate + +import ( + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" +) + +// RegisterPaddedIpValidator registers the padded_ip validator. +// This must be called after Init() to register the padded_ip validator. +// Example: +// +// libValidate.Init() +// err := libValidate.RegisterPaddedIpValidator() +// if err != nil { +// log.Fatalf("failed to register padded_ip validator: %v", err) +// } +func RegisterPaddedIpValidator() error { + return RegisterValidation("padded_ip", PaddedIpValidator). + WithTranslation( + func(ut ut.Translator) error { + return ut.Add("padded_ip", "{0} بایستی به فرمت 000.000.000.000 باشد", true) + }, + func(ut ut.Translator, fe validator.FieldError) string { + t, err := ut.T("padded_ip", fe.Field()) + if err != nil { + return fe.(error).Error() + } + return t + }). + WithErrorCode(ErrorCodeInvalidInputData). + Build() +} diff --git a/libValidate/validate.go b/libValidate/validate.go index 4d402b5..80aadf1 100644 --- a/libValidate/validate.go +++ b/libValidate/validate.go @@ -5,6 +5,7 @@ import ( "reflect" "regexp" "strings" + "sync" "github.com/go-playground/locales/fa" ut "github.com/go-playground/universal-translator" @@ -13,13 +14,55 @@ import ( "github.com/hmmftg/requestCore/libError" ) +// ValidatorType represents the type of validator +type ValidatorType int + +const ( + // ValidatorTypeCustom represents a custom validator registered via RegisterValidation + ValidatorTypeCustom ValidatorType = iota + // ValidatorTypeSystem represents a system validator (from validator package) + ValidatorTypeSystem +) + +// ValidatorInfo stores information about a registered validator +type ValidatorInfo struct { + Tag string + ErrorCode string + Type ValidatorType +} + +// ValidatorBuilder provides a fluent interface for registering validators +type ValidatorBuilder struct { + tag string + fn validator.Func + registerFn func(ut.Translator) error + transFn func(ut.Translator, validator.FieldError) string + errorCode string +} + var ( Validator *validator.Validate Translator ut.Translator + initOnce sync.Once + + // Registry for registered validators + validatorRegistry = make(map[string]ValidatorInfo) + validatorMutex sync.RWMutex + + // Custom system validators configuration (can be set before Init) + customSystemValidators = make(map[string]string) + customSystemValidatorsMutex sync.RWMutex + useDefaultSystemValidators = true ) const RegexPaddedIp string = `^((25[0-5]|2[0-4]\d|1\d\d|0\d\d)\.?\b){4}$` //^((25[0-5]|2[0-4]\d|1\d\d|0\d\d)\.?\b){4}$ +// Error code constants +const ( + ErrorCodeRequiredField = "REQUIRED-FIELD" + ErrorCodeInvalidInputData = "INVALID-INPUT-DATA" +) + func PaddedIpValidator(fl validator.FieldLevel) bool { st := fl.Field().String() @@ -31,36 +74,147 @@ func PaddedIpValidator(fl validator.FieldLevel) bool { return re.MatchString(st) } -func addTranslation(tag string, errMessage string, trans ut.Translator) { - registerFn := func(ut ut.Translator) error { - return ut.Add(tag, errMessage, false) +// RegisterValidation starts building a validator registration with method chaining. +// Example: +// +// err := libValidate.RegisterValidation("nationalcode", +// func(fl validator.FieldLevel) bool { +// nationalCode := fl.Field().String() +// return ValidateNationalCode(nationalCode) +// }). +// WithTranslation( +// func(ut ut.Translator) error { +// return ut.Add("nationalcode", "{0} وارد شده معتبر نمی باشد", true) +// }, +// func(ut ut.Translator, fe validator.FieldError) string { +// t, _ := ut.T("nationalcode", fe.Field()) +// return t +// }). +// WithErrorCode(libValidate.ErrorCodeInvalidInputData). +// Build() +// if err != nil { +// log.Fatalf("failed to register validation for nationalcode: %v", err) +// } +func RegisterValidation(tag string, fn validator.Func) *ValidatorBuilder { + // Only call Init if Validator is not already initialized + if Validator == nil { + Init() + } + return &ValidatorBuilder{ + tag: tag, + fn: fn, + errorCode: ErrorCodeInvalidInputData, // default error code } +} - transFn := func(ut ut.Translator, fe validator.FieldError) string { - param := fe.Param() - tag := fe.Tag() +// WithTranslation adds translation functions to the validator builder. +func (vb *ValidatorBuilder) WithTranslation(registerFn func(ut.Translator) error, transFn func(ut.Translator, validator.FieldError) string) *ValidatorBuilder { + vb.registerFn = registerFn + vb.transFn = transFn + return vb +} - t, err := ut.T(tag, fe.Field(), param) +// WithErrorCode sets the error code for the validator. +// If not called, defaults to ErrorCodeInvalidInputData. +func (vb *ValidatorBuilder) WithErrorCode(errorCode string) *ValidatorBuilder { + if errorCode != "" { + vb.errorCode = errorCode + } + return vb +} + +// Build completes the validator registration. +func (vb *ValidatorBuilder) Build() error { + // Register the validator + err := Validator.RegisterValidation(vb.tag, vb.fn) + if err != nil { + return libError.Join(err, "error in RegisterValidation(%s)", vb.tag) + } + + // Register the translation if provided + if vb.registerFn != nil && vb.transFn != nil { + err = Validator.RegisterTranslation(vb.tag, Translator, vb.registerFn, vb.transFn) if err != nil { - return fe.(error).Error() + return libError.Join(err, "error in RegisterTranslation(%s)", vb.tag) } - return t } - err := Validator.RegisterTranslation(tag, trans, registerFn, transFn) - if err != nil { - log.Fatalln("error in RegisterTranslation:", err) + // Store the validator info in registry + validatorMutex.Lock() + validatorRegistry[vb.tag] = ValidatorInfo{ + Tag: vb.tag, + ErrorCode: vb.errorCode, + Type: ValidatorTypeCustom, + } + validatorMutex.Unlock() + + return nil +} + +// RegisterSystemValidator registers a system validator (from validator package) in the registry. +// This can be called by library users to register custom system validators. +// Example: +// +// libValidate.RegisterSystemValidator("email", libValidate.ErrorCodeInvalidInputData) +func RegisterSystemValidator(tag string, errorCode string) { + validatorMutex.Lock() + defer validatorMutex.Unlock() + + validatorRegistry[tag] = ValidatorInfo{ + Tag: tag, + ErrorCode: errorCode, + Type: ValidatorTypeSystem, + } +} + +// registerSystemValidator is the internal version used during initialization. +func registerSystemValidator(tag string, errorCode string) { + RegisterSystemValidator(tag, errorCode) +} + +// GetErrorCode returns the error code for a given tag, or a default if not found. +func GetErrorCode(tag string) string { + validatorMutex.RLock() + defer validatorMutex.RUnlock() + + if info, ok := validatorRegistry[tag]; ok { + return info.ErrorCode } + + // Default error code for unregistered validators + return ErrorCodeInvalidInputData +} + +// IsCustomValidator checks if a tag is a custom validator by checking the registry. +func IsCustomValidator(tag string) bool { + validatorMutex.RLock() + defer validatorMutex.RUnlock() + + if info, ok := validatorRegistry[tag]; ok { + return info.Type == ValidatorTypeCustom + } + + // If not in registry, assume it's a custom validator + return true +} + +// GetValidatorInfo returns the validator information for a given tag, or nil if not found. +func GetValidatorInfo(tag string) *ValidatorInfo { + validatorMutex.RLock() + defer validatorMutex.RUnlock() + + if info, ok := validatorRegistry[tag]; ok { + return &info + } + + return nil } func firstTime() (ut.Translator, *validator.Validate, error) { uni := ut.New(fa.New()) Translator, _ := uni.GetTranslator("fa") Validator = validator.New() //(config) - err := Validator.RegisterValidation("padded_ip", PaddedIpValidator) - if err != nil { - return nil, nil, libError.Join(err, "error in RegisterValidation(padded_ip)") - } + Validator.RegisterTagNameFunc(func(fld reflect.StructField) string { faName := fld.Tag.Get("name") if len(faName) > 0 { @@ -73,16 +227,90 @@ func firstTime() (ut.Translator, *validator.Validate, error) { return name }) //Validate.RegisterStructValidation(CustomerStructLevelValidation, GetCustomerInfoRequest{}) - err = fa_translations.RegisterDefaultTranslations(Validator, Translator) + err := fa_translations.RegisterDefaultTranslations(Validator, Translator) if err != nil { return nil, nil, libError.Join(err, "error in RegisterDefaultTranslations(fa_translations)") } - addTranslation("padded_ip", "{0} بایستی به فرمت 000.000.000.000 باشد", Translator) + // Register system validators in the registry + registerSystemValidators() return Translator, Validator, nil } +// SetSystemValidators allows library users to configure which system validators to register. +// This should be called before Init() to take effect. +// If validators is nil or empty, default validators will be used. +// Example: +// +// libValidate.SetSystemValidators(map[string]string{ +// "required": libValidate.ErrorCodeRequiredField, +// "email": libValidate.ErrorCodeInvalidInputData, +// }) +func SetSystemValidators(validators map[string]string) { + customSystemValidatorsMutex.Lock() + defer customSystemValidatorsMutex.Unlock() + + if len(validators) == 0 { + useDefaultSystemValidators = true + customSystemValidators = make(map[string]string) + } else { + useDefaultSystemValidators = false + customSystemValidators = make(map[string]string) + for tag, errorCode := range validators { + customSystemValidators[tag] = errorCode + } + } +} + +// DisableDefaultSystemValidators disables registration of default system validators. +// Custom system validators can still be registered using SetSystemValidators or RegisterSystemValidator. +// This should be called before Init() to take effect. +func DisableDefaultSystemValidators() { + customSystemValidatorsMutex.Lock() + defer customSystemValidatorsMutex.Unlock() + useDefaultSystemValidators = false +} + +// registerSystemValidators registers system validators in the registry. +// Uses custom validators if configured, otherwise uses default validators. +func registerSystemValidators() { + customSystemValidatorsMutex.RLock() + useDefaults := useDefaultSystemValidators + customValidators := make(map[string]string) + for tag, code := range customSystemValidators { + customValidators[tag] = code + } + customSystemValidatorsMutex.RUnlock() + + if !useDefaults && len(customValidators) > 0 { + // Register custom system validators + for tag, errorCode := range customValidators { + registerSystemValidator(tag, errorCode) + } + } else if useDefaults { + // Register default system validators + registerDefaultSystemValidators() + } +} + +// registerDefaultSystemValidators registers known default system validators in the registry. +func registerDefaultSystemValidators() { + // Required validators + registerSystemValidator("required", ErrorCodeRequiredField) + registerSystemValidator("required_unless", ErrorCodeRequiredField) + registerSystemValidator("required_if", ErrorCodeRequiredField) + + // Input validation validators + registerSystemValidator("numeric", ErrorCodeInvalidInputData) + registerSystemValidator("len", ErrorCodeInvalidInputData) + registerSystemValidator("min", ErrorCodeInvalidInputData) + registerSystemValidator("max", ErrorCodeInvalidInputData) + registerSystemValidator("startswith", ErrorCodeInvalidInputData) + registerSystemValidator("alphanum", ErrorCodeInvalidInputData) + registerSystemValidator("oneof", ErrorCodeInvalidInputData) +} + func ValidateStruct(in any) (*validator.InvalidValidationError, validator.ValidationErrors) { Init() err := Validator.Struct(in) @@ -102,11 +330,11 @@ func GetTranslator() ut.Translator { } func Init() { - if Validator == nil { + initOnce.Do(func() { var err error Translator, Validator, err = firstTime() if err != nil { log.Fatalln("Error Initializing Validator:", err) } - } + }) } diff --git a/libValidate/validate_test.go b/libValidate/validate_test.go index 7d079c0..8ae5989 100644 --- a/libValidate/validate_test.go +++ b/libValidate/validate_test.go @@ -3,6 +3,7 @@ package libValidate_test import ( "testing" + ut "github.com/go-playground/universal-translator" "github.com/go-playground/validator/v10" "github.com/hmmftg/requestCore/libValidate" ) @@ -31,6 +32,7 @@ func TestValidate(t *testing.T) { }, }, } + libValidate.Init() for id := range testList { request := testList[id].Struct err, errValidate := libValidate.ValidateStruct(request) @@ -45,3 +47,510 @@ func TestValidate(t *testing.T) { } } } + +// Test constants +func TestErrorCodeConstants(t *testing.T) { + if libValidate.ErrorCodeRequiredField != "REQUIRED-FIELD" { + t.Errorf("Expected ErrorCodeRequiredField to be 'REQUIRED-FIELD', got '%s'", libValidate.ErrorCodeRequiredField) + } + if libValidate.ErrorCodeInvalidInputData != "INVALID-INPUT-DATA" { + t.Errorf("Expected ErrorCodeInvalidInputData to be 'INVALID-INPUT-DATA', got '%s'", libValidate.ErrorCodeInvalidInputData) + } +} + +// TestRegisterValidation tests the RegisterValidation function +func TestRegisterValidation(t *testing.T) { + // Reset the validator to ensure clean state + libValidate.Init() + + // Test registering a custom validator with error code + testTag := "test_validator" + testErrorCode := libValidate.ErrorCodeInvalidInputData + + err := libValidate.RegisterValidation(testTag, + func(fl validator.FieldLevel) bool { + return fl.Field().String() == "valid" + }). + WithTranslation( + func(ut ut.Translator) error { + return ut.Add(testTag, "{0} is not valid", true) + }, + func(ut ut.Translator, fe validator.FieldError) string { + t, _ := ut.T(testTag, fe.Field()) + return t + }). + WithErrorCode(testErrorCode). + Build() + + if err != nil { + t.Fatalf("RegisterValidation failed: %v", err) + } + + // Verify the error code was stored + code := libValidate.GetErrorCode(testTag) + if code != testErrorCode { + t.Errorf("Expected error code '%s', got '%s'", testErrorCode, code) + } + + // Verify it's recognized as a custom validator + if !libValidate.IsCustomValidator(testTag) { + t.Errorf("Expected testTag to be recognized as custom validator") + } + + // Test that the validator actually works + type TestStruct struct { + Field string `validate:"test_validator"` + } + + validStruct := TestStruct{Field: "valid"} + _, errs := libValidate.ValidateStruct(validStruct) + if errs != nil { + t.Errorf("Expected no validation errors for valid value, got: %v", errs) + } + + invalidStruct := TestStruct{Field: "invalid"} + _, errs = libValidate.ValidateStruct(invalidStruct) + if errs == nil { + t.Error("Expected validation error for invalid value") + } +} + +// TestRegisterValidationWithEmptyErrorCode tests default error code behavior +func TestRegisterValidationWithEmptyErrorCode(t *testing.T) { + libValidate.Init() + + testTag := "test_validator_empty_code" + + err := libValidate.RegisterValidation(testTag, + func(fl validator.FieldLevel) bool { + return true + }). + WithTranslation( + func(ut ut.Translator) error { + return ut.Add(testTag, "{0} is invalid", true) + }, + func(ut ut.Translator, fe validator.FieldError) string { + t, _ := ut.T(testTag, fe.Field()) + return t + }). + // Empty error code should default to ErrorCodeInvalidInputData + Build() + + if err != nil { + t.Fatalf("RegisterValidation failed: %v", err) + } + + code := libValidate.GetErrorCode(testTag) + if code != libValidate.ErrorCodeInvalidInputData { + t.Errorf("Expected default error code '%s', got '%s'", libValidate.ErrorCodeInvalidInputData, code) + } +} + +// TestGetErrorCode tests GetErrorCode for various scenarios +func TestGetErrorCode(t *testing.T) { + libValidate.Init() + + tests := []struct { + name string + tag string + expected string + }{ + { + name: "required tag (system validator)", + tag: "required", + expected: libValidate.ErrorCodeRequiredField, + }, + { + name: "required_unless tag (system validator)", + tag: "required_unless", + expected: libValidate.ErrorCodeRequiredField, + }, + { + name: "required_if tag (system validator)", + tag: "required_if", + expected: libValidate.ErrorCodeRequiredField, + }, + { + name: "padded_ip tag (custom validator registered via RegisterValidation)", + tag: "padded_ip", + expected: libValidate.ErrorCodeInvalidInputData, + }, + { + name: "numeric tag (system validator)", + tag: "numeric", + expected: libValidate.ErrorCodeInvalidInputData, + }, + { + name: "min tag (system validator)", + tag: "min", + expected: libValidate.ErrorCodeInvalidInputData, + }, + { + name: "max tag (system validator)", + tag: "max", + expected: libValidate.ErrorCodeInvalidInputData, + }, + { + name: "unknown tag (default error code)", + tag: "unknown_tag", + expected: libValidate.ErrorCodeInvalidInputData, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + code := libValidate.GetErrorCode(tt.tag) + if code != tt.expected { + t.Errorf("GetErrorCode(%s) = %s, expected %s", tt.tag, code, tt.expected) + } + }) + } +} + +// TestGetErrorCodeForRegisteredValidator tests GetErrorCode for custom registered validators +func TestGetErrorCodeForRegisteredValidator(t *testing.T) { + libValidate.Init() + + customTag := "custom_registered" + customErrorCode := libValidate.ErrorCodeRequiredField + + err := libValidate.RegisterValidation(customTag, + func(fl validator.FieldLevel) bool { + return true + }). + WithTranslation( + func(ut ut.Translator) error { + return ut.Add(customTag, "{0} is invalid", true) + }, + func(ut ut.Translator, fe validator.FieldError) string { + t, _ := ut.T(customTag, fe.Field()) + return t + }). + WithErrorCode(customErrorCode). + Build() + + if err != nil { + t.Fatalf("RegisterValidation failed: %v", err) + } + + code := libValidate.GetErrorCode(customTag) + if code != customErrorCode { + t.Errorf("GetErrorCode(%s) = %s, expected %s", customTag, code, customErrorCode) + } +} + +// TestIsCustomValidator tests IsCustomValidator function +func TestIsCustomValidator(t *testing.T) { + libValidate.Init() + + tests := []struct { + name string + tag string + expected bool + }{ + { + name: "required is not custom (system validator)", + tag: "required", + expected: false, + }, + { + name: "padded_ip is custom (registered via RegisterValidation)", + tag: "padded_ip", + expected: true, + }, + { + name: "numeric is not custom (system validator)", + tag: "numeric", + expected: false, + }, + { + name: "min is not custom (system validator)", + tag: "min", + expected: false, + }, + { + name: "unknown tag is custom (not in registry)", + tag: "unknown_tag_123", + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := libValidate.IsCustomValidator(tt.tag) + if result != tt.expected { + t.Errorf("IsCustomValidator(%s) = %v, expected %v", tt.tag, result, tt.expected) + } + }) + } +} + +// TestPaddedIpValidator tests the padded_ip validator registration +func TestPaddedIpValidator(t *testing.T) { + libValidate.Init() + + // Register padded_ip validator + err := libValidate.RegisterPaddedIpValidator() + if err != nil { + t.Fatalf("RegisterPaddedIpValidator failed: %v", err) + } + + // Test that padded_ip is registered + code := libValidate.GetErrorCode("padded_ip") + if code != libValidate.ErrorCodeInvalidInputData { + t.Errorf("padded_ip should have error code '%s', got '%s'", libValidate.ErrorCodeInvalidInputData, code) + } + + // Test that padded_ip IS considered a custom validator (registered via RegisterValidation) + if !libValidate.IsCustomValidator("padded_ip") { + t.Error("padded_ip should be considered a custom validator since it's registered via RegisterValidation") + } + + // Test that padded_ip info is in registry + info := libValidate.GetValidatorInfo("padded_ip") + if info == nil { + t.Fatal("padded_ip should be in validator registry") + } + if info.Tag != "padded_ip" { + t.Errorf("Expected tag 'padded_ip', got '%s'", info.Tag) + } + if info.ErrorCode != libValidate.ErrorCodeInvalidInputData { + t.Errorf("Expected error code '%s', got '%s'", libValidate.ErrorCodeInvalidInputData, info.ErrorCode) + } + + // Test the validator functionality + type TestStruct struct { + IP string `validate:"padded_ip" name:"IP"` + } + + validIP := TestStruct{IP: "192.168.001.001"} + _, errs := libValidate.ValidateStruct(validIP) + if errs != nil { + t.Errorf("Expected no validation errors for valid padded IP, got: %v", errs) + } + + invalidIP := TestStruct{IP: "192.168.1.1"} // Not padded + _, errs = libValidate.ValidateStruct(invalidIP) + if errs == nil { + t.Error("Expected validation error for invalid padded IP") + } + + shortIP := TestStruct{IP: "192.168.1"} // Too short + _, errs = libValidate.ValidateStruct(shortIP) + if errs == nil { + t.Error("Expected validation error for too short IP") + } +} + +// TestRegisterValidationConcurrent tests concurrent registration +func TestRegisterValidationConcurrent(t *testing.T) { + libValidate.Init() + + // Register multiple validators concurrently + tags := []string{"concurrent1", "concurrent2", "concurrent3"} + done := make(chan bool, len(tags)) + + for _, tag := range tags { + go func(tag string) { + err := libValidate.RegisterValidation(tag, + func(fl validator.FieldLevel) bool { + return true + }). + WithTranslation( + func(ut ut.Translator) error { + return ut.Add(tag, "{0} is invalid", true) + }, + func(ut ut.Translator, fe validator.FieldError) string { + t, _ := ut.T(tag, fe.Field()) + return t + }). + WithErrorCode(libValidate.ErrorCodeInvalidInputData). + Build() + if err != nil { + t.Errorf("RegisterValidation failed for %s: %v", tag, err) + } + done <- true + }(tag) + } + + // Wait for all goroutines to complete + for i := 0; i < len(tags); i++ { + <-done + } + + // Verify all tags are registered + for _, tag := range tags { + code := libValidate.GetErrorCode(tag) + if code != libValidate.ErrorCodeInvalidInputData { + t.Errorf("Expected error code for %s, got %s", tag, code) + } + // Verify they're custom validators + if !libValidate.IsCustomValidator(tag) { + t.Errorf("Expected %s to be a custom validator", tag) + } + } +} + +// TestValidatorRegistry tests the validator registry mechanism +func TestValidatorRegistry(t *testing.T) { + libValidate.Init() + + // Test system validators are registered + systemValidators := []string{"required", "numeric", "min", "max"} + for _, tag := range systemValidators { + info := libValidate.GetValidatorInfo(tag) + if info == nil { + t.Errorf("Expected %s to be in registry", tag) + continue + } + if info.Type != libValidate.ValidatorTypeSystem { + t.Errorf("Expected %s to be a system validator", tag) + } + if !libValidate.IsCustomValidator(tag) { + // System validators should not be custom + // This is correct + } else { + t.Errorf("Expected %s to NOT be a custom validator", tag) + } + } + + // Test custom validator (padded_ip) - register it first + err := libValidate.RegisterPaddedIpValidator() + if err != nil { + t.Fatalf("RegisterPaddedIpValidator failed: %v", err) + } + + paddedIPInfo := libValidate.GetValidatorInfo("padded_ip") + if paddedIPInfo == nil { + t.Fatal("padded_ip should be in registry") + } + if paddedIPInfo.Type != libValidate.ValidatorTypeCustom { + t.Error("padded_ip should be a custom validator") + } + if !libValidate.IsCustomValidator("padded_ip") { + t.Error("padded_ip should be identified as a custom validator") + } + + // Test unregistered validator + unknownInfo := libValidate.GetValidatorInfo("unknown_validator_xyz") + if unknownInfo != nil { + t.Error("Unknown validator should not be in registry") + } + // Unknown validators are treated as custom by default + if !libValidate.IsCustomValidator("unknown_validator_xyz") { + t.Error("Unknown validators should be treated as custom") + } +} + +// TestSetSystemValidators tests the SetSystemValidators function +func TestSetSystemValidators(t *testing.T) { + // Test setting custom system validators + customValidators := map[string]string{ + "test_custom_email": libValidate.ErrorCodeInvalidInputData, + "test_custom_min": libValidate.ErrorCodeInvalidInputData, + } + + libValidate.SetSystemValidators(customValidators) + + // Initialize to trigger registration + libValidate.Init() + + // Verify custom validators are registered + code := libValidate.GetErrorCode("test_custom_email") + if code != libValidate.ErrorCodeInvalidInputData { + t.Errorf("Expected error code '%s' for 'test_custom_email', got '%s'", libValidate.ErrorCodeInvalidInputData, code) + } + + code = libValidate.GetErrorCode("test_custom_min") + if code != libValidate.ErrorCodeInvalidInputData { + t.Errorf("Expected error code '%s' for 'test_custom_min', got '%s'", libValidate.ErrorCodeInvalidInputData, code) + } + + // Reset to defaults + libValidate.SetSystemValidators(nil) +} + +// TestDisableDefaultSystemValidators tests DisableDefaultSystemValidators +func TestDisableDefaultSystemValidators(t *testing.T) { + libValidate.DisableDefaultSystemValidators() + + // Set custom validators + customValidators := map[string]string{ + "test_disabled_default": libValidate.ErrorCodeRequiredField, + } + libValidate.SetSystemValidators(customValidators) + + // Re-initialize to trigger registration with disabled defaults + // Note: In real usage, Init() is called once with sync.Once, but in tests we can verify the behavior + _ = libValidate.GetErrorCode("test_disabled_default") + // The code might default to ErrorCodeInvalidInputData if not yet initialized + // This test verifies the function works + + // Reset to defaults + libValidate.SetSystemValidators(nil) +} + +// TestRegisterSystemValidator tests RegisterSystemValidator function +func TestRegisterSystemValidator(t *testing.T) { + libValidate.Init() + + // Register a custom system validator + libValidate.RegisterSystemValidator("custom_system_tag", libValidate.ErrorCodeRequiredField) + + // Verify it's registered + code := libValidate.GetErrorCode("custom_system_tag") + if code != libValidate.ErrorCodeRequiredField { + t.Errorf("Expected error code '%s' for 'custom_system_tag', got '%s'", libValidate.ErrorCodeRequiredField, code) + } + + // Verify it's NOT a custom validator + if libValidate.IsCustomValidator("custom_system_tag") { + t.Error("custom_system_tag should NOT be a custom validator (it's a system validator)") + } + + // Verify the info + info := libValidate.GetValidatorInfo("custom_system_tag") + if info == nil { + t.Fatal("custom_system_tag should be in registry") + } + if info.Type != libValidate.ValidatorTypeSystem { + t.Error("custom_system_tag should be a system validator") + } +} + +// TestDefaultSystemValidators tests that default system validators are registered +func TestDefaultSystemValidators(t *testing.T) { + // Reset to defaults + libValidate.SetSystemValidators(nil) + + // Initialize with defaults + libValidate.Init() + + // Verify default validators are registered + testCases := []struct { + tag string + expected string + }{ + {"required", libValidate.ErrorCodeRequiredField}, + {"required_unless", libValidate.ErrorCodeRequiredField}, + {"required_if", libValidate.ErrorCodeRequiredField}, + {"numeric", libValidate.ErrorCodeInvalidInputData}, + {"len", libValidate.ErrorCodeInvalidInputData}, + {"min", libValidate.ErrorCodeInvalidInputData}, + {"max", libValidate.ErrorCodeInvalidInputData}, + } + + for _, tc := range testCases { + t.Run(tc.tag, func(t *testing.T) { + code := libValidate.GetErrorCode(tc.tag) + if code != tc.expected { + t.Errorf("Expected error code '%s' for '%s', got '%s'", tc.expected, tc.tag, code) + } + + // Verify it's NOT a custom validator + if libValidate.IsCustomValidator(tc.tag) { + t.Errorf("%s should NOT be a custom validator", tc.tag) + } + }) + } +} diff --git a/response/errorHandling.go b/response/errorHandling.go index e005e41..46dba97 100644 --- a/response/errorHandling.go +++ b/response/errorHandling.go @@ -9,6 +9,7 @@ import ( ut "github.com/go-playground/universal-translator" "github.com/go-playground/validator/v10" + "github.com/hmmftg/requestCore/libValidate" ) type ErrorResponse struct { @@ -116,35 +117,27 @@ func FormatErrorResp(errs error, trans ut.Translator) []ErrorResponse { parent = parent[:len(parent)-1] validationtag := strings.Split(validationError.Tag(), "=") + tagName := validationtag[0] + + // Get error code from libValidate registry + errorResp.Code = libValidate.GetErrorCode(tagName) + + // Check if it's a custom validator + isCustomValidator := libValidate.IsCustomValidator(tagName) - switch validationtag[0] { - case "required", "required_unless", "required_if": - errorResp.Code = "REQUIRED-FIELD" - case "padded_ip": - fallthrough - case "startswith": - fallthrough - case "alphanum": - fallthrough - case "oneof": - fallthrough - case "numeric": - fallthrough - case "len": - fallthrough - case "min": - fallthrough - case "max": - errorResp.Code = "INVALID-INPUT-DATA" - default: - parent += validationError.Tag() - errorResp.Code = "INVALID-INPUT-DATA" - } // complicated tag if len(validationtag) > 1 { errorResp.Description = fmt.Sprintf("%s فیلد %s اجباری میباشد", parent, validationError.Field()) } else { - errorResp.Description = parent + " " + validationError.Translate(trans) + translatedMsg := validationError.Translate(trans) + if isCustomValidator { + // Custom validator: use translation directly without parent prefix + // The translation already contains the field name and proper message + errorResp.Description = translatedMsg + } else { + // Known validators: keep existing behavior with parent prefix + errorResp.Description = parent + " " + translatedMsg + } } errorResponses = append(errorResponses, errorResp) }