Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion CNAME

This file was deleted.

54 changes: 50 additions & 4 deletions conf/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
"fmt"
"reflect"

"github.com/antonmedv/expr"
"github.com/expr-lang/expr"
)

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

// Validate validates a single variable.
func Validate(tag reflect.StructTag, i interface{}) error {
for name, v := range validators {
if s, ok := tag.Lookup(name); ok {
if err := v.Field(s, i); err != nil {
if len(tag) > 0 {
for name, v := range validators {
if s, ok := tag.Lookup(name); ok && s != "-" {
if err := v.Field(s, i); err != nil {
return err
}
}
}
}
return nil
}

// ValidateStruct validates a single struct.
func ValidateStruct(s interface{}) error {
sValue := reflect.ValueOf(s)
if reflect.Ptr == sValue.Type().Kind() {
// ignore validate nil value
if sValue.IsNil() {
return nil
}
sValue = sValue.Elem()
}
return validStruct(sValue)
}

func validStruct(v reflect.Value) error {
vType := v.Type()
if reflect.Struct != vType.Kind() {
return fmt.Errorf("%s: is not a struct", vType.String())
}

for i := 0; i < v.NumField(); i++ {
fieldVal := v.Field(i)
fieldType := vType.Field(i)
if !fieldType.IsExported() {
continue
}

if err := Validate(fieldType.Tag, fieldVal.Interface()); nil != err {
return err
}

if reflect.Struct == fieldType.Type.Kind() {
if err := validStruct(fieldVal); nil != err {
return err
}
} else if reflect.Ptr == fieldType.Type.Kind() && reflect.Struct == fieldType.Type.Elem().Kind() && !fieldVal.IsNil() {
if err := validStruct(fieldVal.Elem()); nil != err {
return err
}
}
}

return nil
}

Expand Down
27 changes: 26 additions & 1 deletion conf/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (d *emptyValidator) Field(tag string, i interface{}) error {
return nil
}

func TestField(t *testing.T) {
func TestValidateField(t *testing.T) {
i := 6

err := Validate("empty:\"\"", i)
Expand All @@ -57,3 +57,28 @@ func TestField(t *testing.T) {
err = Validate("expr:\"$<3\"", "abc")
assert.Error(t, err, "invalid operation\\: string \\< int \\(1:2\\)")
}

func TestValidateStruct(t *testing.T) {
type testForm struct {
Age int `expr:"$>=18"`
Summary struct {
Weight int `expr:"$>100"`
}
Skip *struct{}
unexported struct{}
}

tf1 := testForm{Age: 18}
tf1.Summary.Weight = 101
err := ValidateStruct(tf1)
assert.Nil(t, err)

tf2 := testForm{Age: 17}
err = ValidateStruct(tf2)
assert.Error(t, err, "validate failed on \"\\$>=18\" for value 17")

tf3 := testForm{Age: 18}
err = ValidateStruct(tf3)
assert.Error(t, err, "validate failed on \"\\$>100\" for value 0")

}
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ module github.com/go-spring-projects/go-spring
go 1.21

require (
github.com/antonmedv/expr v1.15.3
github.com/expr-lang/expr v1.15.6
github.com/golang/mock v1.6.0
github.com/gorilla/mux v1.8.1
github.com/magiconair/properties v1.8.7
github.com/pelletier/go-toml v1.9.5
github.com/spf13/cast v1.5.1
github.com/spf13/cast v1.6.0
gopkg.in/yaml.v2 v2.4.0
)
14 changes: 8 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
github.com/antonmedv/expr v1.15.3 h1:q3hOJZNvLvhqE8OHBs1cFRdbXFNKuA+bHmRaI+AmRmI=
github.com/antonmedv/expr v1.15.3/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/expr-lang/expr v1.15.6 h1:dQFgzj5DBu3wnUz8+PGLZdPMpefAvxaCFTNM3iSjkGA=
github.com/expr-lang/expr v1.15.6/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
Expand All @@ -20,8 +22,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
Expand Down
2 changes: 1 addition & 1 deletion gs/cond/cond.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
"strconv"
"strings"

"github.com/antonmedv/expr"
"github.com/expr-lang/expr"
"github.com/go-spring-projects/go-spring/conf"
"github.com/go-spring-projects/go-spring/internal/utils"
)
Expand Down
188 changes: 188 additions & 0 deletions web/bind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package web

import (
"context"
"fmt"
"net/http"
"reflect"

"github.com/go-spring-projects/go-spring/internal/utils"
"github.com/go-spring-projects/go-spring/web/binding"
"github.com/go-spring-projects/go-spring/web/render"
)

type Renderer interface {
Render(ctx context.Context, err error, result interface{}) render.Renderer
}

type RendererFunc func(ctx context.Context, err error, result interface{}) render.Renderer

func (fn RendererFunc) Render(ctx context.Context, err error, result interface{}) render.Renderer {
return fn(ctx, err, result)
}

// Bind convert fn to HandlerFunc.
//
// func(ctx context.Context)
//
// func(ctx context.Context) R
//
// func(ctx context.Context, req T) R
//
// func(ctx context.Context, req T) (R, error)
//
// func(writer http.ResponseWriter, request *http.Request)
func Bind(fn interface{}, render Renderer) http.HandlerFunc {

fnValue := reflect.ValueOf(fn)
fnType := fnValue.Type()

switch h := fn.(type) {
case http.HandlerFunc:
return h
case http.Handler:
return h.ServeHTTP
case func(http.ResponseWriter, *http.Request):
return h
default:
// valid func
if err := validMappingFunc(fnType); nil != err {
panic(err)
}
}

return func(writer http.ResponseWriter, request *http.Request) {

// param of context
webCtx := &Context{Writer: writer, Request: request}
ctx := WithContext(request.Context(), webCtx)
ctxValue := reflect.ValueOf(ctx)

defer func() {
if nil != request.MultipartForm {
request.MultipartForm.RemoveAll()
}
request.Body.Close()
}()

var returnValues []reflect.Value
var err error

defer func() {
if r := recover(); nil != r {
if e, ok := r.(error); ok {
err = fmt.Errorf("%s: %w", request.URL.Path, e)
} else {
err = fmt.Errorf("%s: %v", request.URL.Path, r)
}

// render error response
render.Render(ctx, err, nil).Render(writer)
}
}()

switch fnType.NumIn() {
case 1:
returnValues = fnValue.Call([]reflect.Value{ctxValue})
case 2:
paramType := fnType.In(1)
pointer := false
if reflect.Ptr == paramType.Kind() {
paramType = paramType.Elem()
pointer = true
}

// new param instance with paramType.
paramValue := reflect.New(paramType)
// bind paramValue with request
if err = binding.Bind(paramValue.Interface(), webCtx); nil != err {
break
}
if !pointer {
paramValue = paramValue.Elem()
}
returnValues = fnValue.Call([]reflect.Value{ctxValue, paramValue})
default:
panic("unreachable here")
}

var result interface{}

if nil == err {
switch len(returnValues) {
case 0:
// nothing
return
case 1:
// write response
result = returnValues[0].Interface()
case 2:
// check error
result = returnValues[0].Interface()
if e, ok := returnValues[1].Interface().(error); ok && nil != e {
err = e
}
default:
panic("unreachable here")
}
}

// render response
render.Render(ctx, err, result).Render(writer)
}
}

func validMappingFunc(fnType reflect.Type) error {
// func(ctx context.Context)
// func(ctx context.Context) R
// func(ctx context.Context, req T) R
// func(ctx context.Context, req T) (R, error)
if !utils.IsFuncType(fnType) {
return fmt.Errorf("%s: not a func", fnType.String())
}

if fnType.NumIn() < 1 || fnType.NumIn() > 2 {
return fmt.Errorf("%s: invalid input parameter count", fnType.String())
}

if fnType.NumOut() > 2 {
return fmt.Errorf("%s: invalid output parameter count", fnType.String())
}

if !utils.IsContextType(fnType.In(0)) {
return fmt.Errorf("%s: first input param type (%s) must be context", fnType.String(), fnType.In(0).String())
}

if fnType.NumIn() > 1 {
argType := fnType.In(1)
if !(reflect.Struct == argType.Kind() || (reflect.Ptr == argType.Kind() && reflect.Struct == argType.Elem().Kind())) {
return fmt.Errorf("%s: second input param type (%s) must be struct/*struct", fnType.String(), argType.String())
}
}

if 0 < fnType.NumOut() && utils.IsErrorType(fnType.Out(0)) {
return fmt.Errorf("%s: first output param type not be error", fnType.String())
}

if 1 < fnType.NumOut() && !utils.IsErrorType(fnType.Out(1)) {
return fmt.Errorf("%s: second output type (%s) must a error", fnType.String(), fnType.Out(1).String())
}

return nil
}
Loading