Skip to content

Commit

Permalink
V2 (#129)
Browse files Browse the repository at this point in the history
# Describe Request

Version 2 of the library.

# Change Type

New feature.
  • Loading branch information
cinar authored Dec 3, 2024
1 parent 80ba6aa commit 33c6896
Show file tree
Hide file tree
Showing 19 changed files with 1,067 additions and 0 deletions.
43 changes: 43 additions & 0 deletions v2/alphanumeric.go
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
}
76 changes: 76 additions & 0 deletions v2/alphanumeric_test.go
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)
}
}
24 changes: 24 additions & 0 deletions v2/check_error.go
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
}
22 changes: 22 additions & 0 deletions v2/check_error_test.go
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)
}
}
11 changes: 11 additions & 0 deletions v2/check_func.go
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)
153 changes: 153 additions & 0 deletions v2/checker.go
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, " ")
}
Loading

0 comments on commit 33c6896

Please sign in to comment.