Skip to content

Commit

Permalink
feat: Added Protobuf and JSON struct validator
Browse files Browse the repository at this point in the history
* 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'
  • Loading branch information
ralvarezdev committed Dec 18, 2024
1 parent 421e35c commit a6655f9
Show file tree
Hide file tree
Showing 10 changed files with 638 additions and 0 deletions.
9 changes: 9 additions & 0 deletions field/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package field

import (
"errors"
)

var (
InvalidBirthdateError = errors.New("invalid birthdate")
)
9 changes: 9 additions & 0 deletions field/mail/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package mail

import (
"errors"
)

var (
InvalidMailAddressError = errors.New("invalid mail address")
)
21 changes: 21 additions & 0 deletions field/mail/mail_address.go
Original file line number Diff line number Diff line change
@@ -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
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/ralvarezdev/go-validator

go 1.23.4

require github.com/ralvarezdev/go-flags v0.2.0 // indirect
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
10 changes: 10 additions & 0 deletions structs/mapper/errors.go
Original file line number Diff line number Diff line change
@@ -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"
)
218 changes: 218 additions & 0 deletions structs/mapper/mapper.go
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 11 additions & 0 deletions structs/validations/errors.go
Original file line number Diff line number Diff line change
@@ -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")
)
Loading

0 comments on commit a6655f9

Please sign in to comment.