Skip to content

Commit fad4435

Browse files
Merge pull request #21 from chenmingyong0423/feature/hook
feat(validator): implements validation hook
2 parents 1328bdf + 85b5572 commit fad4435

File tree

5 files changed

+286
-3
lines changed

5 files changed

+286
-3
lines changed

callback/callback.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ package callback
1717
import (
1818
"context"
1919

20+
"github.com/chenmingyong0423/go-mongox/hook/validator"
21+
2022
"github.com/chenmingyong0423/go-mongox/hook/model"
2123

2224
"github.com/chenmingyong0423/go-mongox/hook/field"
@@ -42,6 +44,12 @@ func initializeCallbacks() *Callback {
4244
return model.Execute(ctx, opCtx, operation.OpTypeBeforeInsert, opts...)
4345
},
4446
},
47+
{
48+
name: "mongox:validation",
49+
fn: func(ctx context.Context, opCtx *operation.OpContext, opts ...any) error {
50+
return validator.Execute(ctx, opCtx, operation.OpTypeBeforeInsert, opts...)
51+
},
52+
},
4553
},
4654
afterInsert: []callbackHandler{
4755
{
@@ -68,6 +76,12 @@ func initializeCallbacks() *Callback {
6876
return model.Execute(ctx, opCtx, operation.OpTypeBeforeUpsert, opts...)
6977
},
7078
},
79+
{
80+
name: "mongox:validation",
81+
fn: func(ctx context.Context, opCtx *operation.OpContext, opts ...any) error {
82+
return validator.Execute(ctx, opCtx, operation.OpTypeBeforeUpsert, opts...)
83+
},
84+
},
7185
},
7286
afterUpsert: []callbackHandler{
7387
{

go.mod

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,30 @@ module github.com/chenmingyong0423/go-mongox
33
go 1.20
44

55
require (
6+
github.com/go-playground/validator/v10 v10.19.0
67
github.com/stretchr/testify v1.8.4
78
go.mongodb.org/mongo-driver v1.13.1
89
go.uber.org/mock v0.4.0
910
)
1011

1112
require (
1213
github.com/davecgh/go-spew v1.1.1 // indirect
14+
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
15+
github.com/go-playground/locales v0.14.1 // indirect
16+
github.com/go-playground/universal-translator v0.18.1 // indirect
1317
github.com/golang/snappy v0.0.4 // indirect
1418
github.com/klauspost/compress v1.17.4 // indirect
19+
github.com/leodido/go-urn v1.4.0 // indirect
1520
github.com/montanaflynn/stats v0.7.1 // indirect
1621
github.com/pmezard/go-difflib v1.0.0 // indirect
1722
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
1823
github.com/xdg-go/scram v1.1.2 // indirect
1924
github.com/xdg-go/stringprep v1.0.4 // indirect
2025
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
21-
golang.org/x/crypto v0.17.0 // indirect
26+
golang.org/x/crypto v0.19.0 // indirect
27+
golang.org/x/net v0.21.0 // indirect
2228
golang.org/x/sync v0.5.0 // indirect
29+
golang.org/x/sys v0.17.0 // indirect
2330
golang.org/x/text v0.14.0 // indirect
2431
gopkg.in/yaml.v3 v3.0.1 // indirect
2532
)

go.sum

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
22
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
4+
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
5+
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
6+
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
7+
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
8+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
9+
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
10+
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
11+
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
312
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
413
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
514
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -8,6 +17,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
817
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
918
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
1019
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
20+
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
21+
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
1122
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
1223
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
1324
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
@@ -33,14 +44,16 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
3344
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
3445
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
3546
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
36-
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
37-
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
47+
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
48+
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
3849
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
3950
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
4051
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
4152
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
4253
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
4354
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
55+
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
56+
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
4457
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
4558
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
4659
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
@@ -52,6 +65,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
5265
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
5366
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
5467
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
68+
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
69+
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
5570
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
5671
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
5772
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

hook/validator/validator.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2024 chenmingyong0423
2+
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package validator
16+
17+
import (
18+
"context"
19+
"reflect"
20+
"time"
21+
22+
"github.com/go-playground/validator/v10"
23+
24+
"github.com/chenmingyong0423/go-mongox/operation"
25+
)
26+
27+
var validate = validator.New()
28+
29+
func getPayload(opCtx *operation.OpContext, opType operation.OpType) any {
30+
switch opType {
31+
case operation.OpTypeBeforeInsert:
32+
return opCtx.Doc
33+
case operation.OpTypeBeforeUpsert:
34+
return opCtx.Replacement
35+
default:
36+
return nil
37+
}
38+
}
39+
40+
func Execute(ctx context.Context, opCtx *operation.OpContext, opType operation.OpType, opts ...any) error {
41+
payLoad := getPayload(opCtx, opType)
42+
if payLoad == nil {
43+
return nil
44+
}
45+
value := reflect.ValueOf(payLoad)
46+
if value.IsZero() {
47+
return nil
48+
}
49+
switch value.Type().Kind() {
50+
case reflect.Slice:
51+
return executeSlice(ctx, value, opts...)
52+
case reflect.Ptr:
53+
return execute(ctx, value, opts...)
54+
default:
55+
return nil
56+
}
57+
}
58+
59+
func executeSlice(ctx context.Context, docs reflect.Value, opts ...any) error {
60+
for i := 0; i < docs.Len(); i++ {
61+
doc := docs.Index(i)
62+
if err := execute(ctx, doc, opts...); err != nil {
63+
return err
64+
}
65+
}
66+
return nil
67+
}
68+
69+
func execute(ctx context.Context, value reflect.Value, _ ...any) error {
70+
doc := validateStruct(value)
71+
if doc == nil {
72+
return nil
73+
}
74+
return validate.StructCtx(ctx, doc)
75+
}
76+
77+
func validateStruct(doc reflect.Value) any {
78+
if doc.Kind() == reflect.Pointer && !doc.IsNil() {
79+
doc = doc.Elem()
80+
}
81+
if doc.Kind() != reflect.Struct || doc.Type().ConvertibleTo(reflect.TypeOf(time.Time{})) {
82+
return nil
83+
}
84+
return doc.Interface()
85+
}

hook/validator/validator_test.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Copyright 2024 chenmingyong0423
2+
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package validator
16+
17+
import (
18+
"context"
19+
"errors"
20+
"log"
21+
"testing"
22+
"time"
23+
24+
"github.com/go-playground/validator/v10"
25+
26+
"github.com/stretchr/testify/require"
27+
28+
"github.com/chenmingyong0423/go-mongox/operation"
29+
)
30+
31+
func TestExecute(t *testing.T) {
32+
type User struct {
33+
Name string `bson:"name"`
34+
Age int `bson:"age" validate:"gte=0,lte=150"`
35+
}
36+
37+
testCases := []struct {
38+
name string
39+
40+
ctx context.Context
41+
doc *operation.OpContext
42+
opType operation.OpType
43+
44+
errFunc require.ErrorAssertionFunc
45+
}{
46+
{
47+
name: "unaccepted operation type",
48+
ctx: context.Background(),
49+
doc: operation.NewOpContext(nil, operation.WithDoc(&User{Name: "Mingyong Chen", Age: 18})),
50+
opType: operation.OpTypeAfterInsert,
51+
errFunc: require.NoError,
52+
},
53+
{
54+
name: "nil value",
55+
ctx: context.Background(),
56+
doc: operation.NewOpContext(nil),
57+
opType: operation.OpTypeBeforeInsert,
58+
errFunc: require.NoError,
59+
},
60+
{
61+
name: "unsupported type",
62+
ctx: context.Background(),
63+
doc: operation.NewOpContext(nil, operation.WithDoc(6)),
64+
opType: operation.OpTypeBeforeInsert,
65+
errFunc: require.NoError,
66+
},
67+
{
68+
name: "unsupported point type",
69+
ctx: context.Background(),
70+
doc: operation.NewOpContext(nil, operation.WithDoc(func() *int {
71+
i := 6
72+
return &i
73+
}())),
74+
opType: operation.OpTypeBeforeInsert,
75+
errFunc: require.NoError,
76+
},
77+
{
78+
name: "special unsupported type - time.Time{}",
79+
ctx: context.Background(),
80+
doc: operation.NewOpContext(nil, operation.WithDoc(&time.Time{})),
81+
opType: operation.OpTypeBeforeInsert,
82+
errFunc: require.NoError,
83+
},
84+
{
85+
name: "*User(nil)",
86+
ctx: context.Background(),
87+
doc: operation.NewOpContext(nil, operation.WithDoc((*User)(nil))),
88+
opType: operation.OpTypeBeforeInsert,
89+
errFunc: require.NoError,
90+
},
91+
{
92+
name: "fails to validate struct in case of BeforeInsert",
93+
ctx: context.Background(),
94+
doc: operation.NewOpContext(nil, operation.WithDoc(&User{Age: -1})),
95+
opType: operation.OpTypeBeforeInsert,
96+
errFunc: func(t require.TestingT, err error, i ...interface{}) {
97+
var e validator.ValidationErrors
98+
if !errors.As(err, &e) {
99+
log.Fatal(err)
100+
}
101+
},
102+
},
103+
{
104+
name: "fails to validate struct in case of BeforeUpsert",
105+
ctx: context.Background(),
106+
doc: operation.NewOpContext(nil, operation.WithReplacement(&User{Age: -1})),
107+
opType: operation.OpTypeBeforeUpsert,
108+
errFunc: func(t require.TestingT, err error, i ...interface{}) {
109+
var e validator.ValidationErrors
110+
if !errors.As(err, &e) {
111+
log.Fatal(err)
112+
}
113+
},
114+
},
115+
{
116+
name: "fails to validate slice in case of BeforeInsert",
117+
ctx: context.Background(),
118+
doc: operation.NewOpContext(nil, operation.WithDoc([]*User{
119+
{Age: -1},
120+
{Age: 18},
121+
})),
122+
opType: operation.OpTypeBeforeInsert,
123+
errFunc: func(t require.TestingT, err error, i ...interface{}) {
124+
var e validator.ValidationErrors
125+
if !errors.As(err, &e) {
126+
log.Fatal(err)
127+
}
128+
},
129+
},
130+
{
131+
name: "validate struct successfully in case of BeforeInsert",
132+
ctx: context.Background(),
133+
doc: operation.NewOpContext(nil, operation.WithDoc(&User{Age: 18})),
134+
opType: operation.OpTypeBeforeInsert,
135+
errFunc: require.NoError,
136+
},
137+
{
138+
name: "validate slice successfully in case of BeforeInsert",
139+
ctx: context.Background(),
140+
doc: operation.NewOpContext(nil, operation.WithDoc([]*User{
141+
{Age: 18},
142+
{Age: 20},
143+
})),
144+
opType: operation.OpTypeBeforeInsert,
145+
errFunc: require.NoError,
146+
},
147+
{
148+
name: "validate struct successfully in case of BeforeUpsert",
149+
ctx: context.Background(),
150+
doc: operation.NewOpContext(nil, operation.WithReplacement(&User{Age: 18})),
151+
opType: operation.OpTypeBeforeUpsert,
152+
errFunc: require.NoError,
153+
},
154+
}
155+
156+
for _, tc := range testCases {
157+
t.Run(tc.name, func(t *testing.T) {
158+
err := Execute(tc.ctx, tc.doc, tc.opType)
159+
tc.errFunc(t, err)
160+
})
161+
}
162+
}

0 commit comments

Comments
 (0)