From 169b9499fe4def519aab5c84f4fd12f4d72fef0c Mon Sep 17 00:00:00 2001 From: "Stanislav N. aka pztrn" Date: Tue, 3 Dec 2019 21:21:57 +0500 Subject: [PATCH] Initial commit, valiwork as framework is ready to use. --- .drone.yml | 83 ++++++++++ .golangci.yml | 17 ++ README.md | 27 +++ debug.go | 25 +++ errors.go | 17 ++ example/main.go | 43 +++++ go.mod | 5 + go.sum | 12 ++ validations.go | 97 +++++++++++ validations_test.go | 266 ++++++++++++++++++++++++++++++ validators/validator_signature.go | 4 + 11 files changed, 596 insertions(+) create mode 100644 .drone.yml create mode 100644 .golangci.yml create mode 100644 README.md create mode 100644 debug.go create mode 100644 errors.go create mode 100644 example/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 validations.go create mode 100644 validations_test.go create mode 100644 validators/validator_signature.go diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..a94e1ce --- /dev/null +++ b/.drone.yml @@ -0,0 +1,83 @@ +--- +kind: pipeline +type: docker +name: build + +steps: + - name: notify-start + image: pztrn/discordrone + settings: + webhook_id: + from_secret: discord_webhook_id + webhook_token: + from_secret: discord_webhook_secret + message: 'Starting testing, benchmarking and linting for **{{repo.name}}#{{build.number}}@{{commit.sha}}** @ {{datetime build.started "02-Jan-2006 15:04:05 MST" "Asia/Yekaterinburg"}} (See {{build.link}} for logs).' + + - name: lint + image: golangci/golangci-lint:latest + environment: + CGO_ENABLED: 0 + commands: + - golangci-lint run + depends_on: + - notify-start + + - name: test + image: golang:1.13.4-alpine + environment: + CGO_ENABLED: 0 + commands: + - go test -test.v ./... + depends_on: + - notify-start + + - name: test-race + image: golang:1.13.4-alpine + environment: + CGO_ENABLED: 0 + commands: + - go test -race -test.v ./... + depends_on: + - notify-start + + - name: benchmark + image: golang:1.13.4-alpine + environment: + CGO_ENABLED: 0 + commands: + - go test -benchmem -run=^$ go.dev.pztrn.name/valiwork -bench . + depends_on: + - notify-start + + - name: benchmark-race + image: golang:1.13.4-alpine + environment: + CGO_ENABLED: 0 + commands: + - go test -benchmem -run=^$ go.dev.pztrn.name/valiwork -race -bench . + depends_on: + - notify-start + + - name: notify-end + when: + status: + - success + - failure + image: pztrn/discordrone + settings: + webhook_id: + from_secret: discord_webhook_id + webhook_token: + from_secret: discord_webhook_secret + message: " + {{#success build.status}} + **{{repo.name}}#{{build.number}}@{{commit.sha}}** tested, benchmarked and linted successfully. + {{ else }} + **{{repo.name}}#{{build.number}}@{{commit.sha}}** failed. See {{build.link}}. + {{/success}}" + depends_on: + - benchmark + - benchmark-race + - lint + - test + - test-race \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..1e0096a --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,17 @@ +run: + deadline: 5m +linters: + enable-all: true + disable: + # Because globals might exist, but according to our codestyle they + # should be lowercased and considered as unexported. + - gochecknoglobals + # While it might be useful it'll create more problems that will solve. + - gocritic + # This linter goes crazy for nothing (almost). + - funlen +linters-settings: + lll: + line-length: 420 + gocyclo: + min-complexity: 40 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..67feb94 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# ValiWork - validation framework + +ValiWork is a validation framework that provides sane API and ability to write own validators that returns arbitrary things. It is goroutine-safe and fast. + +## Default validators + +There are no necessity to enable default validators at all. But if you want to - call: + +```go +valiwork.InitializeDefaultValidators() +``` + +Default validators will return ``error``. + +## Validators registering and namespacing + +Default validators using "T_N" scheme, where ``T`` is data type (string, int, int64, etc.) and ``N`` is a validator name (which can be a generic string). Please, use same naming scheme. Example good validators names: + +* ``string_check_for_very_rare_symbol_that_is_not_allowed`` +* ``int64_check_if_in_bad_range`` +* ``interface_check_if_able_to_be_TheVeryGoodStruct`` + +Key idea is to help you debugging this thing (see [debug section](#Debug) below). + +## Debug + +Define ``VALIWORK_DEBUG`` environment variable and set it to ``true`` to get debug output. Default ``log`` module will be used for that. diff --git a/debug.go b/debug.go new file mode 100644 index 0000000..44be4d9 --- /dev/null +++ b/debug.go @@ -0,0 +1,25 @@ +package valiwork + +import ( + // stdlib + "os" + "strconv" +) + +var ( + DEBUG bool +) + +// Initializes debug output. +// nolint +func init() { + debug, found := os.LookupEnv("VALIWORK_DEBUG") + if found { + debugBool, err := strconv.ParseBool(debug) + if err != nil { + return + } + + DEBUG = debugBool + } +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..ef579da --- /dev/null +++ b/errors.go @@ -0,0 +1,17 @@ +package valiwork + +import ( + // stdlib + "errors" +) + +var ( + // ErrValidatorAlreadyRegistered appears when validator's name + // passed to RegisterValidator function already used for other + // validator function. + ErrValidatorAlreadyRegistered = errors.New("validator with such name already registered") + + // ErrValidatorNotRegistered appears when trying to unregister + // not registered validator function. + ErrValidatorNotRegistered = errors.New("validator with such name wasn't registered") +) diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..393b1de --- /dev/null +++ b/example/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "strings" + // stdlib + "errors" + "log" + + // other + "go.dev.pztrn.name/valiwork" +) + +const ( + stringValidatorName = "string_validate_things" +) + +func main() { + log.Println("Starting validation example...") + log.Println("WARN: to see additional valiwork output define 'VALIWORK_DEBUG' environment variable and set it to 'true'!") + + //stringToValidate := " I am pretty b@d $tring" + + valiwork.RegisterValidator(stringValidatorName, stringValidator) +} + +func stringValidator(thing interface{}, optional ...interface{}) []interface{} { + var errs []interface{} + + stringToValidate, ok := thing.(string) + if !ok { + errs = append(errs, errors.New("passed value is not a string")) + return errs + } + + // Are string begins with spaces? + if strings.HasPrefix(stringToValidate, " ") { + errs = append(errs, errors.New("string begins with space")) + } + + // Does string contains any special characters? + + return errs +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9cf867b --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module go.dev.pztrn.name/valiwork + +go 1.13 + +require github.com/stretchr/testify v1.4.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..68164f9 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/validations.go b/validations.go new file mode 100644 index 0000000..9fbae32 --- /dev/null +++ b/validations.go @@ -0,0 +1,97 @@ +package valiwork + +import ( + // stdlib + "log" + "sync" + + // local + "go.dev.pztrn.name/valiwork/validators" +) + +var ( + registeredValidators map[string]validators.ValidatorFunc + rvMutex sync.RWMutex +) + +// nolint +func init() { + initializeValidatorsStorage() +} + +func initializeValidatorsStorage() { + registeredValidators = make(map[string]validators.ValidatorFunc) +} + +// RegisterValidator registers validation function for later calling. +func RegisterValidator(validatorName string, validator validators.ValidatorFunc) error { + if DEBUG { + log.Println("Trying to register validator: '" + validatorName + "'...") + } + + //rvMutex.RLock() + _, found := registeredValidators[validatorName] + //rvMutex.RUnlock() + + if found { + if DEBUG { + log.Println("Validator already registered!") + } + + return ErrValidatorAlreadyRegistered + } + + //rvMutex.Lock() + registeredValidators[validatorName] = validator + //rvMutex.Unlock() + + return nil +} + +// UnregisterValidator removes registered validator from list of known +// validators. +func UnregisterValidator(validatorName string) error { + if DEBUG { + log.Println("Trying to unregister validator '" + validatorName + "'...") + } + + //rvMutex.RLock() + _, found := registeredValidators[validatorName] + //rvMutex.RUnlock() + + if !found { + if DEBUG { + log.Println("Validator wasn't registered!") + } + + return ErrValidatorNotRegistered + } + + //rvMutex.Lock() + delete(registeredValidators, validatorName) + //rvMutex.Unlock() + + return nil +} + +// Validate launches validation function and returns it's result to +// caller. +func Validate(validatorName string, thing interface{}, optional ...interface{}) []interface{} { + var errs []interface{} + + //rvMutex.RLock() + validator, found := registeredValidators[validatorName] + //rvMutex.RUnlock() + + if !found { + errs = append(errs, ErrValidatorNotRegistered) + return errs + } + + errs1 := validator(thing, optional...) + if len(errs1) > 0 { + errs = append(errs, errs1...) + } + + return errs +} diff --git a/validations_test.go b/validations_test.go new file mode 100644 index 0000000..3f8b51a --- /dev/null +++ b/validations_test.go @@ -0,0 +1,266 @@ +package valiwork + +import ( + // stdlib + "errors" + "strconv" + "strings" + "sync" + "testing" + + // local + "go.dev.pztrn.name/valiwork/validators" + + // other + "github.com/stretchr/testify/require" +) + +func TestRegisterValidator(t *testing.T) { + initializeValidatorsStorage() + + testCases := []struct { + ValidatorName string + ValidatorFunc validators.ValidatorFunc + ShouldFail bool + }{ + { + ValidatorName: "string_test_validator", + ValidatorFunc: func(thing interface{}, optional ...interface{}) []interface{} { + return nil + }, + ShouldFail: false, + }, + // This case is about registering same validator function again. + { + ValidatorName: "string_test_validator", + ValidatorFunc: func(thing interface{}, optional ...interface{}) []interface{} { + return nil + }, + ShouldFail: true, + }, + } + + for _, testCase := range testCases { + err := RegisterValidator(testCase.ValidatorName, testCase.ValidatorFunc) + if !testCase.ShouldFail { + require.Nil(t, err) + } else { + require.NotNil(t, err) + } + } +} + +func BenchmarkRegisterValidator(b *testing.B) { + initializeValidatorsStorage() + + for i := 0; i < b.N; i++ { + _ = RegisterValidator("string_test_validator_"+strconv.Itoa(i), + func(thing interface{}, optional ...interface{}) []interface{} { + return nil + }, + ) + } +} + +func BenchmarkRegisterValidatorAsync(b *testing.B) { + initializeValidatorsStorage() + + var w sync.WaitGroup + + for i := 0; i < b.N; i++ { + w.Add(1) + + go func() { + _ = RegisterValidator("string_test_validator_"+strconv.Itoa(i), + func(thing interface{}, optional ...interface{}) []interface{} { + return nil + }, + ) + w.Done() + }() + + w.Wait() + } +} + +func TestValidate(t *testing.T) { + initializeValidatorsStorage() + + testString := " I am test string" + + RegisterValidator("string_test1", func(thing interface{}, optional ...interface{}) []interface{} { + var errs []interface{} + + stringToValidate, ok := thing.(string) + if !ok { + errs = append(errs, errors.New("not a string")) + return errs + } + + if strings.HasPrefix(stringToValidate, " ") { + errs = append(errs, errors.New("string starts with whitespace, invalid!")) + } + + return errs + }) + + errs := Validate("string_test1", testString, nil) + require.NotNil(t, errs) + require.Len(t, errs, 1) +} + +func BenchmarkValidate(b *testing.B) { + b.StopTimer() + + initializeValidatorsStorage() + + testString := " I am test $tring" + + RegisterValidator("string_test1", func(thing interface{}, optional ...interface{}) []interface{} { + var errs []interface{} + + stringToValidate, ok := thing.(string) + if !ok { + errs = append(errs, errors.New("not a string")) + return errs + } + + if strings.HasPrefix(stringToValidate, " ") { + errs = append(errs, errors.New("string starts with whitespace, invalid!")) + } + + if strings.Contains(stringToValidate, "$") { + errs = append(errs, errors.New("string starts with whitespace, invalid!")) + } + + return errs + }) + + b.StartTimer() + + for i := 0; i < b.N; i++ { + _ = Validate("string_test1", testString) + } +} + +func BenchmarkValidateAsync(b *testing.B) { + b.StopTimer() + + initializeValidatorsStorage() + + testString := " I am test $tring" + + RegisterValidator("string_test1", func(thing interface{}, optional ...interface{}) []interface{} { + var errs []interface{} + + stringToValidate, ok := thing.(string) + if !ok { + errs = append(errs, errors.New("not a string")) + return errs + } + + if strings.HasPrefix(stringToValidate, " ") { + errs = append(errs, errors.New("string starts with whitespace, invalid!")) + } + + if strings.Contains(stringToValidate, "$") { + errs = append(errs, errors.New("string starts with whitespace, invalid!")) + } + + return errs + }) + + b.StartTimer() + + var w sync.WaitGroup + + for i := 0; i < b.N; i++ { + w.Add(1) + + go func() { + _ = Validate("string_test1", testString) + + w.Done() + }() + + w.Wait() + } +} + +func TestUnregisterValidator(t *testing.T) { + initializeValidatorsStorage() + + testCases := []struct { + ValidatorName string + ValidatorFunc validators.ValidatorFunc + }{ + { + ValidatorName: "string_test_validator", + ValidatorFunc: func(thing interface{}, optional ...interface{}) []interface{} { + return nil + }, + }, + } + + for _, testCase := range testCases { + err := RegisterValidator(testCase.ValidatorName, testCase.ValidatorFunc) + require.Nil(t, err) + + err1 := UnregisterValidator(testCase.ValidatorName) + require.Nil(t, err1) + } +} + +func TestUnregisterValidatorNotRegisteredValidator(t *testing.T) { + initializeValidatorsStorage() + + err := UnregisterValidator("this is definetely not registered thing") + require.NotNil(t, err) +} + +func BenchmarkUnregisterValidator(b *testing.B) { + b.StopTimer() + + initializeValidatorsStorage() + + for i := 0; i < b.N; i++ { + _ = RegisterValidator("string_test_validator_"+strconv.Itoa(i), + func(thing interface{}, optional ...interface{}) []interface{} { + return nil + }, + ) + } + + b.StartTimer() + for i := 0; i < b.N; i++ { + _ = UnregisterValidator("string_test_validator_" + strconv.Itoa(i)) + } +} + +func BenchmarkUnregisterValidatorAsync(b *testing.B) { + b.StopTimer() + + initializeValidatorsStorage() + + for i := 0; i < b.N; i++ { + _ = RegisterValidator("string_test_validator_"+strconv.Itoa(i), + func(thing interface{}, optional ...interface{}) []interface{} { + return nil + }, + ) + } + + var w sync.WaitGroup + + b.StartTimer() + for i := 0; i < b.N; i++ { + w.Add(1) + + go func() { + _ = UnregisterValidator("string_test_validator_" + strconv.Itoa(i)) + w.Done() + }() + + w.Wait() + } +} diff --git a/validators/validator_signature.go b/validators/validator_signature.go new file mode 100644 index 0000000..66f13e9 --- /dev/null +++ b/validators/validator_signature.go @@ -0,0 +1,4 @@ +package validators + +// ValidatorFunc represents signature for data validation function. +type ValidatorFunc func(thing interface{}, optional ...interface{}) []interface{}