feat(protocols): add basic HTTP checks

This commit is contained in:
Peter 2022-06-09 22:12:45 +02:00
parent 212f94b6ea
commit f9e3c2f4f1
Signed by: prskr
GPG key ID: C1DB5D2E8DB512F9
22 changed files with 568 additions and 120 deletions

View file

@ -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
View 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
}
}
}
}

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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
View 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
}

View 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
View 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
}

View 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])
}

View 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
}

View 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)
})
}

View file

@ -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 {

View file

@ -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
} }

View file

@ -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:

View file

@ -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 { )
func init() {
registry.Register("equals", func() validation.FromCall[redis.Cmder] {
return new(GenericCmdValidator) return new(GenericCmdValidator)
}, })
} }
)
type ( type CmdValidator interface {
CmdValidator interface {
Validate(cmder redis.Cmder) error 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) { 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

View file

@ -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
} }

View file

@ -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)

View file

@ -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

View file

@ -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)
} }

View file

@ -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
View 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
}