From a6655f9721bad15dd3028a7cb6bd20eedc6a54f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20=C3=81lvarez?= <86166683+ralvarezdev@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:58:36 -0400 Subject: [PATCH] feat: Added Protobuf and JSON struct validator * Added Protobuf and JSON struct validator through reflection based on their tags * Forked from 'github.com/pixel-plaza-dev/uru-databases-2-go-service-common' --- field/errors.go | 9 + field/mail/errors.go | 9 + field/mail/mail_address.go | 21 ++ go.mod | 5 + go.sum | 3 + structs/mapper/errors.go | 10 + structs/mapper/mapper.go | 218 +++++++++++++++++ structs/validations/errors.go | 11 + .../validations/struct_fields_validations.go | 230 ++++++++++++++++++ structs/validations/validate_nil_fields.go | 122 ++++++++++ 10 files changed, 638 insertions(+) create mode 100644 field/errors.go create mode 100644 field/mail/errors.go create mode 100644 field/mail/mail_address.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 structs/mapper/errors.go create mode 100644 structs/mapper/mapper.go create mode 100644 structs/validations/errors.go create mode 100644 structs/validations/struct_fields_validations.go create mode 100644 structs/validations/validate_nil_fields.go diff --git a/field/errors.go b/field/errors.go new file mode 100644 index 0000000..7f98ca4 --- /dev/null +++ b/field/errors.go @@ -0,0 +1,9 @@ +package field + +import ( + "errors" +) + +var ( + InvalidBirthdateError = errors.New("invalid birthdate") +) diff --git a/field/mail/errors.go b/field/mail/errors.go new file mode 100644 index 0000000..c9d6ab5 --- /dev/null +++ b/field/mail/errors.go @@ -0,0 +1,9 @@ +package mail + +import ( + "errors" +) + +var ( + InvalidMailAddressError = errors.New("invalid mail address") +) diff --git a/field/mail/mail_address.go b/field/mail/mail_address.go new file mode 100644 index 0000000..425a520 --- /dev/null +++ b/field/mail/mail_address.go @@ -0,0 +1,21 @@ +package mail + +import ( + "net/mail" +) + +// ValidMailAddress checks if the mail address is valid +func ValidMailAddress(address string) (string, error) { + // Check if the mail address is empty + if address == "" { + return "", InvalidMailAddressError + } + + // Check if the mail address is valid + addr, err := mail.ParseAddress(address) + if err != nil { + return "", InvalidMailAddressError + } + + return addr.Address, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c036c72 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/ralvarezdev/go-validator + +go 1.23.4 + +require github.com/ralvarezdev/go-flags v0.2.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..69b1e58 --- /dev/null +++ b/go.sum @@ -0,0 +1,3 @@ +github.com/ralvarezdev/go-flags v0.1.0 h1:WG6eMbS2yL+UYtI3X8R/HbisrfEgzLiMKMWqXanwyys= +github.com/ralvarezdev/go-flags v0.1.0/go.mod h1:pYw9H7NJ07Y5asZDC/EI5bpBLR0kdL2ISsh6X5ws+3s= +github.com/ralvarezdev/go-flags v0.2.0/go.mod h1:pYw9H7NJ07Y5asZDC/EI5bpBLR0kdL2ISsh6X5ws+3s= diff --git a/structs/mapper/errors.go b/structs/mapper/errors.go new file mode 100644 index 0000000..e2572b3 --- /dev/null +++ b/structs/mapper/errors.go @@ -0,0 +1,10 @@ +package mapper + +var ( + MissingProtobufTagError = "missing protobuf tag: %s" + MissingProtobufTagNameError = "missing protobuf tag name: %s" + DuplicateProtobufTagNameError = "duplicate protobuf tag name: %s" + MissingJSONTagError = "missing json tag: %s" + EmptyJSONTagError = "empty json tag: %s" + DuplicateJSONTagNameError = "duplicate json tag name: %s" +) diff --git a/structs/mapper/mapper.go b/structs/mapper/mapper.go new file mode 100644 index 0000000..99ff4c0 --- /dev/null +++ b/structs/mapper/mapper.go @@ -0,0 +1,218 @@ +package mapper + +import ( + "fmt" + goflagsmode "github.com/ralvarezdev/go-flags/mode" + "reflect" + "strings" +) + +// Protobuf fields generated by the protoc compiler +const ( + State = "state" + SizeCache = "sizeCache" + UnknownFields = "unknownFields" + ProtobufTag = "protobuf" + ProtobufOneOf = "oneof" + ProtobufNamePrefix = "name=" + JSONTag = "json" + JSONOmitempty = "omitempty" +) + +// Mapper is a map of fields to validate from a struct +type Mapper struct { + Fields map[string]string // Key is the field name and value is the name used in the validation error + NestedMappers map[string]*Mapper +} + +// CreateProtobufMapper creates the fields to validate from a Protobuf compiled struct +func CreateProtobufMapper(structInstance interface{}, mode *goflagsmode.Flag) ( + *Mapper, + error, +) { + // Reflection of data + typeReflection := reflect.TypeOf(structInstance) + + // If data is a pointer, dereference it + if typeReflection.Kind() == reflect.Ptr { + typeReflection = typeReflection.Elem() + } + + // Initialize the map fields and the map of nested mappers + rootMapper := make(map[string]string) + rootNestedMappers := make(map[string]*Mapper) + + // Reflection of the type of data + var protobufTag string + var protobufName string + for i := 0; i < typeReflection.NumField(); i++ { + // Get the field type through reflection + field := typeReflection.Field(i) + + // Check if the field is a protoc generated field + if field.Name == State || field.Name == SizeCache || field.Name == UnknownFields { + continue + } + + // Print field on debug mode + fieldType := field.Type + if mode != nil && mode.IsDebug() { + fmt.Printf("field '%v' type: %v\n", field.Name, fieldType) + } + + // Check if the field is a pointer + if fieldType.Kind() != reflect.Ptr { + // Get the Protobuf tag of the field + protobufTag = field.Tag.Get(ProtobufTag) + if protobufTag == "" { + return nil, fmt.Errorf(MissingProtobufTagError, field.Name) + } + } else { + fieldType = fieldType.Elem() + + // Check if the element type is not a struct, which would mean that it is an optional scalar type + if fieldType.Kind() != reflect.Struct { + continue + } + + // Get the Protobuf tag of the field + protobufTag = field.Tag.Get(ProtobufTag) + if protobufTag == "" { + return nil, fmt.Errorf(MissingProtobufTagError, field.Name) + } + + // Check the tag to determine if it contains 'oneof', which means it is an optional struct field + if ok := strings.Contains(protobufTag, ProtobufOneOf); ok { + continue + } + + // Create a new Mapper for the nested struct field + fieldNestedMapper, err := CreateProtobufMapper( + reflect.New(fieldType).Interface(), + mode, + ) + if err != nil { + return nil, err + } + + // Add the nested fields to the map + rootNestedMappers[field.Name] = fieldNestedMapper + } + + // Get the field name from the Protobuf tag + tagParts := strings.Split(protobufTag, ",") + protobufName = "" + for _, part := range tagParts { + if strings.HasPrefix(part, ProtobufNamePrefix) { + protobufName = strings.TrimPrefix(part, ProtobufNamePrefix) + break + } + } + + // Check if the field name is empty + if protobufName == "" { + return nil, fmt.Errorf(MissingProtobufTagNameError, field.Name) + } + + // Check if the field name has already been assigned + if _, ok := rootMapper[protobufName]; ok { + return nil, fmt.Errorf(DuplicateProtobufTagNameError, protobufName) + } + + // Add the field to the map + rootMapper[field.Name] = protobufName + } + + return &Mapper{ + Fields: rootMapper, + NestedMappers: rootNestedMappers, + }, nil +} + +// CreateJSONMapper creates the fields to validate from a JSON struct +func CreateJSONMapper(structInstance interface{}, mode *goflagsmode.Flag) ( + *Mapper, + error, +) { + // Reflection of data + typeReflection := reflect.TypeOf(structInstance) + + // If data is a pointer, dereference it + if typeReflection.Kind() == reflect.Ptr { + typeReflection = typeReflection.Elem() + } + + // Initialize the map fields and the map of nested mappers + rootMapper := make(map[string]string) + rootNestedMappers := make(map[string]*Mapper) + + // Reflection of the type of data + var jsonTag string + var jsonName string + for i := 0; i < typeReflection.NumField(); i++ { + // Get the field type through reflection + field := typeReflection.Field(i) + + // Print field on debug mode + fieldType := field.Type + if mode != nil && mode.IsDebug() { + fmt.Printf("field '%v' type: %v\n", field.Name, fieldType) + } + + // Get the JSON tag of the field + jsonTag = field.Tag.Get(JSONTag) + if jsonTag == "" { + return nil, fmt.Errorf(MissingJSONTagError, field.Name) + } + + // Check if the JSON tag is unassigned + if jsonTag == "-" { + continue + } + + // Check the tag to determine if it contains 'omitempty', which means it is an optional field + if ok := strings.Contains(jsonTag, JSONOmitempty); ok { + continue + } + + // Check if the field is a pointer + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + } + + // Check if the element type is a struct + if fieldType.Kind() == reflect.Struct { + // Create a new Mapper for the nested struct field + fieldNestedMapper, err := CreateJSONMapper( + reflect.New(fieldType).Interface(), + mode, + ) + if err != nil { + return nil, err + } + + // Add the nested fields to the map + rootNestedMappers[field.Name] = fieldNestedMapper + } + + // Get the field name from the JSON tag + tagParts := strings.Split(jsonTag, ",") + if len(tagParts) == 0 { + return nil, fmt.Errorf(EmptyJSONTagError, field.Name) + } + jsonName = tagParts[0] + + // Check if the field name has already been assigned + if _, ok := rootMapper[jsonName]; ok { + return nil, fmt.Errorf(DuplicateJSONTagNameError, jsonName) + } + + // Add the field to the map + rootMapper[field.Name] = jsonName + } + + return &Mapper{ + Fields: rootMapper, + NestedMappers: rootNestedMappers, + }, nil +} diff --git a/structs/validations/errors.go b/structs/validations/errors.go new file mode 100644 index 0000000..6aae587 --- /dev/null +++ b/structs/validations/errors.go @@ -0,0 +1,11 @@ +package validations + +import ( + "errors" +) + +var ( + NilDataError = errors.New("data cannot be nil") + NilMapperError = errors.New("mapper cannot be nil") + FieldNotFoundError = errors.New("field not found") +) diff --git a/structs/validations/struct_fields_validations.go b/structs/validations/struct_fields_validations.go new file mode 100644 index 0000000..076c448 --- /dev/null +++ b/structs/validations/struct_fields_validations.go @@ -0,0 +1,230 @@ +package validations + +import ( + "strings" +) + +// Constants for the struct fields validations +const ( + Validations = "_validations" + Errors = "_errors" +) + +// MapperValidations is a struct that holds the error messages for failed validations of a struct +type MapperValidations struct { + FailedMapperValidations *map[string][]error + NestedMappersValidations *map[string]*MapperValidations +} + +// NewMapperValidations creates a new MapperValidations struct +func NewMapperValidations() *MapperValidations { + // Initialize the struct fields validations + failedFieldsValidations := make(map[string][]error) + nestedFieldsValidations := make(map[string]*MapperValidations) + + return &MapperValidations{ + FailedMapperValidations: &failedFieldsValidations, + NestedMappersValidations: &nestedFieldsValidations, + } +} + +// HasFailed returns true if there are failed validations +func (s *MapperValidations) HasFailed() bool { + // Check if there's a nested failed validations + if s.NestedMappersValidations != nil { + for _, nestedStructFieldsValidations := range *s.NestedMappersValidations { + if nestedStructFieldsValidations.HasFailed() { + return true + } + } + } + + // Check if there are failed fields validations + if s.FailedMapperValidations == nil { + return false + } + return len(*s.FailedMapperValidations) > 0 +} + +// AddFailedFieldValidationError adds a failed field validation error to the struct +func (s *MapperValidations) AddFailedFieldValidationError(validationName string, validationError error) { + // Check if the field name is already in the map + failedFieldsValidations := *s.FailedMapperValidations + if _, ok := failedFieldsValidations[validationName]; !ok { + failedFieldsValidations[validationName] = []error{validationError} + } else { + // Append the validation error to the field name + failedFieldsValidations[validationName] = append(failedFieldsValidations[validationName], validationError) + } +} + +// SetNestedMapperValidations sets the nested struct fields validations to the struct +func (s *MapperValidations) SetNestedMapperValidations( + validationName string, + nestedStructFieldsValidations *MapperValidations, +) { + (*s.NestedMappersValidations)[validationName] = nestedStructFieldsValidations +} + +// GetLevelPadding returns the padding for the level +func (s *MapperValidations) GetLevelPadding(level int) string { + var padding strings.Builder + for i := 0; i < level; i++ { + padding.WriteString("\t") + } + return padding.String() +} + +// FailedValidationsMessage returns a formatted error message for MapperValidations +func (s *MapperValidations) FailedValidationsMessage(level int) *string { + // Check if there are failed validations + if !s.HasFailed() { + return nil + } + + // Get the padding for initial level, the fields, their properties and errors + basePadding := s.GetLevelPadding(level) + fieldPadding := s.GetLevelPadding(level + 1) + fieldPropertiesPadding := s.GetLevelPadding(level + 2) + fieldErrorsPadding := s.GetLevelPadding(level + 3) + + // Create the message + var message strings.Builder + message.WriteString(basePadding) + message.WriteString(Validations) + message.WriteString(": {\n") + + // Get the number of nested fields validations + iteratedFields := make(map[string]bool) + fieldsValidationsNumber := 0 + nestedFieldsValidationsNumber := 0 + iteratedFieldsValidationsNumber := 0 + iteratedNestedFieldsValidationsNumber := 0 + + if s.FailedMapperValidations != nil { + fieldsValidationsNumber = len(*s.FailedMapperValidations) + } + if s.NestedMappersValidations != nil { + nestedFieldsValidationsNumber = len(*s.NestedMappersValidations) + } + + // Iterate over all fields and their errors + for field, fieldErrors := range *s.FailedMapperValidations { + iteratedFieldsValidationsNumber++ + + // Check if the field has no errors + if len(fieldErrors) == 0 { + continue + } + + // Add field name + message.WriteString(fieldPadding) + message.WriteString(field) + message.WriteString(": {\n") + + // Add field properties flag + message.WriteString(fieldPropertiesPadding) + message.WriteString(Errors) + message.WriteString(": [\n") + + // Iterate over all errors for the field + iteratedFields[field] = true + for index, err := range fieldErrors { + message.WriteString(fieldErrorsPadding) + message.WriteString(err.Error()) + + // Add comma if not the last error + if index < len(fieldErrors)-1 { + message.WriteString(",\n") + } else { + message.WriteString("\n") + } + } + + // Get the nested fields validations for the field if it has any + var nestedFieldValidations *MapperValidations + ok := false + if nestedFieldsValidationsNumber > 0 { + nestedFieldValidations, ok = (*s.NestedMappersValidations)[field] + } + + // Add comma if not it does not have nested fields + message.WriteString(fieldPropertiesPadding) + if !ok || !nestedFieldValidations.HasFailed() { + if ok { + iteratedNestedFieldsValidationsNumber++ + } + + message.WriteString("]\n") + } else { + iteratedNestedFieldsValidationsNumber++ + nestedFieldValidationsMessage := nestedFieldValidations.FailedValidationsMessage(level + 1) + + // Add nested fields errors + if nestedFieldValidationsMessage != nil { + message.WriteString("],\n") + message.WriteString(*nestedFieldValidationsMessage) + } + } + + // Add comma if is not the last field + message.WriteString(fieldPadding) + if iteratedFieldsValidationsNumber < fieldsValidationsNumber || iteratedNestedFieldsValidationsNumber < nestedFieldsValidationsNumber { + message.WriteString("},\n") + } else { + message.WriteString("}\n") + } + } + + // Iterate over all nested fields validations + if iteratedNestedFieldsValidationsNumber < nestedFieldsValidationsNumber { + for field, nestedFieldValidations := range *s.NestedMappersValidations { + if _, ok := iteratedFields[field]; ok { + continue + } + + iteratedNestedFieldsValidationsNumber++ + nestedFieldValidationsMessage := nestedFieldValidations.FailedValidationsMessage(level + 1) + + // Add field name + message.WriteString(fieldPadding) + message.WriteString(field) + message.WriteString(": {\n") + + // Add nested fields errors + message.WriteString(fieldPropertiesPadding) + message.WriteString(*nestedFieldValidationsMessage) + + // Add comma if is not the last field + message.WriteString(fieldPadding) + if iteratedNestedFieldsValidationsNumber < nestedFieldsValidationsNumber { + message.WriteString("},\n") + } else { + message.WriteString("}\n") + } + } + } + + // Add closing bracket + message.WriteString(basePadding) + message.WriteString("}") + + // Get message string + messageString := message.String() + + return &messageString +} + +// String returns a formatted error message. If there are no failed validations, it returns nil +func (s *MapperValidations) String() *string { + // Return the failed validations message + message := s.FailedValidationsMessage(0) + + // Replace all escaped characters + if message != nil { + *message = strings.ReplaceAll(*message, "\\t", "\t") + *message = strings.ReplaceAll(*message, "\\n", "\n") + return message + } + return nil +} diff --git a/structs/validations/validate_nil_fields.go b/structs/validations/validate_nil_fields.go new file mode 100644 index 0000000..d564a83 --- /dev/null +++ b/structs/validations/validate_nil_fields.go @@ -0,0 +1,122 @@ +package validations + +import ( + "fmt" + goflagsmode "github.com/ralvarezdev/go-flags/mode" + "github.com/ralvarezdev/go-validator/structs/mapper" + "reflect" +) + +// ValidateMapperNilFields validates if the fields are not nil +func ValidateMapperNilFields( + data interface{}, + mapper *mapper.Mapper, + mode *goflagsmode.Flag, +) (mapperValidations *MapperValidations, err error) { + // Check if either the data or the struct fields to validate are nil + if data == nil { + return nil, NilDataError + } + if mapper == nil { + return nil, NilMapperError + } + + // Initialize struct fields validations + mapperValidations = NewMapperValidations() + + // Reflection of data + valueReflection := reflect.ValueOf(data) + + // If data is a pointer, dereference it + if valueReflection.Kind() == reflect.Ptr { + valueReflection = valueReflection.Elem() + } + + // Iterate over the fields + fields := (*mapper).Fields + nestedMappers := (*mapper).NestedMappers + + // Check if the struct has fields to validate + if fields == nil && nestedMappers == nil { + return nil, nil + } + + // Iterate over the fields + typeReflection := valueReflection.Type() + for i := 0; i < valueReflection.NumField(); i++ { + fieldValue := valueReflection.Field(i) + fieldType := typeReflection.Field(i) + + // Print field on debug mode + if mode != nil && mode.IsDebug() { + fmt.Printf("field '%v' of type '%v' value: %v\n", fieldType.Name, fieldType.Type, fieldValue) + } + + // Check if the field is a pointer + if fieldValue.Kind() != reflect.Ptr { + // Check if the field has to be validated + if fields == nil { + continue + } + validationName, ok := fields[fieldType.Name] + if !ok { + continue + } + + // Check if the field is uninitialized + if fieldValue.IsZero() { + // Print error on debug mode + if mode != nil && mode.IsDebug() { + fmt.Printf("field is uninitialized: %v\n", fieldType.Name) + } + mapperValidations.AddFailedFieldValidationError(validationName, FieldNotFoundError) + } + continue + } + + // Check if the field is a nested struct + if fieldValue.Elem().Kind() != reflect.Struct { + continue // It's an optional field + } + + // Check if the nested struct has to be validated + if fields == nil { + continue + } + validationName, ok := fields[fieldType.Name] + if !ok { + continue + } + + // Check if the field is initialized + if fieldValue.IsNil() { + // Print error on dev mode + if mode != nil && mode.IsDev() { + fmt.Printf("field is uninitialized: %v\n", fieldType.Name) + } + mapperValidations.AddFailedFieldValidationError(validationName, FieldNotFoundError) + continue + } + + // Get the nested struct fields to validate + fieldNestedMapper, ok := nestedMappers[fieldType.Name] + if !ok { + continue + } + + // Validate nested struct + fieldNestedMapperValidations, err := ValidateMapperNilFields( + fieldValue.Addr().Interface(), // TEST IF THIS A POINTER OF THE STRUCT + fieldNestedMapper, + mode, + ) + if err != nil { + return nil, err + } + + // Add nested struct validations to the struct fields validations + mapperValidations.SetNestedMapperValidations(validationName, fieldNestedMapperValidations) + } + + return mapperValidations, nil +}