feat(protocols): add basic HTTP checks
This commit is contained in:
parent
212f94b6ea
commit
f9e3c2f4f1
22 changed files with 568 additions and 120 deletions
|
@ -78,7 +78,7 @@ linters:
|
|||
- funlen
|
||||
- gocognit
|
||||
- goconst
|
||||
# - gocritic
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godox
|
||||
- gofumpt
|
||||
|
@ -86,12 +86,12 @@ linters:
|
|||
- gomoddirectives
|
||||
- gomnd
|
||||
- gosec
|
||||
- gosimple
|
||||
# - gosimple
|
||||
- govet
|
||||
- ifshort
|
||||
- importas
|
||||
- ineffassign
|
||||
# - ireturn - enable later
|
||||
- ireturn
|
||||
- lll
|
||||
- misspell
|
||||
- nakedret
|
||||
|
@ -99,13 +99,14 @@ linters:
|
|||
- nilnil
|
||||
- noctx
|
||||
- nolintlint
|
||||
- nosprintfhostport
|
||||
- paralleltest
|
||||
- prealloc
|
||||
- predeclared
|
||||
- promlinter
|
||||
- staticcheck
|
||||
# - staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
# - stylecheck
|
||||
- tenv
|
||||
- testpackage
|
||||
- thelper
|
||||
|
|
31
build.cue
Normal file
31
build.cue
Normal file
|
@ -0,0 +1,31 @@
|
|||
package nurse
|
||||
|
||||
import (
|
||||
"dagger.io/dagger"
|
||||
"universe.dagger.io/go"
|
||||
)
|
||||
|
||||
dagger.#Plan & {
|
||||
client: {
|
||||
filesystem: {
|
||||
"./": read: {
|
||||
contents: dagger.#FS
|
||||
exclude: [
|
||||
"README.md",
|
||||
"build.cue",
|
||||
]
|
||||
}
|
||||
}
|
||||
network: "unix:///var/run/docker.sock": connect: dagger.#Socket // Docker daemon socket
|
||||
}
|
||||
actions: {
|
||||
test: go.#Test & {
|
||||
package: "./..."
|
||||
source: client.filesystem."./".read.contents
|
||||
mounts: docker: {
|
||||
dest: "/var/run/docker.sock"
|
||||
contents: client.network."unix:///var/run/docker.sock".connect
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import (
|
|||
var (
|
||||
ErrNoSuchCheck = errors.New("no such check")
|
||||
ErrConflictingCheck = errors.New("check with same name already registered")
|
||||
ErrNoSuchValidator = errors.New("no such validator")
|
||||
)
|
||||
|
||||
type (
|
||||
|
|
1
go.mod
1
go.mod
|
@ -44,6 +44,7 @@ require (
|
|||
github.com/opencontainers/runc v1.1.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -707,6 +707,8 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/
|
|||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
|
||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||
|
|
2
main.go
2
main.go
|
@ -43,7 +43,7 @@ func main() {
|
|||
logger.Debug("Loaded config", zap.Any("config", nurseInstance))
|
||||
|
||||
chkRegistry := check.NewRegistry()
|
||||
if err := chkRegistry.Register(redis.Module()); err != nil {
|
||||
if err = chkRegistry.Register(redis.Module()); err != nil {
|
||||
logger.Fatal("Failed to register Redis module", zap.Error(err))
|
||||
}
|
||||
|
||||
|
|
27
protocols/http/checks.go
Normal file
27
protocols/http/checks.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/baez90/nurse/check"
|
||||
)
|
||||
|
||||
func Module() *check.Module {
|
||||
m, _ := check.NewModule(
|
||||
"http",
|
||||
check.WithCheck("get", check.FactoryFunc(func() check.SystemChecker {
|
||||
return &GenericCheck{Method: http.MethodGet}
|
||||
})),
|
||||
check.WithCheck("post", check.FactoryFunc(func() check.SystemChecker {
|
||||
return &GenericCheck{Method: http.MethodPost}
|
||||
})),
|
||||
check.WithCheck("put", check.FactoryFunc(func() check.SystemChecker {
|
||||
return &GenericCheck{Method: http.MethodPut}
|
||||
})),
|
||||
check.WithCheck("delete", check.FactoryFunc(func() check.SystemChecker {
|
||||
return &GenericCheck{Method: http.MethodDelete}
|
||||
})),
|
||||
)
|
||||
|
||||
return m
|
||||
}
|
144
protocols/http/checks_test.go
Normal file
144
protocols/http/checks_test.go
Normal file
|
@ -0,0 +1,144 @@
|
|||
package http_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/maxatome/go-testdeep/td"
|
||||
|
||||
"github.com/baez90/nurse/grammar"
|
||||
httpcheck "github.com/baez90/nurse/protocols/http"
|
||||
)
|
||||
|
||||
func TestChecks_Execute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
httpModule := httpcheck.Module()
|
||||
|
||||
type serverResponse struct {
|
||||
status int
|
||||
body io.Reader
|
||||
err error
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
check string
|
||||
resp serverResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "GET check without validation",
|
||||
check: `http.GET("%s/api/books")`,
|
||||
resp: serverResponse{
|
||||
status: 200,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "GET check - status validation",
|
||||
check: `http.GET("%s/api/books") => Status(200)`,
|
||||
resp: serverResponse{
|
||||
status: 200,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "GET check - JSON path validation",
|
||||
check: `http.GET("%s/api/books") => JSONPath("$.firstName", "Homer")`,
|
||||
resp: serverResponse{
|
||||
status: 200,
|
||||
body: strings.NewReader(`{"firstName": "Homer"}`),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "GET check - Status and JSON path validation",
|
||||
check: `http.GET("%s/api/books") => Status(200) -> JSONPath("$.firstName", "Homer")`,
|
||||
resp: serverResponse{
|
||||
status: 200,
|
||||
body: strings.NewReader(`{"firstName": "Homer"}`),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "POST check without validation",
|
||||
check: `http.POST("%s/api/books")`,
|
||||
resp: serverResponse{
|
||||
status: 204,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "POST check - Status validation",
|
||||
check: `http.POST("%s/api/books") => Status(204)`,
|
||||
resp: serverResponse{
|
||||
status: 204,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "PUT check without validation",
|
||||
check: `http.PUT("%s/api/books/1")`,
|
||||
resp: serverResponse{
|
||||
status: 200,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "DELETE check without validation",
|
||||
check: `http.DELETE("%s/api/books/1")`,
|
||||
resp: serverResponse{
|
||||
status: 200,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
if tt.resp.err != nil {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = writer.Write([]byte(tt.resp.err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
writer.WriteHeader(tt.resp.status)
|
||||
if tt.resp.body != nil {
|
||||
_, _ = io.Copy(writer, tt.resp.body)
|
||||
}
|
||||
}))
|
||||
|
||||
t.Cleanup(testServer.Close)
|
||||
|
||||
parser, err := grammar.NewParser[grammar.Check]()
|
||||
td.CmpNoError(t, err, "grammar.NewParser()")
|
||||
parsedCheck, err := parser.Parse(fmt.Sprintf(tt.check, testServer.URL))
|
||||
td.CmpNoError(t, err, "parser.Parse()")
|
||||
|
||||
chk, err := httpModule.Lookup(*parsedCheck, nil)
|
||||
td.CmpNoError(t, err, "http.LookupCheck()")
|
||||
|
||||
if clientInjectable, ok := chk.(httpcheck.ClientInjectable); !ok {
|
||||
t.Fatal("Failed to inject client to check")
|
||||
} else {
|
||||
clientInjectable.SetClient(testServer.Client())
|
||||
}
|
||||
|
||||
if tt.wantErr {
|
||||
td.CmpError(t, chk.Execute(context.Background()))
|
||||
} else {
|
||||
td.CmpNoError(t, chk.Execute(context.Background()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
85
protocols/http/get.go
Normal file
85
protocols/http/get.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/baez90/nurse/check"
|
||||
"github.com/baez90/nurse/config"
|
||||
"github.com/baez90/nurse/grammar"
|
||||
"github.com/baez90/nurse/validation"
|
||||
)
|
||||
|
||||
type ClientInjectable interface {
|
||||
SetClient(client *http.Client)
|
||||
}
|
||||
|
||||
var (
|
||||
_ check.SystemChecker = (*GenericCheck)(nil)
|
||||
_ ClientInjectable = (*GenericCheck)(nil)
|
||||
)
|
||||
|
||||
type GenericCheck struct {
|
||||
*http.Client
|
||||
validators validation.Validator[*http.Response]
|
||||
Method string
|
||||
Body []byte
|
||||
URL string
|
||||
}
|
||||
|
||||
func (g *GenericCheck) SetClient(client *http.Client) {
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
|
||||
g.Client = client
|
||||
}
|
||||
|
||||
func (g *GenericCheck) Execute(ctx context.Context) error {
|
||||
var body io.Reader
|
||||
if len(g.Body) > 0 {
|
||||
body = bytes.NewReader(g.Body)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, g.Method, g.URL, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := g.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
return g.validators.Validate(resp)
|
||||
}
|
||||
|
||||
func (g *GenericCheck) UnmarshalCheck(c grammar.Check, _ config.ServerLookup) error {
|
||||
const urlArgsNumber = 1
|
||||
|
||||
inst := c.Initiator
|
||||
|
||||
if err := grammar.ValidateParameterCount(inst.Params, urlArgsNumber); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if g.Client == nil {
|
||||
g.Client = http.DefaultClient
|
||||
}
|
||||
|
||||
var err error
|
||||
if g.URL, err = inst.Params[0].AsString(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if g.validators, err = registry.ValidatorsForFilters(c.Validators); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
57
protocols/http/json_validation.go
Normal file
57
protocols/http/json_validation.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/valyala/bytebufferpool"
|
||||
|
||||
"github.com/baez90/nurse/grammar"
|
||||
"github.com/baez90/nurse/validation"
|
||||
)
|
||||
|
||||
var _ validation.FromCall[*http.Response] = (*JSONPathValidator)(nil)
|
||||
|
||||
type JSONPathValidator struct {
|
||||
validator *validation.JSONPathValidator
|
||||
}
|
||||
|
||||
func (j *JSONPathValidator) UnmarshalCall(c grammar.Call) (err error) {
|
||||
if err = grammar.ValidateParameterCount(c.Params, 2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var jsonPath string
|
||||
|
||||
if jsonPath, err = c.Params[0].AsString(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch c.Params[1].Type() {
|
||||
case grammar.ParamTypeInt:
|
||||
j.validator, err = validation.JSONPathValidatorFor(jsonPath, *c.Params[1].Int)
|
||||
case grammar.ParamTypeFloat:
|
||||
j.validator, err = validation.JSONPathValidatorFor(jsonPath, *c.Params[1].Float)
|
||||
case grammar.ParamTypeString:
|
||||
j.validator, err = validation.JSONPathValidatorFor(jsonPath, *c.Params[1].String)
|
||||
case grammar.ParamTypeUnknown:
|
||||
fallthrough
|
||||
default:
|
||||
return errors.New("param type unknown")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *JSONPathValidator) Validate(resp *http.Response) error {
|
||||
buf := bytebufferpool.Get()
|
||||
defer bytebufferpool.Put(buf)
|
||||
|
||||
readBytes, err := io.Copy(buf, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return j.validator.Equals(buf.B[:readBytes])
|
||||
}
|
34
protocols/http/status_validation.go
Normal file
34
protocols/http/status_validation.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/baez90/nurse/grammar"
|
||||
"github.com/baez90/nurse/validation"
|
||||
)
|
||||
|
||||
var _ validation.FromCall[*http.Response] = (*StatusValidator)(nil)
|
||||
|
||||
type StatusValidator struct {
|
||||
Want int
|
||||
}
|
||||
|
||||
func (s *StatusValidator) Validate(resp *http.Response) error {
|
||||
if resp.StatusCode != s.Want {
|
||||
return fmt.Errorf("want HTTP status %d but got %d", s.Want, resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StatusValidator) UnmarshalCall(c grammar.Call) error {
|
||||
if err := grammar.ValidateParameterCount(c.Params, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
s.Want, err = c.Params[0].AsInt()
|
||||
|
||||
return err
|
||||
}
|
19
protocols/http/validation.go
Normal file
19
protocols/http/validation.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/baez90/nurse/validation"
|
||||
)
|
||||
|
||||
var registry = validation.NewRegistry[*http.Response]()
|
||||
|
||||
func init() {
|
||||
registry.Register("jsonpath", func() validation.FromCall[*http.Response] {
|
||||
return new(JSONPathValidator)
|
||||
})
|
||||
|
||||
registry.Register("status", func() validation.FromCall[*http.Response] {
|
||||
return new(StatusValidator)
|
||||
})
|
||||
}
|
|
@ -90,9 +90,6 @@ func TestChecks_Execute(t *testing.T) {
|
|||
chk, err := redisModule.Lookup(*parsedCheck, register)
|
||||
td.CmpNoError(t, err, "redis.LookupCheck()")
|
||||
|
||||
td.CmpNoError(t, chk.UnmarshalCheck(*parsedCheck, register), "get.UnmarshalCheck()")
|
||||
td.CmpNoError(t, chk.Execute(context.Background()))
|
||||
|
||||
if tt.wantErr {
|
||||
td.CmpError(t, chk.Execute(context.Background()))
|
||||
} else {
|
||||
|
|
|
@ -8,13 +8,14 @@ import (
|
|||
"github.com/baez90/nurse/check"
|
||||
"github.com/baez90/nurse/config"
|
||||
"github.com/baez90/nurse/grammar"
|
||||
"github.com/baez90/nurse/validation"
|
||||
)
|
||||
|
||||
var _ check.SystemChecker = (*GetCheck)(nil)
|
||||
|
||||
type GetCheck struct {
|
||||
redis.UniversalClient
|
||||
validators ValidationChain
|
||||
validators validation.Validator[redis.Cmder]
|
||||
Key string
|
||||
}
|
||||
|
||||
|
@ -44,7 +45,7 @@ func (g *GetCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup) e
|
|||
return err
|
||||
}
|
||||
|
||||
if g.validators, err = ValidatorsForFilters(c.Validators); err != nil {
|
||||
if g.validators, err = registry.ValidatorsForFilters(c.Validators); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -9,13 +9,14 @@ import (
|
|||
"github.com/baez90/nurse/check"
|
||||
"github.com/baez90/nurse/config"
|
||||
"github.com/baez90/nurse/grammar"
|
||||
"github.com/baez90/nurse/validation"
|
||||
)
|
||||
|
||||
var _ check.SystemChecker = (*PingCheck)(nil)
|
||||
|
||||
type PingCheck struct {
|
||||
redis.UniversalClient
|
||||
validators ValidationChain
|
||||
validators validation.Validator[redis.Cmder]
|
||||
Message string
|
||||
}
|
||||
|
||||
|
@ -39,7 +40,11 @@ func (p *PingCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup)
|
|||
)
|
||||
|
||||
val, _ := GenericCommandValidatorFor("PONG")
|
||||
p.validators = append(p.validators, val)
|
||||
|
||||
validators := validation.Chain[redis.Cmder]{}
|
||||
validators = append(validators, val)
|
||||
|
||||
p.validators = validators
|
||||
|
||||
init := c.Initiator
|
||||
switch len(init.Params) {
|
||||
|
@ -50,7 +55,7 @@ func (p *PingCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup)
|
|||
return err
|
||||
} else {
|
||||
val, _ := GenericCommandValidatorFor(msg)
|
||||
p.validators = ValidationChain{val}
|
||||
p.validators = validation.Chain[redis.Cmder]{val}
|
||||
}
|
||||
fallthrough
|
||||
case serverOnlyArgCount:
|
||||
|
|
|
@ -2,74 +2,27 @@ package redis
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
||||
"github.com/baez90/nurse/check"
|
||||
"github.com/baez90/nurse/grammar"
|
||||
"github.com/baez90/nurse/validation"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoSuchValidator = errors.New("no such validator")
|
||||
|
||||
_ CmdValidator = (ValidationChain)(nil)
|
||||
_ CmdValidator = (*GenericCmdValidator)(nil)
|
||||
|
||||
knownValidators = map[string]func() unmarshallableCmdValidator{
|
||||
"equals": func() unmarshallableCmdValidator {
|
||||
registry = validation.NewRegistry[redis.Cmder]()
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("equals", func() validation.FromCall[redis.Cmder] {
|
||||
return new(GenericCmdValidator)
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
type (
|
||||
CmdValidator interface {
|
||||
type CmdValidator interface {
|
||||
Validate(cmder redis.Cmder) error
|
||||
}
|
||||
unmarshallableCmdValidator interface {
|
||||
CmdValidator
|
||||
check.CallUnmarshaler
|
||||
}
|
||||
)
|
||||
|
||||
func ValidatorsForFilters(filters *grammar.Filters) (ValidationChain, error) {
|
||||
if filters == nil || filters.Chain == nil {
|
||||
return ValidationChain{}, nil
|
||||
}
|
||||
chain := make(ValidationChain, 0, len(filters.Chain))
|
||||
for i := range filters.Chain {
|
||||
validationCall := filters.Chain[i]
|
||||
if validatorProvider, ok := knownValidators[strings.ToLower(validationCall.Name)]; !ok {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNoSuchValidator, validationCall.Name)
|
||||
} else {
|
||||
validator := validatorProvider()
|
||||
if err := validator.UnmarshalCall(validationCall); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chain = append(chain, validator)
|
||||
}
|
||||
}
|
||||
|
||||
return chain, nil
|
||||
}
|
||||
|
||||
type ValidationChain []CmdValidator
|
||||
|
||||
func (v ValidationChain) UnmarshalCall(grammar.Call) error {
|
||||
return errors.New("cannot unmarshal chain")
|
||||
}
|
||||
|
||||
func (v ValidationChain) Validate(cmder redis.Cmder) error {
|
||||
for i := range v {
|
||||
if err := v[i].Validate(cmder); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GenericCommandValidatorFor[T validation.Value](want T) (*GenericCmdValidator, error) {
|
||||
|
@ -92,7 +45,6 @@ func (g *GenericCmdValidator) UnmarshalCall(c grammar.Call) error {
|
|||
}
|
||||
|
||||
var err error
|
||||
|
||||
switch c.Params[0].Type() {
|
||||
case grammar.ParamTypeInt:
|
||||
if g.comparator, err = validation.JSONValueComparatorFor(*c.Params[0].Int); err != nil {
|
||||
|
@ -107,13 +59,13 @@ func (g *GenericCmdValidator) UnmarshalCall(c grammar.Call) error {
|
|||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("param type is unkown")
|
||||
return errors.New("param type is unknown")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g GenericCmdValidator) Validate(cmder redis.Cmder) error {
|
||||
func (g *GenericCmdValidator) Validate(cmder redis.Cmder) error {
|
||||
if err := cmder.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -124,9 +76,8 @@ func (g GenericCmdValidator) Validate(cmder redis.Cmder) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !g.comparator.Equals(res) {
|
||||
return fmt.Errorf("got %s - but didn't match expected value", res)
|
||||
}
|
||||
|
||||
return g.comparator.Equals(res)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package validation
|
||||
|
||||
import "math"
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
const equalityThreshold = 0.00000001
|
||||
|
||||
|
@ -10,7 +13,7 @@ var (
|
|||
)
|
||||
|
||||
type ValueComparator interface {
|
||||
Equals(got any) bool
|
||||
Equals(got any) error
|
||||
}
|
||||
|
||||
type GenericComparator[T int | string] struct {
|
||||
|
@ -18,22 +21,30 @@ type GenericComparator[T int | string] struct {
|
|||
Parser func(got any) (T, error)
|
||||
}
|
||||
|
||||
func (g GenericComparator[T]) Equals(got any) bool {
|
||||
func (g GenericComparator[T]) Equals(got any) error {
|
||||
parsed, err := g.Parser(got)
|
||||
if err != nil {
|
||||
return false
|
||||
return err
|
||||
}
|
||||
|
||||
return parsed == g.Want
|
||||
if parsed != g.Want {
|
||||
return fmt.Errorf("want %v but got %v", g.Want, parsed)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type FloatComparator float64
|
||||
|
||||
func (f FloatComparator) Equals(got any) bool {
|
||||
func (f FloatComparator) Equals(got any) error {
|
||||
val, err := ParseJSONFloat(got)
|
||||
if err != nil {
|
||||
return false
|
||||
return err
|
||||
}
|
||||
|
||||
return math.Abs(float64(f)-val) < equalityThreshold
|
||||
if math.Abs(float64(f)-val) > equalityThreshold {
|
||||
return fmt.Errorf("want %f but got %f", float64(f), val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -26,14 +26,14 @@ type JSONPathValidator struct {
|
|||
Comparator ValueComparator
|
||||
}
|
||||
|
||||
func (j JSONPathValidator) Equals(got any) bool {
|
||||
func (j JSONPathValidator) Equals(got any) error {
|
||||
parsed, err := parse(got)
|
||||
if err != nil {
|
||||
return false
|
||||
return err
|
||||
}
|
||||
val, err := jsonpath.Get(j.Path, parsed)
|
||||
if err != nil {
|
||||
return false
|
||||
return err
|
||||
}
|
||||
|
||||
return j.Comparator.Equals(val)
|
||||
|
|
|
@ -11,13 +11,14 @@ type jsonPathValidator_EqualsTestCase[V validation.Value] struct {
|
|||
expected V
|
||||
jsonPath string
|
||||
json string
|
||||
want bool
|
||||
wantErr bool
|
||||
}
|
||||
|
||||
func (tt jsonPathValidator_EqualsTestCase[V]) name() string {
|
||||
return tt.testName
|
||||
}
|
||||
|
||||
//nolint:thelper // is not a helper
|
||||
func (tt jsonPathValidator_EqualsTestCase[V]) run(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Helper()
|
||||
|
@ -26,8 +27,10 @@ func (tt jsonPathValidator_EqualsTestCase[V]) run(t *testing.T) {
|
|||
t.Fatalf("JSONPathValidatorFor() err = %v", err)
|
||||
}
|
||||
|
||||
if validator.Equals(tt.json) != tt.want {
|
||||
t.Errorf("Failed to equal value in %s to %v", tt.json, tt.expected)
|
||||
if err := validator.Equals(tt.json); err != nil {
|
||||
if !tt.wantErr {
|
||||
t.Errorf("Failed to equal value in %s to %v: %v", tt.json, tt.expected, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,42 +42,42 @@ func TestJSONPathValidator_Equals(t *testing.T) {
|
|||
expected: "hello",
|
||||
jsonPath: "$.greeting",
|
||||
json: `{"greeting": "hello"}`,
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonPathValidator_EqualsTestCase[string]{
|
||||
testName: "Simple object navigation - number as string",
|
||||
expected: "42",
|
||||
jsonPath: "$.number",
|
||||
json: `{"number": 42}`,
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonPathValidator_EqualsTestCase[string]{
|
||||
testName: "Simple array navigation",
|
||||
expected: "world",
|
||||
jsonPath: "$[1]",
|
||||
json: `["hello", "world"]`,
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonPathValidator_EqualsTestCase[int]{
|
||||
testName: "Simple array navigation - string to int",
|
||||
expected: 37,
|
||||
jsonPath: "$[1]",
|
||||
json: `["13", "37"]`,
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonPathValidator_EqualsTestCase[int]{
|
||||
testName: "Simple array navigation - string to int - wrong value",
|
||||
expected: 42,
|
||||
jsonPath: "$[1]",
|
||||
json: `["13", "37"]`,
|
||||
want: false,
|
||||
wantErr: true,
|
||||
},
|
||||
jsonPathValidator_EqualsTestCase[string]{
|
||||
testName: "Simple array navigation - int to string",
|
||||
expected: "37",
|
||||
jsonPath: "$[1]",
|
||||
json: `[13, 37]`,
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
//nolint:paralleltest
|
||||
|
|
|
@ -47,6 +47,6 @@ type JSONValueComparator struct {
|
|||
Comparator ValueComparator
|
||||
}
|
||||
|
||||
func (j JSONValueComparator) Equals(got any) bool {
|
||||
func (j JSONValueComparator) Equals(got any) error {
|
||||
return j.Comparator.Equals(got)
|
||||
}
|
||||
|
|
|
@ -15,9 +15,10 @@ type jsonValueComparator_EqualsTestCase[V validation.Value] struct {
|
|||
testName string
|
||||
expected V
|
||||
got any
|
||||
want bool
|
||||
wantErr bool
|
||||
}
|
||||
|
||||
//nolint:thelper // is not a helper
|
||||
func (tt jsonValueComparator_EqualsTestCase[V]) run(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Helper()
|
||||
|
@ -26,8 +27,10 @@ func (tt jsonValueComparator_EqualsTestCase[V]) run(t *testing.T) {
|
|||
t.Fatalf("validation.JSONValueComparatorFor() err = %v", err)
|
||||
}
|
||||
|
||||
if got := comparator.Equals(tt.got); got != tt.want {
|
||||
t.Errorf("Equals() = %v, want %v", got, tt.want)
|
||||
if err := comparator.Equals(tt.got); err != nil {
|
||||
if !tt.wantErr {
|
||||
t.Errorf("Equals() = %v, want %v", err, tt.wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,133 +45,133 @@ func TestJSONValueComparator_Equals(t *testing.T) {
|
|||
testName: "Test int equality",
|
||||
expected: 42,
|
||||
got: 42,
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[int]{
|
||||
testName: "Test int equality - wrong value",
|
||||
expected: 42,
|
||||
got: 43,
|
||||
want: false,
|
||||
wantErr: true,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[int]{
|
||||
testName: "Test int equality - string value",
|
||||
expected: 42,
|
||||
got: "42",
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[int]{
|
||||
testName: "Test int equality - []byte value",
|
||||
expected: 42,
|
||||
got: []byte("42"),
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[int]{
|
||||
testName: "Test int equality - float value",
|
||||
expected: 42,
|
||||
got: 42.0,
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[int8]{
|
||||
testName: "Test int8 equality",
|
||||
expected: 42,
|
||||
got: 42,
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[int8]{
|
||||
testName: "Test int8 equality - wrong value",
|
||||
expected: 42,
|
||||
got: 43,
|
||||
want: false,
|
||||
wantErr: true,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[int8]{
|
||||
testName: "Test int8 equality - int16 value",
|
||||
expected: 42,
|
||||
got: int16(42),
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[int8]{
|
||||
testName: "Test int8 equality - uint16 value",
|
||||
expected: 42,
|
||||
got: uint16(42),
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[float32]{
|
||||
testName: "Test float32 equality - float value",
|
||||
expected: 42.0,
|
||||
got: 42.0,
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[float32]{
|
||||
testName: "Test float32 equality - float value",
|
||||
expected: 42.0,
|
||||
got: float64(42.0),
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[float64]{
|
||||
testName: "Test float64 equality - float value",
|
||||
expected: 42.0,
|
||||
got: 42.0,
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[float64]{
|
||||
testName: "Test float64 equality - int value",
|
||||
expected: 42.0,
|
||||
got: 42,
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[float64]{
|
||||
testName: "Test float64 equality - []byte value",
|
||||
expected: 42.0,
|
||||
got: []byte("42"),
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[float64]{
|
||||
testName: "Test float64 equality - float32 value",
|
||||
expected: 42.0,
|
||||
got: float32(42.0),
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[float64]{
|
||||
testName: "Test float64 equality - string value",
|
||||
expected: 42.0,
|
||||
got: "42.0",
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[float64]{
|
||||
testName: "Test float64 equality - string value without dot",
|
||||
expected: 42.0,
|
||||
got: "42",
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[string]{
|
||||
testName: "Test string equality",
|
||||
expected: "hello",
|
||||
got: "hello",
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[string]{
|
||||
testName: "Test string equality - []byte value",
|
||||
expected: "hello",
|
||||
got: []byte("hello"),
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[string]{
|
||||
testName: "Test string equality - int value",
|
||||
expected: "1337",
|
||||
got: 1337,
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[string]{
|
||||
testName: "Test string equality - float value",
|
||||
expected: "13.37",
|
||||
got: 13.37,
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
jsonValueComparator_EqualsTestCase[string]{
|
||||
testName: "Test string equality - wrong case",
|
||||
expected: "hello",
|
||||
got: "HELLO",
|
||||
want: false,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
75
validation/registry.go
Normal file
75
validation/registry.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/baez90/nurse/check"
|
||||
"github.com/baez90/nurse/grammar"
|
||||
)
|
||||
|
||||
type (
|
||||
Validator[T any] interface {
|
||||
Validate(in T) error
|
||||
}
|
||||
|
||||
FromCall[T any] interface {
|
||||
Validator[T]
|
||||
check.CallUnmarshaler
|
||||
}
|
||||
|
||||
Chain[T any] []Validator[T]
|
||||
)
|
||||
|
||||
func (c Chain[T]) Validate(in T) error {
|
||||
for i := range c {
|
||||
if err := c[i].Validate(in); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewRegistry[R any]() *Registry[R] {
|
||||
return &Registry[R]{
|
||||
validators: make(map[string]func() FromCall[R]),
|
||||
}
|
||||
}
|
||||
|
||||
type Registry[R any] struct {
|
||||
lock sync.Mutex
|
||||
validators map[string]func() FromCall[R]
|
||||
}
|
||||
|
||||
func (r *Registry[R]) Register(name string, factory func() FromCall[R]) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
r.validators[strings.ToLower(name)] = factory
|
||||
}
|
||||
|
||||
func (r *Registry[R]) ValidatorsForFilters(filters *grammar.Filters) (Chain[R], error) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
if filters == nil || filters.Chain == nil {
|
||||
return Chain[R]{}, nil
|
||||
}
|
||||
chain := make(Chain[R], 0, len(filters.Chain))
|
||||
for i := range filters.Chain {
|
||||
validationCall := filters.Chain[i]
|
||||
if validatorProvider, ok := r.validators[strings.ToLower(validationCall.Name)]; !ok {
|
||||
return nil, fmt.Errorf("%w: %s", check.ErrNoSuchValidator, validationCall.Name)
|
||||
} else {
|
||||
validator := validatorProvider()
|
||||
if err := validator.UnmarshalCall(validationCall); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chain = append(chain, validator)
|
||||
}
|
||||
}
|
||||
|
||||
return chain, nil
|
||||
}
|
Loading…
Reference in a new issue