-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
# Describe Request Version 2 of the library. # Change Type New feature.
- Loading branch information
Showing
19 changed files
with
1,067 additions
and
0 deletions.
There are no files selected for viewing
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,43 @@ | ||
// Copyright (c) 2023-2024 Onur Cinar. | ||
// Use of this source code is governed by a MIT-style | ||
// license that can be found in the LICENSE file. | ||
// https://github.com/cinar/checker | ||
|
||
package v2 | ||
|
||
import ( | ||
"reflect" | ||
"unicode" | ||
) | ||
|
||
const ( | ||
// nameAlphanumeric is the name of the alphanumeric check. | ||
nameAlphanumeric = "alphanumeric" | ||
) | ||
|
||
var ( | ||
// ErrNotAlphanumeric indicates that the given string contains non-alphanumeric characters. | ||
ErrNotAlphanumeric = NewCheckError("ALPHANUMERIC") | ||
) | ||
|
||
// IsAlphanumeric checks if the given string consists of only alphanumeric characters. | ||
func IsAlphanumeric(value string) (string, error) { | ||
for _, c := range value { | ||
if !unicode.IsDigit(c) && !unicode.IsLetter(c) { | ||
return value, ErrNotAlphanumeric | ||
} | ||
} | ||
|
||
return value, nil | ||
} | ||
|
||
// checkAlphanumeric checks if the given string consists of only alphanumeric characters. | ||
func isAlphanumeric(value reflect.Value) (reflect.Value, error) { | ||
_, err := IsAlphanumeric(value.Interface().(string)) | ||
return value, err | ||
} | ||
|
||
// makeAlphanumeric makes a checker function for the alphanumeric checker. | ||
func makeAlphanumeric(_ string) CheckFunc[reflect.Value] { | ||
return isAlphanumeric | ||
} |
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,76 @@ | ||
// Copyright (c) 2023-2024 Onur Cinar. | ||
// Use of this source code is governed by a MIT-style | ||
// license that can be found in the LICENSE file. | ||
// https://github.com/cinar/checker | ||
|
||
package v2_test | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
v2 "github.com/cinar/checker/v2" | ||
) | ||
|
||
func ExampleIsAlphanumeric() { | ||
_, err := v2.IsAlphanumeric("ABcd1234") | ||
if err != nil { | ||
fmt.Println(err) | ||
} | ||
} | ||
|
||
func TestIsAlphanumericInvalid(t *testing.T) { | ||
_, err := v2.IsAlphanumeric("-/") | ||
if err == nil { | ||
t.Fatal("expected error") | ||
} | ||
} | ||
|
||
func TestIsAlphanumericValid(t *testing.T) { | ||
_, err := v2.IsAlphanumeric("ABcd1234") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
} | ||
|
||
func TestCheckAlphanumericNonString(t *testing.T) { | ||
defer FailIfNoPanic(t, "expected panic") | ||
|
||
type Person struct { | ||
Name int `checkers:"alphanumeric"` | ||
} | ||
|
||
person := &Person{} | ||
|
||
v2.CheckStruct(person) | ||
} | ||
|
||
func TestCheckAlphanumericInvalid(t *testing.T) { | ||
type Person struct { | ||
Name string `checkers:"alphanumeric"` | ||
} | ||
|
||
person := &Person{ | ||
Name: "name-/", | ||
} | ||
|
||
_, ok := v2.CheckStruct(person) | ||
if ok { | ||
t.Fatal("expected error") | ||
} | ||
} | ||
|
||
func TestCheckAlphanumericValid(t *testing.T) { | ||
type Person struct { | ||
Name string `checkers:"alphanumeric"` | ||
} | ||
|
||
person := &Person{ | ||
Name: "ABcd1234", | ||
} | ||
|
||
errs, ok := v2.CheckStruct(person) | ||
if !ok { | ||
t.Fatal(errs) | ||
} | ||
} |
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,24 @@ | ||
// Copyright (c) 2023-2024 Onur Cinar. | ||
// Use of this source code is governed by a MIT-style | ||
// license that can be found in the LICENSE file. | ||
// https://github.com/cinar/checker | ||
|
||
package v2 | ||
|
||
// CheckError defines the check error. | ||
type CheckError struct { | ||
// code is the error code. | ||
code string | ||
} | ||
|
||
// NewCheckError creates a new check error with the specified error code. | ||
func NewCheckError(code string) *CheckError { | ||
return &CheckError{ | ||
code: code, | ||
} | ||
} | ||
|
||
// Error returns the error message for the check. | ||
func (c *CheckError) Error() string { | ||
return c.code | ||
} |
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,22 @@ | ||
// Copyright (c) 2023-2024 Onur Cinar. | ||
// Use of this source code is governed by a MIT-style | ||
// license that can be found in the LICENSE file. | ||
// https://github.com/cinar/checker | ||
|
||
package v2_test | ||
|
||
import ( | ||
"testing" | ||
|
||
v2 "github.com/cinar/checker/v2" | ||
) | ||
|
||
func TestCheckErrorError(t *testing.T) { | ||
code := "CODE" | ||
|
||
err := v2.NewCheckError(code) | ||
|
||
if err.Error() != code { | ||
t.Fatalf("actaul %s expected %s", err.Error(), code) | ||
} | ||
} |
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,11 @@ | ||
// Copyright (c) 2023-2024 Onur Cinar. | ||
// Use of this source code is governed by a MIT-style | ||
// license that can be found in the LICENSE file. | ||
// https://github.com/cinar/checker | ||
|
||
package v2 | ||
|
||
// CheckFunc is a function that takes a value of type T and performs | ||
// a check on it. It returns the resulting value and any error that | ||
// occurred during the check. | ||
type CheckFunc[T any] func(value T) (T, error) |
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,153 @@ | ||
// Copyright (c) 2023-2024 Onur Cinar. | ||
// Use of this source code is governed by a MIT-style | ||
// license that can be found in the LICENSE file. | ||
// https://github.com/cinar/checker | ||
|
||
package v2 | ||
|
||
import ( | ||
"fmt" | ||
"reflect" | ||
"strings" | ||
) | ||
|
||
const ( | ||
// checkerTag is the name of the field tag used for checker. | ||
checkerTag = "checkers" | ||
|
||
// sliceConfigPrefix is the prefix used to distinguish slice-level checks from item-level checks. | ||
sliceConfigPrefix = "@" | ||
) | ||
|
||
// checkStructJob defines a check strcut job. | ||
type checkStructJob struct { | ||
Name string | ||
Value reflect.Value | ||
Config string | ||
} | ||
|
||
// Check applies the given check functions to a value sequentially. | ||
// It returns the final value and the first encountered error, if any. | ||
func Check[T any](value T, checks ...CheckFunc[T]) (T, error) { | ||
var err error | ||
|
||
for _, check := range checks { | ||
value, err = check(value) | ||
if err != nil { | ||
break | ||
} | ||
} | ||
|
||
return value, err | ||
} | ||
|
||
// CheckWithConfig applies the check functions specified by the config string to the given value. | ||
// It returns the modified value and the first encountered error, if any. | ||
func CheckWithConfig[T any](value T, config string) (T, error) { | ||
newValue, err := ReflectCheckWithConfig(reflect.Indirect(reflect.ValueOf(value)), config) | ||
return newValue.Interface().(T), err | ||
} | ||
|
||
// ReflectCheckWithConfig applies the check functions specified by the config string | ||
// to the given reflect.Value. It returns the modified reflect.Value and the first | ||
// encountered error, if any. | ||
func ReflectCheckWithConfig(value reflect.Value, config string) (reflect.Value, error) { | ||
return Check(value, makeChecks(config)...) | ||
} | ||
|
||
// CheckStruct checks the given struct based on the validation rules specified in the | ||
// "checker" tag of each struct field. It returns a map of field names to their | ||
// corresponding errors, and a boolean indicating if all checks passed. | ||
func CheckStruct(st any) (map[string]error, bool) { | ||
errs := make(map[string]error) | ||
|
||
jobs := []*checkStructJob{ | ||
{ | ||
Name: "", | ||
Value: reflect.Indirect(reflect.ValueOf(st)), | ||
}, | ||
} | ||
|
||
for len(jobs) > 0 { | ||
job := jobs[0] | ||
jobs = jobs[1:] | ||
|
||
switch job.Value.Kind() { | ||
case reflect.Struct: | ||
for i := 0; i < job.Value.NumField(); i++ { | ||
field := job.Value.Type().Field(i) | ||
|
||
name := fieldName(job.Name, field) | ||
value := reflect.Indirect(job.Value.FieldByIndex(field.Index)) | ||
|
||
jobs = append(jobs, &checkStructJob{ | ||
Name: name, | ||
Value: value, | ||
Config: field.Tag.Get(checkerTag), | ||
}) | ||
} | ||
|
||
case reflect.Slice: | ||
sliceConfig, itemConfig := splitSliceConfig(job.Config) | ||
job.Config = sliceConfig | ||
|
||
for i := 0; i < job.Value.Len(); i++ { | ||
name := fmt.Sprintf("%s[%d]", job.Name, i) | ||
value := reflect.Indirect(job.Value.Index(i)) | ||
|
||
jobs = append(jobs, &checkStructJob{ | ||
Name: name, | ||
Value: value, | ||
Config: itemConfig, | ||
}) | ||
} | ||
} | ||
|
||
if job.Config != "" { | ||
newValue, err := ReflectCheckWithConfig(job.Value, job.Config) | ||
if err != nil { | ||
errs[job.Name] = err | ||
} | ||
|
||
job.Value.Set(newValue) | ||
} | ||
} | ||
|
||
return errs, len(errs) == 0 | ||
} | ||
|
||
// fieldName returns the field name. If a "json" tag is present, it uses the | ||
// tag value instead. It also prepends the parent struct's name (if any) to | ||
// create a fully qualified field name. | ||
func fieldName(prefix string, field reflect.StructField) string { | ||
// Default to field name | ||
name := field.Name | ||
|
||
// Use json tag if present | ||
if jsonTag, ok := field.Tag.Lookup("json"); ok { | ||
name = jsonTag | ||
} | ||
|
||
// Prepend parent name | ||
if prefix != "" { | ||
name = prefix + "." + name | ||
} | ||
|
||
return name | ||
} | ||
|
||
// splitSliceConfig splits config string into slice and item-level configurations. | ||
func splitSliceConfig(config string) (string, string) { | ||
sliceFileds := make([]string, 0) | ||
itemFields := make([]string, 0) | ||
|
||
for _, configField := range strings.Fields(config) { | ||
if strings.HasPrefix(configField, sliceConfigPrefix) { | ||
sliceFileds = append(sliceFileds, strings.TrimPrefix(configField, sliceConfigPrefix)) | ||
} else { | ||
itemFields = append(itemFields, configField) | ||
} | ||
} | ||
|
||
return strings.Join(sliceFileds, " "), strings.Join(itemFields, " ") | ||
} |
Oops, something went wrong.