Skip to content

Commit

Permalink
Merge pull request #55 from muonsoft/unique-constraint
Browse files Browse the repository at this point in the history
unique-constraint
  • Loading branch information
strider2038 authored Jun 20, 2021
2 parents 9b429f7 + 9891183 commit a82edc5
Show file tree
Hide file tree
Showing 22 changed files with 381 additions and 69 deletions.
39 changes: 24 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ List of common [validation arguments](https://pkg.go.dev/github.com/muonsoft/val
* `validation.Bool()` - passes boolean value.
* `validation.Number()` - passes any numeric value. At the moment it uses reflection for executing validation process.
* `validation.String()` - passes 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.
Expand All @@ -76,6 +77,7 @@ For single value validation, you can use shorthand versions of the validation me
* `validator.ValidateBool()`
* `validator.ValidateNumber()`
* `validator.ValidateString()`
* `validator.ValidateStrings()`
* `validator.ValidateIterable()`
* `validator.ValidateCountable()`
* `validator.ValidateTime()`
Expand Down Expand Up @@ -173,6 +175,7 @@ For a better experience with struct validation, you can use shorthand versions o
* `validation.BoolProperty()`
* `validation.NumberProperty()`
* `validation.StringProperty()`
* `validation.StringsProperty()`
* `validation.IterableProperty()`
* `validation.CountableProperty()`
* `validation.TimeProperty()`
Expand Down Expand Up @@ -200,22 +203,25 @@ There are few ways to validate structs. The simplest one is to call the `validat
```golang
document := Document{
Title: "",
Keywords: []string{""},
Keywords: []string{"", "book", "fantasy", "book"},
}

err := validator.Validate(
validation.StringProperty("title", &document.Title, it.IsNotBlank()),
validation.CountableProperty("keywords", len(document.Keywords), it.HasCountBetween(2, 10)),
validation.CountableProperty("keywords", len(document.Keywords), it.HasCountBetween(5, 10)),
validation.StringsProperty("keywords", document.Keywords, it.HasUniqueValues()),
validation.EachStringProperty("keywords", document.Keywords, it.IsNotBlank()),
)

violations := err.(validation.ViolationList)
for _, violation := range violations {
fmt.Println(violation.Error())
if violations, ok := validation.UnwrapViolationList(err); ok {
for violation := violations.First(); violation != nil; violation = violation.Next() {
fmt.Println(violation)
}
}
// Output:
// violation at 'title': This value should not be blank.
// violation at 'keywords': This collection should contain 2 elements or more.
// violation at 'keywords': This collection should contain 5 elements or more.
// violation at 'keywords': This collection should contain only unique elements.
// violation at 'keywords[0]': This value should not be blank.
```

Expand All @@ -231,7 +237,9 @@ type Product struct {
func (p Product) Validate(validator *validation.Validator) error {
return validator.Validate(
validation.StringProperty("name", &p.Name, it.IsNotBlank()),
validation.IterableProperty("tags", p.Tags, it.HasMinCount(1)),
validation.CountableProperty("tags", len(p.Tags), it.HasMinCount(5)),
validation.StringsProperty("tags", p.Tags, it.HasUniqueValues()),
validation.EachStringProperty("tags", p.Tags, it.IsNotBlank()),
// this also runs validation on each of the components
validation.IterableProperty("components", p.Components, it.HasMinCount(1)),
)
Expand All @@ -253,6 +261,7 @@ func (c Component) Validate(validator *validation.Validator) error {
func main() {
p := Product{
Name: "",
Tags: []string{"device", "", "phone", "device"},
Components: []Component{
{
ID: 1,
Expand All @@ -269,7 +278,9 @@ func main() {
}
// Output:
// violation at 'name': This value should not be blank.
// violation at 'tags': This collection should contain 1 element or more.
// violation at 'tags': This collection should contain 5 elements or more.
// violation at 'tags': This collection should contain only unique elements.
// violation at 'tags[1]': This value should not be blank.
// violation at 'components[0].name': This value should not be blank.
// violation at 'components[0].tags': This collection should contain 1 element or more.
}
Expand Down Expand Up @@ -313,13 +324,10 @@ Also, you can use helper function `validation.UnwrapViolationList()`.

```golang
err := validator.Validate(/* validation arguments */)
if err != nil {
violations, ok := validation.UnwrapViolationList(err)
if ok {
// handle violations
} else {
// handle internal error
}
if violations, ok := validation.UnwrapViolationList(err); ok {
// handle violations
} else if err != nil {
// handle internal error
}
```

Expand Down Expand Up @@ -465,6 +473,7 @@ Everything you need to create a custom constraint is to implement one of the int
* `BoolConstraint` - for validating boolean values;
* `NumberConstraint` - for validating numeric values;
* `StringConstraint` - for validating string values;
* `StringsConstraint` - for validating slice of strings;
* `IterableConstraint` - for validating iterable values: arrays, slices, or maps;
* `CountableConstraint` - for validating iterable values based only on the count of elements;
* `TimeConstraint` - for validating date\time values.
Expand Down
14 changes: 14 additions & 0 deletions arguments.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,20 @@ func StringProperty(name string, value *string, options ...Option) Argument {
return String(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 {
arguments.addValidator(newStringsValidator(values, options))

return nil
})
}

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

// Iterable argument is used to validate arrays, slices, or maps. 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.
Expand Down
1 change: 1 addition & 0 deletions code/codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
NotNil = "notNil"
NotPositive = "notPositive"
NotPositiveOrZero = "notPositiveOrZero"
NotUnique = "notUnique"
NotValid = "notValid"
ProhibitedIP = "prohibitedIP"
TooEarly = "tooEarly"
Expand Down
6 changes: 6 additions & 0 deletions contraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ type StringConstraint interface {
ValidateString(value *string, scope Scope) error
}

// StringsConstraint is used to build constraints to validate an array or a slice of strings.
type StringsConstraint interface {
Constraint
ValidateStrings(values []string, scope Scope) error
}

// IterableConstraint is used to build constraints for validation of iterables (arrays, slices, or maps).
//
// At this moment working with numbers is based on reflection.
Expand Down
28 changes: 25 additions & 3 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,26 @@ func ExampleStringProperty() {
// violation at 'title': This value should not be blank.
}

func ExampleStrings() {
v := []string{"foo", "bar", "baz", "foo"}
err := validator.Validate(
validation.Strings(v, it.HasUniqueValues()),
)
fmt.Println(err)
// Output:
// violation: This collection should contain only unique elements.
}

func ExampleStringsProperty() {
v := Book{Keywords: []string{"foo", "bar", "baz", "foo"}}
err := validator.Validate(
validation.StringsProperty("keywords", v.Keywords, it.HasUniqueValues()),
)
fmt.Println(err)
// Output:
// violation at 'keywords': This collection should contain only unique elements.
}

func ExampleIterable() {
v := make([]string, 0)
err := validator.Validate(validation.Iterable(v, it.IsNotBlank()))
Expand Down Expand Up @@ -376,12 +396,13 @@ func ExampleValidator_Validate_basicStructValidation() {
Keywords []string
}{
Title: "",
Keywords: []string{""},
Keywords: []string{"", "book", "fantasy", "book"},
}

err := validator.Validate(
validation.StringProperty("title", &document.Title, it.IsNotBlank()),
validation.CountableProperty("keywords", len(document.Keywords), it.HasCountBetween(2, 10)),
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 @@ -392,7 +413,8 @@ func ExampleValidator_Validate_basicStructValidation() {
}
// Output:
// violation at 'title': This value should not be blank.
// violation at 'keywords': This collection should contain 2 elements or more.
// violation at 'keywords': This collection should contain 5 elements or more.
// violation at 'keywords': This collection should contain only unique elements.
// violation at 'keywords[0]': This value should not be blank.
}

Expand Down
9 changes: 7 additions & 2 deletions example_validatable_struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ type Product struct {
func (p Product) Validate(validator *validation.Validator) error {
return validator.Validate(
validation.StringProperty("name", &p.Name, it.IsNotBlank()),
validation.IterableProperty("tags", p.Tags, it.HasMinCount(1)),
validation.CountableProperty("tags", len(p.Tags), it.HasMinCount(5)),
validation.StringsProperty("tags", p.Tags, it.HasUniqueValues()),
validation.EachStringProperty("tags", p.Tags, it.IsNotBlank()),
// this also runs validation on each of the components
validation.IterableProperty("components", p.Components, it.HasMinCount(1)),
)
Expand All @@ -39,6 +41,7 @@ func (c Component) Validate(validator *validation.Validator) error {
func ExampleValidator_ValidateValidatable_validatableStruct() {
p := Product{
Name: "",
Tags: []string{"device", "", "phone", "device"},
Components: []Component{
{
ID: 1,
Expand All @@ -56,7 +59,9 @@ func ExampleValidator_ValidateValidatable_validatableStruct() {
}
// Output:
// violation at 'name': This value should not be blank.
// violation at 'tags': This collection should contain 1 element or more.
// violation at 'tags': This collection should contain 5 elements or more.
// violation at 'tags': This collection should contain only unique elements.
// violation at 'tags[1]': This value should not be blank.
// violation at 'components[0].name': This value should not be blank.
// violation at 'components[0].tags': This collection should contain 1 element or more.
}
18 changes: 18 additions & 0 deletions is/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,21 @@ import "encoding/json"
func JSON(value string) bool {
return json.Valid([]byte(value))
}

// UniqueStrings checks that slice of strings has unique values.
func UniqueStrings(values []string) bool {
if len(values) == 0 {
return true
}

uniques := make(map[string]struct{}, len(values))

for _, value := range values {
if _, exists := uniques[value]; exists {
return false
}
uniques[value] = struct{}{}
}

return true
}
10 changes: 10 additions & 0 deletions is/data_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,13 @@ func ExampleJSON() {
// true
// false
}

func ExampleUniqueStrings() {
fmt.Println(is.UniqueStrings([]string{}))
fmt.Println(is.UniqueStrings([]string{"one", "two", "three"}))
fmt.Println(is.UniqueStrings([]string{"one", "two", "one"}))
// Output:
// true
// true
// false
}
Loading

0 comments on commit a82edc5

Please sign in to comment.