Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unique-constraint #55

Merged
merged 1 commit into from
Jun 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -98,6 +98,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 @@ -377,12 +397,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 @@ -393,7 +414,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