Skip to content

Commit 32763d1

Browse files
authored
Add web package for web applications support (#9)
* Add web package for web applications support
1 parent e021b41 commit 32763d1

38 files changed

+2618
-15
lines changed

CNAME

Lines changed: 0 additions & 1 deletion
This file was deleted.

conf/validate.go

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
"fmt"
2121
"reflect"
2222

23-
"github.com/antonmedv/expr"
23+
"github.com/expr-lang/expr"
2424
)
2525

2626
var validators = map[string]Validator{
@@ -39,13 +39,59 @@ func Register(name string, v Validator) {
3939

4040
// Validate validates a single variable.
4141
func Validate(tag reflect.StructTag, i interface{}) error {
42-
for name, v := range validators {
43-
if s, ok := tag.Lookup(name); ok {
44-
if err := v.Field(s, i); err != nil {
42+
if len(tag) > 0 {
43+
for name, v := range validators {
44+
if s, ok := tag.Lookup(name); ok && s != "-" {
45+
if err := v.Field(s, i); err != nil {
46+
return err
47+
}
48+
}
49+
}
50+
}
51+
return nil
52+
}
53+
54+
// ValidateStruct validates a single struct.
55+
func ValidateStruct(s interface{}) error {
56+
sValue := reflect.ValueOf(s)
57+
if reflect.Ptr == sValue.Type().Kind() {
58+
// ignore validate nil value
59+
if sValue.IsNil() {
60+
return nil
61+
}
62+
sValue = sValue.Elem()
63+
}
64+
return validStruct(sValue)
65+
}
66+
67+
func validStruct(v reflect.Value) error {
68+
vType := v.Type()
69+
if reflect.Struct != vType.Kind() {
70+
return fmt.Errorf("%s: is not a struct", vType.String())
71+
}
72+
73+
for i := 0; i < v.NumField(); i++ {
74+
fieldVal := v.Field(i)
75+
fieldType := vType.Field(i)
76+
if !fieldType.IsExported() {
77+
continue
78+
}
79+
80+
if err := Validate(fieldType.Tag, fieldVal.Interface()); nil != err {
81+
return err
82+
}
83+
84+
if reflect.Struct == fieldType.Type.Kind() {
85+
if err := validStruct(fieldVal); nil != err {
86+
return err
87+
}
88+
} else if reflect.Ptr == fieldType.Type.Kind() && reflect.Struct == fieldType.Type.Elem().Kind() && !fieldVal.IsNil() {
89+
if err := validStruct(fieldVal.Elem()); nil != err {
4590
return err
4691
}
4792
}
4893
}
94+
4995
return nil
5096
}
5197

conf/validate_test.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func (d *emptyValidator) Field(tag string, i interface{}) error {
4141
return nil
4242
}
4343

44-
func TestField(t *testing.T) {
44+
func TestValidateField(t *testing.T) {
4545
i := 6
4646

4747
err := Validate("empty:\"\"", i)
@@ -57,3 +57,28 @@ func TestField(t *testing.T) {
5757
err = Validate("expr:\"$<3\"", "abc")
5858
assert.Error(t, err, "invalid operation\\: string \\< int \\(1:2\\)")
5959
}
60+
61+
func TestValidateStruct(t *testing.T) {
62+
type testForm struct {
63+
Age int `expr:"$>=18"`
64+
Summary struct {
65+
Weight int `expr:"$>100"`
66+
}
67+
Skip *struct{}
68+
unexported struct{}
69+
}
70+
71+
tf1 := testForm{Age: 18}
72+
tf1.Summary.Weight = 101
73+
err := ValidateStruct(tf1)
74+
assert.Nil(t, err)
75+
76+
tf2 := testForm{Age: 17}
77+
err = ValidateStruct(tf2)
78+
assert.Error(t, err, "validate failed on \"\\$>=18\" for value 17")
79+
80+
tf3 := testForm{Age: 18}
81+
err = ValidateStruct(tf3)
82+
assert.Error(t, err, "validate failed on \"\\$>100\" for value 0")
83+
84+
}

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ module github.com/go-spring-projects/go-spring
33
go 1.21
44

55
require (
6-
github.com/antonmedv/expr v1.15.3
6+
github.com/expr-lang/expr v1.15.6
77
github.com/golang/mock v1.6.0
8+
github.com/gorilla/mux v1.8.1
89
github.com/magiconair/properties v1.8.7
910
github.com/pelletier/go-toml v1.9.5
10-
github.com/spf13/cast v1.5.1
11+
github.com/spf13/cast v1.6.0
1112
gopkg.in/yaml.v2 v2.4.0
1213
)

go.sum

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
github.com/antonmedv/expr v1.15.3 h1:q3hOJZNvLvhqE8OHBs1cFRdbXFNKuA+bHmRaI+AmRmI=
2-
github.com/antonmedv/expr v1.15.3/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE=
31
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
42
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5-
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
6-
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
3+
github.com/expr-lang/expr v1.15.6 h1:dQFgzj5DBu3wnUz8+PGLZdPMpefAvxaCFTNM3iSjkGA=
4+
github.com/expr-lang/expr v1.15.6/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
5+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
6+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
77
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
88
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
99
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
1010
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
11+
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
12+
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
1113
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
1214
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
1315
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -20,8 +22,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
2022
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2123
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
2224
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
23-
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
24-
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
25+
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
26+
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
2527
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
2628
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
2729
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=

gs/cond/cond.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import (
2525
"strconv"
2626
"strings"
2727

28-
"github.com/antonmedv/expr"
28+
"github.com/expr-lang/expr"
2929
"github.com/go-spring-projects/go-spring/conf"
3030
"github.com/go-spring-projects/go-spring/internal/utils"
3131
)

web/bind.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package web
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"net/http"
23+
"reflect"
24+
25+
"github.com/go-spring-projects/go-spring/internal/utils"
26+
"github.com/go-spring-projects/go-spring/web/binding"
27+
"github.com/go-spring-projects/go-spring/web/render"
28+
)
29+
30+
type Renderer interface {
31+
Render(ctx context.Context, err error, result interface{}) render.Renderer
32+
}
33+
34+
type RendererFunc func(ctx context.Context, err error, result interface{}) render.Renderer
35+
36+
func (fn RendererFunc) Render(ctx context.Context, err error, result interface{}) render.Renderer {
37+
return fn(ctx, err, result)
38+
}
39+
40+
// Bind convert fn to HandlerFunc.
41+
//
42+
// func(ctx context.Context)
43+
//
44+
// func(ctx context.Context) R
45+
//
46+
// func(ctx context.Context, req T) R
47+
//
48+
// func(ctx context.Context, req T) (R, error)
49+
//
50+
// func(writer http.ResponseWriter, request *http.Request)
51+
func Bind(fn interface{}, render Renderer) http.HandlerFunc {
52+
53+
fnValue := reflect.ValueOf(fn)
54+
fnType := fnValue.Type()
55+
56+
switch h := fn.(type) {
57+
case http.HandlerFunc:
58+
return h
59+
case http.Handler:
60+
return h.ServeHTTP
61+
case func(http.ResponseWriter, *http.Request):
62+
return h
63+
default:
64+
// valid func
65+
if err := validMappingFunc(fnType); nil != err {
66+
panic(err)
67+
}
68+
}
69+
70+
return func(writer http.ResponseWriter, request *http.Request) {
71+
72+
// param of context
73+
webCtx := &Context{Writer: writer, Request: request}
74+
ctx := WithContext(request.Context(), webCtx)
75+
ctxValue := reflect.ValueOf(ctx)
76+
77+
defer func() {
78+
if nil != request.MultipartForm {
79+
request.MultipartForm.RemoveAll()
80+
}
81+
request.Body.Close()
82+
}()
83+
84+
var returnValues []reflect.Value
85+
var err error
86+
87+
defer func() {
88+
if r := recover(); nil != r {
89+
if e, ok := r.(error); ok {
90+
err = fmt.Errorf("%s: %w", request.URL.Path, e)
91+
} else {
92+
err = fmt.Errorf("%s: %v", request.URL.Path, r)
93+
}
94+
95+
// render error response
96+
render.Render(ctx, err, nil).Render(writer)
97+
}
98+
}()
99+
100+
switch fnType.NumIn() {
101+
case 1:
102+
returnValues = fnValue.Call([]reflect.Value{ctxValue})
103+
case 2:
104+
paramType := fnType.In(1)
105+
pointer := false
106+
if reflect.Ptr == paramType.Kind() {
107+
paramType = paramType.Elem()
108+
pointer = true
109+
}
110+
111+
// new param instance with paramType.
112+
paramValue := reflect.New(paramType)
113+
// bind paramValue with request
114+
if err = binding.Bind(paramValue.Interface(), webCtx); nil != err {
115+
break
116+
}
117+
if !pointer {
118+
paramValue = paramValue.Elem()
119+
}
120+
returnValues = fnValue.Call([]reflect.Value{ctxValue, paramValue})
121+
default:
122+
panic("unreachable here")
123+
}
124+
125+
var result interface{}
126+
127+
if nil == err {
128+
switch len(returnValues) {
129+
case 0:
130+
// nothing
131+
return
132+
case 1:
133+
// write response
134+
result = returnValues[0].Interface()
135+
case 2:
136+
// check error
137+
result = returnValues[0].Interface()
138+
if e, ok := returnValues[1].Interface().(error); ok && nil != e {
139+
err = e
140+
}
141+
default:
142+
panic("unreachable here")
143+
}
144+
}
145+
146+
// render response
147+
render.Render(ctx, err, result).Render(writer)
148+
}
149+
}
150+
151+
func validMappingFunc(fnType reflect.Type) error {
152+
// func(ctx context.Context)
153+
// func(ctx context.Context) R
154+
// func(ctx context.Context, req T) R
155+
// func(ctx context.Context, req T) (R, error)
156+
if !utils.IsFuncType(fnType) {
157+
return fmt.Errorf("%s: not a func", fnType.String())
158+
}
159+
160+
if fnType.NumIn() < 1 || fnType.NumIn() > 2 {
161+
return fmt.Errorf("%s: invalid input parameter count", fnType.String())
162+
}
163+
164+
if fnType.NumOut() > 2 {
165+
return fmt.Errorf("%s: invalid output parameter count", fnType.String())
166+
}
167+
168+
if !utils.IsContextType(fnType.In(0)) {
169+
return fmt.Errorf("%s: first input param type (%s) must be context", fnType.String(), fnType.In(0).String())
170+
}
171+
172+
if fnType.NumIn() > 1 {
173+
argType := fnType.In(1)
174+
if !(reflect.Struct == argType.Kind() || (reflect.Ptr == argType.Kind() && reflect.Struct == argType.Elem().Kind())) {
175+
return fmt.Errorf("%s: second input param type (%s) must be struct/*struct", fnType.String(), argType.String())
176+
}
177+
}
178+
179+
if 0 < fnType.NumOut() && utils.IsErrorType(fnType.Out(0)) {
180+
return fmt.Errorf("%s: first output param type not be error", fnType.String())
181+
}
182+
183+
if 1 < fnType.NumOut() && !utils.IsErrorType(fnType.Out(1)) {
184+
return fmt.Errorf("%s: second output type (%s) must a error", fnType.String(), fnType.Out(1).String())
185+
}
186+
187+
return nil
188+
}

0 commit comments

Comments
 (0)