generated from OtusGolang/home_work
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Pavel Pogodaev <[email protected]>
- Loading branch information
Pavel Pogodaev
committed
Nov 4, 2024
1 parent
411b524
commit e1d4150
Showing
5 changed files
with
319 additions
and
10 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,11 @@ | ||
module github.com/fixme_my_friend/hw09_struct_validator | ||
|
||
go 1.22 | ||
|
||
require github.com/stretchr/testify v1.9.0 | ||
|
||
require ( | ||
github.com/davecgh/go-spew v1.1.1 // indirect | ||
github.com/pmezard/go-difflib v1.0.0 // indirect | ||
gopkg.in/yaml.v3 v3.0.1 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | ||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,210 @@ | ||
package hw09structvalidator | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"reflect" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
) | ||
|
||
type ValidationError struct { | ||
Field string | ||
Err error | ||
} | ||
|
||
type ValidationErrors []ValidationError | ||
|
||
var hashType = map[reflect.Kind]bool{ | ||
reflect.Int: true, | ||
reflect.Int8: true, | ||
reflect.Int16: true, | ||
reflect.Int32: true, | ||
reflect.Int64: true, | ||
reflect.Uint: true, | ||
reflect.Uint8: true, | ||
reflect.Uint16: true, | ||
reflect.Uint32: true, | ||
reflect.Uint64: true, | ||
} | ||
|
||
func (v ValidationErrors) Error() string { | ||
panic("implement me") | ||
errSlc := make([]string, 0, len(v)) | ||
for _, msg := range v { | ||
errSlc = append(errSlc, msg.Err.Error()) | ||
} | ||
|
||
return strings.Join(errSlc, "; ") | ||
} | ||
|
||
func Validate(v interface{}) error { | ||
// Place your code here. | ||
var ( | ||
dataType = reflect.TypeOf(v) | ||
valueType = reflect.ValueOf(v) | ||
errSlice ValidationErrors | ||
) | ||
|
||
if dataType.Kind() != reflect.Struct { | ||
log.Fatalf("invalid type: expected struct, got %v", dataType.Kind()) | ||
} | ||
|
||
for i := 0; i < dataType.NumField(); i++ { | ||
if alias, ok := dataType.Field(i).Tag.Lookup("validate"); ok { | ||
if len(alias) == 0 { | ||
continue | ||
} | ||
|
||
ValidateField( | ||
&errSlice, | ||
strings.Fields(alias), | ||
dataType.Field(i).Name, | ||
valueType.FieldByName(dataType.Field(i).Name), | ||
) | ||
} | ||
} | ||
if len(errSlice) != 0 { | ||
return errSlice | ||
} | ||
return nil | ||
} | ||
|
||
func ValidateField(errSlice *ValidationErrors, tags []string, fieldName string, value reflect.Value) { | ||
tagData := strings.Split(tags[0], "|") | ||
valid := func(tag string, value reflect.Value) { | ||
validateField( | ||
errSlice, | ||
tag, | ||
fieldName, | ||
value, | ||
) | ||
} | ||
|
||
for _, tag := range tagData { | ||
if value.Kind() == reflect.Slice { | ||
if typeEl := value.Type().Elem(); hashType[typeEl.Kind()] { | ||
valid(tag, value) | ||
} else { | ||
for j := 0; j < value.Len(); j++ { | ||
valid(tag, value.Index(j)) | ||
} | ||
} | ||
} else { | ||
valid(tag, value) | ||
} | ||
} | ||
} | ||
|
||
func validateField(errSlice *ValidationErrors, tag string, fieldName string, value reflect.Value) { | ||
switch strings.Split(tag, ":")[0] { | ||
case "regexp": | ||
validateRegexp(errSlice, fieldName, tag, value) | ||
case "len": | ||
validateLength(errSlice, fieldName, tag, value) | ||
case "in": | ||
validateIn(errSlice, fieldName, tag, value) | ||
case "max": | ||
validateMax(errSlice, fieldName, tag, value) | ||
case "min": | ||
validateMin(errSlice, fieldName, tag, value) | ||
} | ||
} | ||
|
||
func validateMin(errSlice *ValidationErrors, fieldName, tag string, value reflect.Value) { | ||
extremum, err := strconv.Atoi(strings.Split(tag, ":")[1]) | ||
if err != nil { | ||
log.Fatalf("can't parse value: %v", err) | ||
} | ||
|
||
if extremum <= int(value.Int()) { | ||
log.Printf("validation field %v success", fieldName) | ||
} else { | ||
*errSlice = append( | ||
*errSlice, | ||
ValidationError{ | ||
Field: fieldName, | ||
Err: fmt.Errorf("validation error: field '%s'", fieldName), | ||
}, | ||
) | ||
} | ||
} | ||
|
||
func validateMax(errSlice *ValidationErrors, fieldName, tag string, value reflect.Value) { | ||
extremum, err := strconv.Atoi(strings.Split(tag, ":")[1]) | ||
if err != nil { | ||
log.Fatalf("parsing int error: %v", err) | ||
} | ||
|
||
if extremum >= int(value.Int()) { | ||
log.Printf("validation field %v success", fieldName) | ||
} else { | ||
*errSlice = append( | ||
*errSlice, | ||
ValidationError{ | ||
Field: fieldName, | ||
Err: fmt.Errorf("validation error: field '%s'", fieldName), | ||
}, | ||
) | ||
} | ||
} | ||
|
||
func validateRegexp(errSlice *ValidationErrors, fieldName, tag string, value reflect.Value) { | ||
re := regexp.MustCompile(strings.Split(tag, ":")[1]) | ||
|
||
if len(re.Find([]byte(value.String()))) != 0 { | ||
log.Printf("validation field %v success", fieldName) | ||
} else { | ||
*errSlice = append( | ||
*errSlice, | ||
ValidationError{ | ||
Field: fieldName, | ||
Err: fmt.Errorf("validation error: field '%s'", fieldName), | ||
}, | ||
) | ||
} | ||
} | ||
|
||
func validateIn(errSlice *ValidationErrors, fieldName, tag string, value reflect.Value) { | ||
var inData string | ||
if value.Kind() == reflect.String { | ||
inData = value.String() | ||
} | ||
|
||
if value.Kind() == reflect.Int { | ||
inData = strconv.Itoa(int(value.Int())) | ||
} | ||
|
||
pattern := strings.ReplaceAll(strings.Split(tag, ":")[1], ",", "|") | ||
re := regexp.MustCompile(pattern) | ||
|
||
if find := re.FindStringSubmatch(inData); len(find) != 0 { | ||
log.Printf("validation field %v success", fieldName) | ||
} else { | ||
*errSlice = append( | ||
*errSlice, | ||
ValidationError{ | ||
Field: fieldName, | ||
Err: fmt.Errorf("validation error: field '%s'", fieldName), | ||
}, | ||
) | ||
} | ||
} | ||
|
||
func validateLength(errSlice *ValidationErrors, fieldName, tag string, value reflect.Value) { | ||
length, err := strconv.Atoi(strings.Split(tag, ":")[1]) | ||
if err != nil { | ||
log.Fatalf("parsing int error: %v", err) | ||
} | ||
|
||
if value.Len() <= length { | ||
log.Printf("validation field %v success", fieldName) | ||
} else { | ||
*errSlice = append( | ||
*errSlice, | ||
ValidationError{ | ||
Field: fieldName, | ||
Err: fmt.Errorf("validation error: field '%s'", fieldName), | ||
}, | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,8 +2,11 @@ package hw09structvalidator | |
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" //nolint:depguard | ||
) | ||
|
||
type UserRole string | ||
|
@@ -26,7 +29,7 @@ type ( | |
|
||
Token struct { | ||
Header []byte | ||
Payload []byte | ||
Payload []byte `validate:"len:5"` | ||
Signature []byte | ||
} | ||
|
||
|
@@ -37,24 +40,119 @@ type ( | |
) | ||
|
||
func TestValidate(t *testing.T) { | ||
tests := []struct { | ||
testsSuccess := []struct { | ||
in interface{} | ||
expectedErr error | ||
}{ | ||
{ | ||
in: User{ | ||
ID: "LNL-b5un6PiLrgv", | ||
Name: "John Smith", | ||
Age: 30, | ||
Email: "[email protected]", | ||
Role: "admin", | ||
Phones: []string{"+347880011"}, | ||
}, | ||
expectedErr: nil, | ||
}, | ||
{ | ||
in: App{ | ||
Version: "6.2.1", | ||
}, | ||
expectedErr: nil, | ||
}, | ||
{ | ||
in: Response{ | ||
Code: 200, | ||
Body: "Success message", | ||
}, | ||
expectedErr: nil, | ||
}, | ||
|
||
{ | ||
in: Token{ | ||
Header: []byte("Content-Length"), | ||
Payload: []byte("12345"), | ||
Signature: []byte("SHA-256"), | ||
}, | ||
expectedErr: nil, | ||
}, | ||
} | ||
|
||
testsFail := []struct { | ||
in interface{} | ||
expectedErr error | ||
}{ | ||
{ | ||
// Place your code here. | ||
in: User{ | ||
ID: "12345678", | ||
Name: "John", | ||
Age: 30, | ||
Email: "[email protected]", | ||
Role: "admin", | ||
Phones: []string{"1234567890"}, | ||
}, | ||
expectedErr: ValidationErrors{ | ||
ValidationError{ | ||
Field: "Email", | ||
Err: fmt.Errorf("validation error: field 'Email'"), | ||
}, | ||
}, | ||
}, | ||
{ | ||
in: Token{ | ||
Header: []byte("Content-Size"), | ||
Payload: []byte("123456"), | ||
Signature: []byte("md5"), | ||
}, | ||
expectedErr: ValidationErrors{ | ||
ValidationError{ | ||
Field: "Payload", | ||
Err: fmt.Errorf("validation error: field 'Payload'"), | ||
}, | ||
}, | ||
}, | ||
{ | ||
in: User{ | ||
ID: "98283q3487987329847", | ||
Name: "John Deer", | ||
Age: 15, | ||
Email: "[email protected]", | ||
Role: "admin", | ||
Phones: []string{"986745321"}, | ||
}, | ||
expectedErr: ValidationErrors{ | ||
ValidationError{ | ||
Field: "Age", | ||
Err: fmt.Errorf("validation error: field 'Age'"), | ||
}, | ||
ValidationError{ | ||
Field: "Email", | ||
Err: fmt.Errorf("validation error: field 'Email'"), | ||
}, | ||
}, | ||
}, | ||
// ... | ||
// Place your code here. | ||
} | ||
|
||
for i, tt := range tests { | ||
for i, tt := range testsSuccess { | ||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { | ||
tt := tt | ||
t.Parallel() | ||
err := Validate(tt.in) | ||
require.Equal(t, err, tt.expectedErr) | ||
}) | ||
} | ||
|
||
// Place your code here. | ||
_ = tt | ||
for i, tt := range testsFail { | ||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { | ||
tt := tt | ||
t.Parallel() | ||
err := Validate(tt.in) | ||
if !errors.As(err, &ValidationErrors{}) { | ||
t.Errorf("unexpected error: got %v, expected %v", err, tt.expectedErr) | ||
} else { | ||
require.Equal(t, tt.expectedErr, err) | ||
} | ||
}) | ||
} | ||
} |