Skip to content

Commit

Permalink
Merge pull request #58 from muonsoft/next
Browse files Browse the repository at this point in the history
Next version
  • Loading branch information
strider2038 authored Aug 11, 2021
2 parents 08e96c9 + 29ee61a commit 9065c50
Show file tree
Hide file tree
Showing 34 changed files with 707 additions and 497 deletions.
60 changes: 30 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ go get -u github.com/muonsoft/validation
The validation process is built around functional options and passing values by specific typed arguments. A common way to use validation is to call the `validator.Validate` method and pass the argument option with the list of validation constraints.

```golang
s := ""

err := validator.Validate(validation.String(&s, it.IsNotBlank()))
err := validator.Validate(context.Background(), validation.String("", it.IsNotBlank()))

violations := err.(validation.ViolationList)
for _, violation := range violations {
Expand All @@ -61,12 +59,15 @@ List of common [validation arguments](https://pkg.go.dev/github.com/muonsoft/val

* `validation.Value()` - passes any value. It uses reflection to detect the type of the argument and pass it to a specific validation method.
* `validation.Bool()` - passes boolean value.
* `validation.NilBool()` - passes nillable boolean value.
* `validation.Number()` - passes any numeric value. At the moment it uses reflection for executing validation process.
* `validation.String()` - passes string value.
* `validation.NilString()` - passes nillable string value.
* `validation.Strings()` - passes slice of strings value.
* `validation.Iterable()` - passes array, slice or a map. At the moment it uses reflection for executing validation process.
* `validation.Countable()` - you can pass result of `len()` to use easy way of iterable validation based only on count of the elements.
* `validation.Time()` - passes `time.Time` value.
* `validation.NilTime()` - passes nillable `time.Time` value.
* `validation.Each()` - passes array, slice or a map. Used to validate each value of iterable. It uses reflection.
* `validation.EachString()` - passes slice of strings. This is more performant version than `Each`.
* `validation.Valid()` - passes `Validatable` value to run embedded validation.
Expand Down Expand Up @@ -134,11 +135,10 @@ The [property path](https://pkg.go.dev/github.com/muonsoft/validation#PropertyPa
You can pass a property name or an array index via `validation.PropertyName()` and `validation.ArrayIndex()` options.

```golang
s := ""

err := validator.Validate(
context.Background(),
validation.String(
&s,
"",
validation.PropertyName("properties"),
validation.ArrayIndex(1),
validation.PropertyName("tag"),
Expand All @@ -155,13 +155,11 @@ fmt.Println("property path:", violation.GetPropertyPath().Format())
Also, you can create scoped validator by using `valdiator.AtProperty()` or `validator.AtIndex()` methods. It can be used to validate a couple of attributes of one object.

```golang
s := ""

err := validator.
AtProperty("properties").
AtIndex(1).
AtProperty("tag").
Validate(validation.String(&s, it.IsNotBlank()))
Validate(context.Background(), validation.String("", it.IsNotBlank()))

violation := err.(validation.ViolationList)[0]
fmt.Println("property path:", violation.GetPropertyPath().Format())
Expand All @@ -173,21 +171,23 @@ For a better experience with struct validation, you can use shorthand versions o

* `validation.PropertyValue()`
* `validation.BoolProperty()`
* `validation.NilBoolProperty()`
* `validation.NumberProperty()`
* `validation.StringProperty()`
* `validation.NilStringProperty()`
* `validation.StringsProperty()`
* `validation.IterableProperty()`
* `validation.CountableProperty()`
* `validation.TimeProperty()`
* `validation.NilTimeProperty()`
* `validation.EachProperty()`
* `validation.EachStringProperty()`
* `validation.ValidProperty()`

```golang
s := ""

err := validator.Validate(
validation.StringProperty("property", &s, it.IsNotBlank()),
context.Background(),
validation.StringProperty("property", "", it.IsNotBlank()),
)

violation := err.(validation.ViolationList)[0]
Expand All @@ -207,7 +207,8 @@ document := Document{
}

err := validator.Validate(
validation.StringProperty("title", &document.Title, it.IsNotBlank()),
context.Background(),
validation.StringProperty("title", document.Title, it.IsNotBlank()),
validation.CountableProperty("keywords", len(document.Keywords), it.HasCountBetween(5, 10)),
validation.StringsProperty("keywords", document.Keywords, it.HasUniqueValues()),
validation.EachStringProperty("keywords", document.Keywords, it.IsNotBlank()),
Expand All @@ -234,9 +235,10 @@ type Product struct {
Components []Component
}

func (p Product) Validate(validator *validation.Validator) error {
func (p Product) Validate(ctx context.Context, validator *validation.Validator) error {
return validator.Validate(
validation.StringProperty("name", &p.Name, it.IsNotBlank()),
ctx,
validation.StringProperty("name", p.Name, it.IsNotBlank()),
validation.CountableProperty("tags", len(p.Tags), it.HasMinCount(5)),
validation.StringsProperty("tags", p.Tags, it.HasUniqueValues()),
validation.EachStringProperty("tags", p.Tags, it.IsNotBlank()),
Expand All @@ -251,9 +253,10 @@ type Component struct {
Tags []string
}

func (c Component) Validate(validator *validation.Validator) error {
func (c Component) Validate(ctx context.Context, validator *validation.Validator) error {
return validator.Validate(
validation.StringProperty("name", &c.Name, it.IsNotBlank()),
ctx,
validation.StringProperty("name", c.Name, it.IsNotBlank()),
validation.CountableProperty("tags", len(c.Tags), it.HasMinCount(1)),
)
}
Expand All @@ -270,7 +273,7 @@ func main() {
},
}

err := validator.ValidateValidatable(p)
err := validator.ValidateValidatable(context.Background(), p)

violations := err.(validation.ViolationList)
for _, violation := range violations {
Expand All @@ -292,7 +295,8 @@ You can use the `When()` method on any of the built-in constraints to execute co

```golang
err := validator.Validate(
validation.StringProperty("text", &note.Text, it.IsNotBlank().When(note.IsPublic)),
context.Background(),
validation.StringProperty("text", note.Text, it.IsNotBlank().When(note.IsPublic)),
)

violations := err.(validation.ViolationList)
Expand Down Expand Up @@ -363,8 +367,7 @@ validator, _ := validation.NewValidator(
validation.DefaultLanguage(language.Russian),
)

s := ""
err := validator.ValidateString(&s, it.IsNotBlank())
err := validator.ValidateString(context.Background(), "", it.IsNotBlank())

violations := err.(validation.ViolationList)
for _, violation := range violations {
Expand All @@ -381,10 +384,10 @@ validator, _ := validation.NewValidator(
validation.Translations(russian.Messages),
)

s := ""
err := validator.Validate(
context.Background(),
validation.Language(language.Russian),
validation.String(&s, it.IsNotBlank()),
validation.String("", it.IsNotBlank()),
)

violations := err.(validation.ViolationList)
Expand All @@ -395,7 +398,7 @@ for _, violation := range violations {
// violation: Значение не должно быть пустым.
```

The last way is to pass language via context. It is provided by the `github.com/muonsoft/language` package and can be useful in combination with [language middleware](https://github.com/muonsoft/language/blob/main/middleware.go). You can pass the context by using the `validation.Context()` argument or by creating a scoped validator with the `validator.WithContext()` method.
The last way is to pass language via context. It is provided by the `github.com/muonsoft/language` package and can be useful in combination with [language middleware](https://github.com/muonsoft/language/blob/main/middleware.go).

```golang
// import "github.com/muonsoft/language"
Expand All @@ -404,10 +407,8 @@ validator, _ := validation.NewValidator(
validation.Translations(russian.Messages),
)
ctx := language.WithContext(context.Background(), language.Russian)
validator = validator.WithContext(ctx)

s := ""
err := validator.ValidateString(&s, it.IsNotBlank())
err := validator.ValidateString(ctx, "", it.IsNotBlank())

violations := err.(validation.ViolationList)
for _, violation := range violations {
Expand All @@ -424,9 +425,7 @@ You can see the complex example with handling HTTP request [here](https://pkg.go
You may customize the violation message on any of the built-in constraints by calling the `Message()` method or similar if the constraint has more than one template. Also, you can include template parameters in it. See details of a specific constraint to know what parameters are available.

```golang
s := ""

err := validator.ValidateString(&s, it.IsNotBlank().Message("this value is required"))
err := validator.ValidateString(context.Background(), "", it.IsNotBlank().Message("this value is required"))

violations := err.(validation.ViolationList)
for _, violation := range violations {
Expand All @@ -453,6 +452,7 @@ validator, _ := validation.NewValidator(

var tags []string
err := validator.ValidateIterable(
context.Background(),
tags,
validation.Language(language.Russian),
it.HasMinCount(1).MinMessage(customMessage),
Expand Down
76 changes: 51 additions & 25 deletions arguments.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package validation

import (
"context"
"fmt"
"time"

Expand Down Expand Up @@ -54,19 +53,33 @@ func PropertyValue(name string, value interface{}, options ...Option) Argument {
}

// Bool argument is used to validate boolean values.
func Bool(value *bool, options ...Option) Argument {
func Bool(value bool, options ...Option) Argument {
return argumentFunc(func(arguments *Arguments) error {
arguments.addValidator(newBoolValidator(value, options))
arguments.addValidator(newBoolValidator(&value, options))

return nil
})
}

// BoolProperty argument is an alias for Bool that automatically adds property name to the current scope.
func BoolProperty(name string, value *bool, options ...Option) Argument {
func BoolProperty(name string, value bool, options ...Option) Argument {
return Bool(value, append([]Option{PropertyName(name)}, options...)...)
}

// NilBool argument is used to validate nillable boolean values.
func NilBool(value *bool, options ...Option) Argument {
return argumentFunc(func(arguments *Arguments) error {
arguments.addValidator(newBoolValidator(value, options))

return nil
})
}

// NilBoolProperty argument is an alias for NilBool that automatically adds property name to the current scope.
func NilBoolProperty(name string, value *bool, options ...Option) Argument {
return NilBool(value, append([]Option{PropertyName(name)}, options...)...)
}

// Number argument is used to validate numbers (any types of integers or floats). At the moment it uses
// reflection to detect numeric value. Given value is internally converted into int64 or float64 to make comparisons.
//
Expand All @@ -90,19 +103,33 @@ func NumberProperty(name string, value interface{}, options ...Option) Argument
}

// String argument is used to validate strings.
func String(value *string, options ...Option) Argument {
func String(value string, options ...Option) Argument {
return argumentFunc(func(arguments *Arguments) error {
arguments.addValidator(newStringValidator(value, options))
arguments.addValidator(newStringValidator(&value, options))

return nil
})
}

// StringProperty argument is an alias for String that automatically adds property name to the current scope.
func StringProperty(name string, value *string, options ...Option) Argument {
func StringProperty(name string, value string, options ...Option) Argument {
return String(value, append([]Option{PropertyName(name)}, options...)...)
}

// NilString argument is used to validate nillable strings.
func NilString(value *string, options ...Option) Argument {
return argumentFunc(func(arguments *Arguments) error {
arguments.addValidator(newStringValidator(value, options))

return nil
})
}

// NilStringProperty argument is an alias for NilString that automatically adds property name to the current scope.
func NilStringProperty(name string, value *string, options ...Option) Argument {
return NilString(value, append([]Option{PropertyName(name)}, options...)...)
}

// Strings argument is used to validate slice of strings.
func Strings(values []string, options ...Option) Argument {
return argumentFunc(func(arguments *Arguments) error {
Expand Down Expand Up @@ -156,19 +183,33 @@ func CountableProperty(name string, count int, options ...Option) Argument {
}

// Time argument is used to validate time.Time value.
func Time(value *time.Time, options ...Option) Argument {
func Time(value time.Time, options ...Option) Argument {
return argumentFunc(func(arguments *Arguments) error {
arguments.addValidator(newTimeValidator(value, options))
arguments.addValidator(newTimeValidator(&value, options))

return nil
})
}

// TimeProperty argument is an alias for Time that automatically adds property name to the current scope.
func TimeProperty(name string, value *time.Time, options ...Option) Argument {
func TimeProperty(name string, value time.Time, options ...Option) Argument {
return Time(value, append([]Option{PropertyName(name)}, options...)...)
}

// NilTime argument is used to validate nillable time.Time value.
func NilTime(value *time.Time, options ...Option) Argument {
return argumentFunc(func(arguments *Arguments) error {
arguments.addValidator(newTimeValidator(value, options))

return nil
})
}

// NilTimeProperty argument is an alias for NilTime that automatically adds property name to the current scope.
func NilTimeProperty(name string, value *time.Time, options ...Option) Argument {
return NilTime(value, append([]Option{PropertyName(name)}, options...)...)
}

// Each is used to validate each value of iterable (array, slice, or map). At the moment it uses reflection
// to iterate over values. So you can expect a performance hit using this method. For better performance
// it is recommended to make a custom type that implements the Validatable interface. Also, you can use
Expand Down Expand Up @@ -222,21 +263,6 @@ func ValidProperty(name string, value Validatable, options ...Option) Argument {
return Valid(value, append([]Option{PropertyName(name)}, options...)...)
}

// Context can be used to pass context to validation constraints via scope.
//
// Example
// err := validator.Validate(
// Context(request.Context()),
// String(&s, it.IsNotBlank()), // now all called constraints will use passed context in their methods
// )
func Context(ctx context.Context) Argument {
return argumentFunc(func(arguments *Arguments) error {
arguments.scope.context = ctx

return nil
})
}

// Language argument sets the current language for translation of a violation message.
//
// Example
Expand Down
15 changes: 7 additions & 8 deletions example_context_with_recursion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,13 @@ type recursionKey string

const nestingLevelKey recursionKey = "nestingLevel"

func (p Property) Validate(validator *validation.Validator) error {
// Incrementing nesting level in context with special function.
ctx := contextWithNextNestingLevel(validator.Context())

return validator.WithContext(ctx).Validate(
func (p Property) Validate(ctx context.Context, validator *validation.Validator) error {
return validator.Validate(
// Incrementing nesting level in context with special function.
contextWithNextNestingLevel(ctx),
// Executing validation for maximum nesting level of properties.
PropertyArgument(&p, ItIsNotDeeperThan(3)),
validation.StringProperty("name", &p.Name, it.IsNotBlank()),
validation.StringProperty("name", p.Name, it.IsNotBlank()),
// This should run recursive validation for properties.
validation.IterableProperty("properties", p.Properties),
)
Expand All @@ -102,7 +101,7 @@ func contextWithNextNestingLevel(ctx context.Context) context.Context {
return context.WithValue(ctx, nestingLevelKey, level+1)
}

func ExampleValidator_Context_usingContextWithRecursion() {
func ExampleValidator_Validate_usingContextWithRecursion() {
properties := []Property{
{
Name: "top",
Expand All @@ -123,7 +122,7 @@ func ExampleValidator_Context_usingContextWithRecursion() {
},
}

err := validator.ValidateIterable(properties)
err := validator.Validate(context.Background(), validation.Iterable(properties))

fmt.Println(err)
// Output:
Expand Down
3 changes: 1 addition & 2 deletions example_custom_argument_constraint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,10 @@ func ExampleNewArgument_customArgumentConstraintValidator() {
isEntityUnique := &UniqueBrandConstraint{brands: repository}

brand := Brand{Name: "Apple"}
ctx := context.WithValue(context.Background(), exampleKey, "value")

err := validator.Validate(
// you can pass here the context value to the validation scope
validation.Context(ctx),
context.WithValue(context.Background(), exampleKey, "value"),
BrandArgument(&brand, it.IsNotBlank(), isEntityUnique),
)

Expand Down
Loading

0 comments on commit 9065c50

Please sign in to comment.