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
|
- funlen
|
||||||
- gocognit
|
- gocognit
|
||||||
- goconst
|
- goconst
|
||||||
# - gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- godox
|
- godox
|
||||||
- gofumpt
|
- gofumpt
|
||||||
|
@ -86,12 +86,12 @@ linters:
|
||||||
- gomoddirectives
|
- gomoddirectives
|
||||||
- gomnd
|
- gomnd
|
||||||
- gosec
|
- gosec
|
||||||
- gosimple
|
# - gosimple
|
||||||
- govet
|
- govet
|
||||||
- ifshort
|
- ifshort
|
||||||
- importas
|
- importas
|
||||||
- ineffassign
|
- ineffassign
|
||||||
# - ireturn - enable later
|
- ireturn
|
||||||
- lll
|
- lll
|
||||||
- misspell
|
- misspell
|
||||||
- nakedret
|
- nakedret
|
||||||
|
@ -99,13 +99,14 @@ linters:
|
||||||
- nilnil
|
- nilnil
|
||||||
- noctx
|
- noctx
|
||||||
- nolintlint
|
- nolintlint
|
||||||
|
- nosprintfhostport
|
||||||
- paralleltest
|
- paralleltest
|
||||||
- prealloc
|
- prealloc
|
||||||
- predeclared
|
- predeclared
|
||||||
- promlinter
|
- promlinter
|
||||||
- staticcheck
|
# - staticcheck
|
||||||
- structcheck
|
- structcheck
|
||||||
- stylecheck
|
# - stylecheck
|
||||||
- tenv
|
- tenv
|
||||||
- testpackage
|
- testpackage
|
||||||
- thelper
|
- 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 (
|
var (
|
||||||
ErrNoSuchCheck = errors.New("no such check")
|
ErrNoSuchCheck = errors.New("no such check")
|
||||||
ErrConflictingCheck = errors.New("check with same name already registered")
|
ErrConflictingCheck = errors.New("check with same name already registered")
|
||||||
|
ErrNoSuchValidator = errors.New("no such validator")
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -44,6 +44,7 @@ require (
|
||||||
github.com/opencontainers/runc v1.1.2 // indirect
|
github.com/opencontainers/runc v1.1.2 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/sirupsen/logrus v1.8.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.opencensus.io v0.23.0 // indirect
|
||||||
go.uber.org/atomic v1.7.0 // indirect
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
go.uber.org/multierr v1.6.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.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.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
github.com/urfave/cli v1.22.2/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 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.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||||
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
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))
|
logger.Debug("Loaded config", zap.Any("config", nurseInstance))
|
||||||
|
|
||||||
chkRegistry := check.NewRegistry()
|
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))
|
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)
|
chk, err := redisModule.Lookup(*parsedCheck, register)
|
||||||
td.CmpNoError(t, err, "redis.LookupCheck()")
|
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 {
|
if tt.wantErr {
|
||||||
td.CmpError(t, chk.Execute(context.Background()))
|
td.CmpError(t, chk.Execute(context.Background()))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -8,13 +8,14 @@ import (
|
||||||
"github.com/baez90/nurse/check"
|
"github.com/baez90/nurse/check"
|
||||||
"github.com/baez90/nurse/config"
|
"github.com/baez90/nurse/config"
|
||||||
"github.com/baez90/nurse/grammar"
|
"github.com/baez90/nurse/grammar"
|
||||||
|
"github.com/baez90/nurse/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ check.SystemChecker = (*GetCheck)(nil)
|
var _ check.SystemChecker = (*GetCheck)(nil)
|
||||||
|
|
||||||
type GetCheck struct {
|
type GetCheck struct {
|
||||||
redis.UniversalClient
|
redis.UniversalClient
|
||||||
validators ValidationChain
|
validators validation.Validator[redis.Cmder]
|
||||||
Key string
|
Key string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +45,7 @@ func (g *GetCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup) e
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if g.validators, err = ValidatorsForFilters(c.Validators); err != nil {
|
if g.validators, err = registry.ValidatorsForFilters(c.Validators); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,13 +9,14 @@ import (
|
||||||
"github.com/baez90/nurse/check"
|
"github.com/baez90/nurse/check"
|
||||||
"github.com/baez90/nurse/config"
|
"github.com/baez90/nurse/config"
|
||||||
"github.com/baez90/nurse/grammar"
|
"github.com/baez90/nurse/grammar"
|
||||||
|
"github.com/baez90/nurse/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ check.SystemChecker = (*PingCheck)(nil)
|
var _ check.SystemChecker = (*PingCheck)(nil)
|
||||||
|
|
||||||
type PingCheck struct {
|
type PingCheck struct {
|
||||||
redis.UniversalClient
|
redis.UniversalClient
|
||||||
validators ValidationChain
|
validators validation.Validator[redis.Cmder]
|
||||||
Message string
|
Message string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +40,11 @@ func (p *PingCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup)
|
||||||
)
|
)
|
||||||
|
|
||||||
val, _ := GenericCommandValidatorFor("PONG")
|
val, _ := GenericCommandValidatorFor("PONG")
|
||||||
p.validators = append(p.validators, val)
|
|
||||||
|
validators := validation.Chain[redis.Cmder]{}
|
||||||
|
validators = append(validators, val)
|
||||||
|
|
||||||
|
p.validators = validators
|
||||||
|
|
||||||
init := c.Initiator
|
init := c.Initiator
|
||||||
switch len(init.Params) {
|
switch len(init.Params) {
|
||||||
|
@ -50,7 +55,7 @@ func (p *PingCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup)
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
val, _ := GenericCommandValidatorFor(msg)
|
val, _ := GenericCommandValidatorFor(msg)
|
||||||
p.validators = ValidationChain{val}
|
p.validators = validation.Chain[redis.Cmder]{val}
|
||||||
}
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
case serverOnlyArgCount:
|
case serverOnlyArgCount:
|
||||||
|
|
|
@ -2,74 +2,27 @@ package redis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
|
|
||||||
"github.com/baez90/nurse/check"
|
|
||||||
"github.com/baez90/nurse/grammar"
|
"github.com/baez90/nurse/grammar"
|
||||||
"github.com/baez90/nurse/validation"
|
"github.com/baez90/nurse/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNoSuchValidator = errors.New("no such validator")
|
|
||||||
|
|
||||||
_ CmdValidator = (ValidationChain)(nil)
|
|
||||||
_ CmdValidator = (*GenericCmdValidator)(nil)
|
_ CmdValidator = (*GenericCmdValidator)(nil)
|
||||||
|
|
||||||
knownValidators = map[string]func() unmarshallableCmdValidator{
|
registry = validation.NewRegistry[redis.Cmder]()
|
||||||
"equals": func() unmarshallableCmdValidator {
|
|
||||||
return new(GenericCmdValidator)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
func init() {
|
||||||
CmdValidator interface {
|
registry.Register("equals", func() validation.FromCall[redis.Cmder] {
|
||||||
Validate(cmder redis.Cmder) error
|
return new(GenericCmdValidator)
|
||||||
}
|
})
|
||||||
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
|
type CmdValidator interface {
|
||||||
|
Validate(cmder redis.Cmder) error
|
||||||
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) {
|
func GenericCommandValidatorFor[T validation.Value](want T) (*GenericCmdValidator, error) {
|
||||||
|
@ -92,7 +45,6 @@ func (g *GenericCmdValidator) UnmarshalCall(c grammar.Call) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
switch c.Params[0].Type() {
|
switch c.Params[0].Type() {
|
||||||
case grammar.ParamTypeInt:
|
case grammar.ParamTypeInt:
|
||||||
if g.comparator, err = validation.JSONValueComparatorFor(*c.Params[0].Int); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("param type is unkown")
|
return errors.New("param type is unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g GenericCmdValidator) Validate(cmder redis.Cmder) error {
|
func (g *GenericCmdValidator) Validate(cmder redis.Cmder) error {
|
||||||
if err := cmder.Err(); err != nil {
|
if err := cmder.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -124,9 +76,8 @@ func (g GenericCmdValidator) Validate(cmder redis.Cmder) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package validation
|
package validation
|
||||||
|
|
||||||
import "math"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
const equalityThreshold = 0.00000001
|
const equalityThreshold = 0.00000001
|
||||||
|
|
||||||
|
@ -10,7 +13,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ValueComparator interface {
|
type ValueComparator interface {
|
||||||
Equals(got any) bool
|
Equals(got any) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type GenericComparator[T int | string] struct {
|
type GenericComparator[T int | string] struct {
|
||||||
|
@ -18,22 +21,30 @@ type GenericComparator[T int | string] struct {
|
||||||
Parser func(got any) (T, error)
|
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)
|
parsed, err := g.Parser(got)
|
||||||
if err != nil {
|
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
|
type FloatComparator float64
|
||||||
|
|
||||||
func (f FloatComparator) Equals(got any) bool {
|
func (f FloatComparator) Equals(got any) error {
|
||||||
val, err := ParseJSONFloat(got)
|
val, err := ParseJSONFloat(got)
|
||||||
if err != nil {
|
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
|
Comparator ValueComparator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j JSONPathValidator) Equals(got any) bool {
|
func (j JSONPathValidator) Equals(got any) error {
|
||||||
parsed, err := parse(got)
|
parsed, err := parse(got)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return err
|
||||||
}
|
}
|
||||||
val, err := jsonpath.Get(j.Path, parsed)
|
val, err := jsonpath.Get(j.Path, parsed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return j.Comparator.Equals(val)
|
return j.Comparator.Equals(val)
|
||||||
|
|
|
@ -11,13 +11,14 @@ type jsonPathValidator_EqualsTestCase[V validation.Value] struct {
|
||||||
expected V
|
expected V
|
||||||
jsonPath string
|
jsonPath string
|
||||||
json string
|
json string
|
||||||
want bool
|
wantErr bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tt jsonPathValidator_EqualsTestCase[V]) name() string {
|
func (tt jsonPathValidator_EqualsTestCase[V]) name() string {
|
||||||
return tt.testName
|
return tt.testName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:thelper // is not a helper
|
||||||
func (tt jsonPathValidator_EqualsTestCase[V]) run(t *testing.T) {
|
func (tt jsonPathValidator_EqualsTestCase[V]) run(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
@ -26,8 +27,10 @@ func (tt jsonPathValidator_EqualsTestCase[V]) run(t *testing.T) {
|
||||||
t.Fatalf("JSONPathValidatorFor() err = %v", err)
|
t.Fatalf("JSONPathValidatorFor() err = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if validator.Equals(tt.json) != tt.want {
|
if err := validator.Equals(tt.json); err != nil {
|
||||||
t.Errorf("Failed to equal value in %s to %v", tt.json, tt.expected)
|
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",
|
expected: "hello",
|
||||||
jsonPath: "$.greeting",
|
jsonPath: "$.greeting",
|
||||||
json: `{"greeting": "hello"}`,
|
json: `{"greeting": "hello"}`,
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonPathValidator_EqualsTestCase[string]{
|
jsonPathValidator_EqualsTestCase[string]{
|
||||||
testName: "Simple object navigation - number as string",
|
testName: "Simple object navigation - number as string",
|
||||||
expected: "42",
|
expected: "42",
|
||||||
jsonPath: "$.number",
|
jsonPath: "$.number",
|
||||||
json: `{"number": 42}`,
|
json: `{"number": 42}`,
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonPathValidator_EqualsTestCase[string]{
|
jsonPathValidator_EqualsTestCase[string]{
|
||||||
testName: "Simple array navigation",
|
testName: "Simple array navigation",
|
||||||
expected: "world",
|
expected: "world",
|
||||||
jsonPath: "$[1]",
|
jsonPath: "$[1]",
|
||||||
json: `["hello", "world"]`,
|
json: `["hello", "world"]`,
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonPathValidator_EqualsTestCase[int]{
|
jsonPathValidator_EqualsTestCase[int]{
|
||||||
testName: "Simple array navigation - string to int",
|
testName: "Simple array navigation - string to int",
|
||||||
expected: 37,
|
expected: 37,
|
||||||
jsonPath: "$[1]",
|
jsonPath: "$[1]",
|
||||||
json: `["13", "37"]`,
|
json: `["13", "37"]`,
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonPathValidator_EqualsTestCase[int]{
|
jsonPathValidator_EqualsTestCase[int]{
|
||||||
testName: "Simple array navigation - string to int - wrong value",
|
testName: "Simple array navigation - string to int - wrong value",
|
||||||
expected: 42,
|
expected: 42,
|
||||||
jsonPath: "$[1]",
|
jsonPath: "$[1]",
|
||||||
json: `["13", "37"]`,
|
json: `["13", "37"]`,
|
||||||
want: false,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
jsonPathValidator_EqualsTestCase[string]{
|
jsonPathValidator_EqualsTestCase[string]{
|
||||||
testName: "Simple array navigation - int to string",
|
testName: "Simple array navigation - int to string",
|
||||||
expected: "37",
|
expected: "37",
|
||||||
jsonPath: "$[1]",
|
jsonPath: "$[1]",
|
||||||
json: `[13, 37]`,
|
json: `[13, 37]`,
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
//nolint:paralleltest
|
//nolint:paralleltest
|
||||||
|
|
|
@ -47,6 +47,6 @@ type JSONValueComparator struct {
|
||||||
Comparator ValueComparator
|
Comparator ValueComparator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j JSONValueComparator) Equals(got any) bool {
|
func (j JSONValueComparator) Equals(got any) error {
|
||||||
return j.Comparator.Equals(got)
|
return j.Comparator.Equals(got)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,10 @@ type jsonValueComparator_EqualsTestCase[V validation.Value] struct {
|
||||||
testName string
|
testName string
|
||||||
expected V
|
expected V
|
||||||
got any
|
got any
|
||||||
want bool
|
wantErr bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:thelper // is not a helper
|
||||||
func (tt jsonValueComparator_EqualsTestCase[V]) run(t *testing.T) {
|
func (tt jsonValueComparator_EqualsTestCase[V]) run(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
@ -26,8 +27,10 @@ func (tt jsonValueComparator_EqualsTestCase[V]) run(t *testing.T) {
|
||||||
t.Fatalf("validation.JSONValueComparatorFor() err = %v", err)
|
t.Fatalf("validation.JSONValueComparatorFor() err = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if got := comparator.Equals(tt.got); got != tt.want {
|
if err := comparator.Equals(tt.got); err != nil {
|
||||||
t.Errorf("Equals() = %v, want %v", got, tt.want)
|
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",
|
testName: "Test int equality",
|
||||||
expected: 42,
|
expected: 42,
|
||||||
got: 42,
|
got: 42,
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[int]{
|
jsonValueComparator_EqualsTestCase[int]{
|
||||||
testName: "Test int equality - wrong value",
|
testName: "Test int equality - wrong value",
|
||||||
expected: 42,
|
expected: 42,
|
||||||
got: 43,
|
got: 43,
|
||||||
want: false,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[int]{
|
jsonValueComparator_EqualsTestCase[int]{
|
||||||
testName: "Test int equality - string value",
|
testName: "Test int equality - string value",
|
||||||
expected: 42,
|
expected: 42,
|
||||||
got: "42",
|
got: "42",
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[int]{
|
jsonValueComparator_EqualsTestCase[int]{
|
||||||
testName: "Test int equality - []byte value",
|
testName: "Test int equality - []byte value",
|
||||||
expected: 42,
|
expected: 42,
|
||||||
got: []byte("42"),
|
got: []byte("42"),
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[int]{
|
jsonValueComparator_EqualsTestCase[int]{
|
||||||
testName: "Test int equality - float value",
|
testName: "Test int equality - float value",
|
||||||
expected: 42,
|
expected: 42,
|
||||||
got: 42.0,
|
got: 42.0,
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[int8]{
|
jsonValueComparator_EqualsTestCase[int8]{
|
||||||
testName: "Test int8 equality",
|
testName: "Test int8 equality",
|
||||||
expected: 42,
|
expected: 42,
|
||||||
got: 42,
|
got: 42,
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[int8]{
|
jsonValueComparator_EqualsTestCase[int8]{
|
||||||
testName: "Test int8 equality - wrong value",
|
testName: "Test int8 equality - wrong value",
|
||||||
expected: 42,
|
expected: 42,
|
||||||
got: 43,
|
got: 43,
|
||||||
want: false,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[int8]{
|
jsonValueComparator_EqualsTestCase[int8]{
|
||||||
testName: "Test int8 equality - int16 value",
|
testName: "Test int8 equality - int16 value",
|
||||||
expected: 42,
|
expected: 42,
|
||||||
got: int16(42),
|
got: int16(42),
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[int8]{
|
jsonValueComparator_EqualsTestCase[int8]{
|
||||||
testName: "Test int8 equality - uint16 value",
|
testName: "Test int8 equality - uint16 value",
|
||||||
expected: 42,
|
expected: 42,
|
||||||
got: uint16(42),
|
got: uint16(42),
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[float32]{
|
jsonValueComparator_EqualsTestCase[float32]{
|
||||||
testName: "Test float32 equality - float value",
|
testName: "Test float32 equality - float value",
|
||||||
expected: 42.0,
|
expected: 42.0,
|
||||||
got: 42.0,
|
got: 42.0,
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[float32]{
|
jsonValueComparator_EqualsTestCase[float32]{
|
||||||
testName: "Test float32 equality - float value",
|
testName: "Test float32 equality - float value",
|
||||||
expected: 42.0,
|
expected: 42.0,
|
||||||
got: float64(42.0),
|
got: float64(42.0),
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[float64]{
|
jsonValueComparator_EqualsTestCase[float64]{
|
||||||
testName: "Test float64 equality - float value",
|
testName: "Test float64 equality - float value",
|
||||||
expected: 42.0,
|
expected: 42.0,
|
||||||
got: 42.0,
|
got: 42.0,
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[float64]{
|
jsonValueComparator_EqualsTestCase[float64]{
|
||||||
testName: "Test float64 equality - int value",
|
testName: "Test float64 equality - int value",
|
||||||
expected: 42.0,
|
expected: 42.0,
|
||||||
got: 42,
|
got: 42,
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[float64]{
|
jsonValueComparator_EqualsTestCase[float64]{
|
||||||
testName: "Test float64 equality - []byte value",
|
testName: "Test float64 equality - []byte value",
|
||||||
expected: 42.0,
|
expected: 42.0,
|
||||||
got: []byte("42"),
|
got: []byte("42"),
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[float64]{
|
jsonValueComparator_EqualsTestCase[float64]{
|
||||||
testName: "Test float64 equality - float32 value",
|
testName: "Test float64 equality - float32 value",
|
||||||
expected: 42.0,
|
expected: 42.0,
|
||||||
got: float32(42.0),
|
got: float32(42.0),
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[float64]{
|
jsonValueComparator_EqualsTestCase[float64]{
|
||||||
testName: "Test float64 equality - string value",
|
testName: "Test float64 equality - string value",
|
||||||
expected: 42.0,
|
expected: 42.0,
|
||||||
got: "42.0",
|
got: "42.0",
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[float64]{
|
jsonValueComparator_EqualsTestCase[float64]{
|
||||||
testName: "Test float64 equality - string value without dot",
|
testName: "Test float64 equality - string value without dot",
|
||||||
expected: 42.0,
|
expected: 42.0,
|
||||||
got: "42",
|
got: "42",
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[string]{
|
jsonValueComparator_EqualsTestCase[string]{
|
||||||
testName: "Test string equality",
|
testName: "Test string equality",
|
||||||
expected: "hello",
|
expected: "hello",
|
||||||
got: "hello",
|
got: "hello",
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[string]{
|
jsonValueComparator_EqualsTestCase[string]{
|
||||||
testName: "Test string equality - []byte value",
|
testName: "Test string equality - []byte value",
|
||||||
expected: "hello",
|
expected: "hello",
|
||||||
got: []byte("hello"),
|
got: []byte("hello"),
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[string]{
|
jsonValueComparator_EqualsTestCase[string]{
|
||||||
testName: "Test string equality - int value",
|
testName: "Test string equality - int value",
|
||||||
expected: "1337",
|
expected: "1337",
|
||||||
got: 1337,
|
got: 1337,
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[string]{
|
jsonValueComparator_EqualsTestCase[string]{
|
||||||
testName: "Test string equality - float value",
|
testName: "Test string equality - float value",
|
||||||
expected: "13.37",
|
expected: "13.37",
|
||||||
got: 13.37,
|
got: 13.37,
|
||||||
want: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
jsonValueComparator_EqualsTestCase[string]{
|
jsonValueComparator_EqualsTestCase[string]{
|
||||||
testName: "Test string equality - wrong case",
|
testName: "Test string equality - wrong case",
|
||||||
expected: "hello",
|
expected: "hello",
|
||||||
got: "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