Skip to content

Commit

Permalink
feat: support query array in httpx.Parse
Browse files Browse the repository at this point in the history
  • Loading branch information
kevwan committed Oct 27, 2024
1 parent f822c9a commit b9b8cfd
Show file tree
Hide file tree
Showing 13 changed files with 221 additions and 11 deletions.
21 changes: 21 additions & 0 deletions core/mapping/unmarshaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type (

unmarshalOptions struct {
fillDefault bool
fromArray bool
fromString bool
opaqueKeys bool
canonicalKey func(key string) string
Expand Down Expand Up @@ -811,6 +812,16 @@ func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect
return u.processNamedFieldWithoutValue(field.Type, value, opts, fullName)
}

if u.opts.fromArray {
fieldKind := field.Type.Kind()
if fieldKind != reflect.Slice && fieldKind != reflect.Array {
valueKind := reflect.TypeOf(mapValue).Kind()
if valueKind == reflect.Slice || valueKind == reflect.Array {
mapValue = reflect.ValueOf(mapValue).Index(0).Interface()
}
}
}

return u.processNamedFieldWithValue(field.Type, value, valueWithParent{
value: mapValue,
parent: valuer,
Expand Down Expand Up @@ -990,6 +1001,16 @@ func WithDefault() UnmarshalOption {
}
}

// WithFromArray customizes an Unmarshaler with converting array values to non-array types.
// For example, if the field type is []string, and the value is [hello],
// the field type can be `string`, instead of `[]string`.
// Typically, this option is used for unmarshaling from form values.
func WithFromArray() UnmarshalOption {
return func(opt *unmarshalOptions) {
opt.fromArray = true
}
}

// WithOpaqueKeys customizes an Unmarshaler with opaque keys.
// Opaque keys are keys that are not processed by the unmarshaler.
func WithOpaqueKeys() UnmarshalOption {
Expand Down
56 changes: 56 additions & 0 deletions core/mapping/unmarshaler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5639,6 +5639,62 @@ func TestUnmarshalFromStringSliceForTypeMismatch(t *testing.T) {
}, &v))
}

func TestUnmarshalWithFromArray(t *testing.T) {
t.Run("array", func(t *testing.T) {
var v struct {
Value []string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{
"value": []string{"foo", "bar"},
}, &v)) {
assert.ElementsMatch(t, []string{"foo", "bar"}, v.Value)
}
})

t.Run("not array", func(t *testing.T) {
var v struct {
Value string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{
"value": []string{"foo"},
}, &v)) {
assert.Equal(t, "foo", v.Value)
}
})

t.Run("not array and empty", func(t *testing.T) {
var v struct {
Value string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{
"value": []string{""},
}, &v)) {
assert.Empty(t, v.Value)
}
})

t.Run("not array and no value", func(t *testing.T) {
var v struct {
Value string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
assert.Error(t, unmarshaler.Unmarshal(map[string]any{}, &v))
})

t.Run("not array and no value and optional", func(t *testing.T) {
var v struct {
Value string `key:"value,optional"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{}, &v)) {
assert.Empty(t, v.Value)
}
})
}

func TestUnmarshalWithOpaqueKeys(t *testing.T) {
var v struct {
Opaque string `key:"opaque.key"`
Expand Down
14 changes: 14 additions & 0 deletions core/proc/shutdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
package proc

import (
"fmt"
"os"
"os/signal"
"runtime/debug"
"sync"
"sync/atomic"
"syscall"
"time"

"github.com/davecgh/go-spew/spew"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stringx"
"github.com/zeromicro/go-zero/core/threading"
)

Expand Down Expand Up @@ -70,20 +75,29 @@ type listenerManager struct {
lock sync.Mutex
waitGroup sync.WaitGroup
listeners []func()
count int32
}

func (lm *listenerManager) addListener(fn func()) (waitForCalled func()) {
fmt.Println(string(debug.Stack()))
id := stringx.RandId()
lm.waitGroup.Add(1)
fmt.Println("add:", atomic.AddInt32(&lm.count, 1), id)

lm.lock.Lock()
lm.listeners = append(lm.listeners, func() {
defer lm.waitGroup.Done()
fn()
fmt.Println("done:", atomic.AddInt32(&lm.count, -1), id)
})
lm.lock.Unlock()
spew.Dump(&lm.waitGroup)

return func() {
fmt.Println("wait")
spew.Dump(&lm.waitGroup)
lm.waitGroup.Wait()
fmt.Println("waited")
}
}

Expand Down
36 changes: 36 additions & 0 deletions core/proc/shutdown_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,42 @@ func TestShutdown(t *testing.T) {
assert.Equal(t, 3, val)
}

func TestShutdownWithMultipleServices(t *testing.T) {
SetTimeToForceQuit(time.Hour)
assert.Equal(t, time.Hour, delayTimeBeforeForceQuit)

var val int
called1 := AddShutdownListener(func() {
val++
})
called2 := AddShutdownListener(func() {
val += 2
})
Shutdown()
called1()
called2()

assert.Equal(t, 3, val)
}

func TestWrapUpWithMultipleServices(t *testing.T) {
SetTimeToForceQuit(time.Hour)
assert.Equal(t, time.Hour, delayTimeBeforeForceQuit)

var val int
called1 := AddWrapUpListener(func() {
val++
})
called2 := AddWrapUpListener(func() {
val += 2
})
WrapUp()
called1()
called2()

assert.Equal(t, 3, val)
}

func TestNotifyMoreThanOnce(t *testing.T) {
ch := make(chan struct{}, 1)

Expand Down
7 changes: 6 additions & 1 deletion core/service/servicegroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,14 @@ func (sg *ServiceGroup) doStart() {
}

func (sg *ServiceGroup) doStop() {
group := threading.NewRoutineGroup()
for _, service := range sg.services {
service.Stop()
// new variable to avoid closure problems, can be removed after go 1.22
// see https://golang.org/doc/faq#closures_and_goroutines
service := service
group.Run(service.Stop)
}
group.Wait()
}

// WithStart wraps a start func as a Service.
Expand Down
6 changes: 6 additions & 0 deletions gateway/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type (
Server struct {
*rest.Server
upstreams []Upstream
conns []zrpc.Client
processHeader func(http.Header) []string
dialer func(conf zrpc.RpcClientConf) zrpc.Client
}
Expand All @@ -48,10 +49,14 @@ func MustNewServer(c GatewayConf, opts ...Option) *Server {
func (s *Server) Start() {
logx.Must(s.build())
s.Server.Start()
fmt.Println("gateway stopped")
}

// Stop stops the gateway server.
func (s *Server) Stop() {
for _, conn := range s.conns {
conn.Conn().Close()
}
s.Server.Stop()
}

Expand All @@ -71,6 +76,7 @@ func (s *Server) build() error {
} else {
cli = zrpc.MustNewClient(up.Grpc)
}
s.conns = append(s.conns, cli)

source, err := s.createDescriptorSource(cli, up)
if err != nil {
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module github.com/zeromicro/go-zero

go 1.20
go 1.21

toolchain go1.22.5

require (
github.com/DATA-DOG/go-sqlmock v1.5.2
Expand Down
13 changes: 10 additions & 3 deletions rest/httpx/requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,16 @@ const (
)

var (
formUnmarshaler = mapping.NewUnmarshaler(formKey, mapping.WithStringValues(), mapping.WithOpaqueKeys())
pathUnmarshaler = mapping.NewUnmarshaler(pathKey, mapping.WithStringValues(), mapping.WithOpaqueKeys())
validator atomic.Value
formUnmarshaler = mapping.NewUnmarshaler(
formKey,
mapping.WithStringValues(),
mapping.WithOpaqueKeys(),
mapping.WithFromArray())
pathUnmarshaler = mapping.NewUnmarshaler(
pathKey,
mapping.WithStringValues(),
mapping.WithOpaqueKeys())
validator atomic.Value
)

// Validator defines the interface for validating the request.
Expand Down
55 changes: 55 additions & 0 deletions rest/httpx/requests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,61 @@ func TestParseForm(t *testing.T) {
})
}

func TestParseFormArray(t *testing.T) {
t.Run("slice", func(t *testing.T) {
var v struct {
Name []string `form:"name"`
Age []int `form:"age"`
Percent []float64 `form:"percent,optional"`
}

r, err := http.NewRequest(
http.MethodGet,
"/a?name=hello&name=world&age=18&age=19&percent=3.4&percent=4.5",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"hello", "world"}, v.Name)
assert.ElementsMatch(t, []int{18, 19}, v.Age)
assert.ElementsMatch(t, []float64{3.4, 4.5}, v.Percent)
}
})

t.Run("slice with single value", func(t *testing.T) {
var v struct {
Name []string `form:"name"`
Age []int `form:"age"`
Percent []float64 `form:"percent,optional"`
}

r, err := http.NewRequest(
http.MethodGet,
"/a?name=hello&age=18&percent=3.4",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"hello"}, v.Name)
assert.ElementsMatch(t, []int{18}, v.Age)
assert.ElementsMatch(t, []float64{3.4}, v.Percent)
}
})

t.Run("slice with empty and non-empty", func(t *testing.T) {
var v struct {
Name []string `form:"name"`
}

r, err := http.NewRequest(
http.MethodGet,
"/a?name=&name=1",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"", "1"}, v.Name)
}
})
}

func TestParseForm_Error(t *testing.T) {
var v struct {
Name string `form:"name"`
Expand Down
7 changes: 2 additions & 5 deletions rest/httpx/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ func GetFormValues(r *http.Request) (map[string]any, error) {
}

params := make(map[string]any, len(r.Form))
for name := range r.Form {
formValue := r.Form.Get(name)
if len(formValue) > 0 {
params[name] = formValue
}
for name, values := range r.Form {
params[name] = values
}

return params, nil
Expand Down
5 changes: 5 additions & 0 deletions rest/internal/starter.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,18 @@ func start(host string, port int, handler http.Handler, run func(svr *http.Serve

waitForCalled := proc.AddShutdownListener(func() {
healthManager.MarkNotReady()
fmt.Println("calling shutdown")
if e := server.Shutdown(context.Background()); e != nil {
logx.Error(e)
}
fmt.Println("called shutdown")
})
defer func() {
fmt.Println("1")
if errors.Is(err, http.ErrServerClosed) {
fmt.Println("2")
waitForCalled()
fmt.Println("3")
}
}()

Expand Down
4 changes: 3 additions & 1 deletion tools/goctl/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module github.com/zeromicro/go-zero/tools/goctl

go 1.20
go 1.21

toolchain go1.22.5

require (
github.com/DATA-DOG/go-sqlmock v1.5.2
Expand Down
4 changes: 4 additions & 0 deletions zrpc/internal/rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,14 @@ func (s *rpcServer) Start(register RegisterFn) error {
// we need to make sure all others are wrapped up,
// so we do graceful stop at shutdown phase instead of wrap up phase
waitForCalled := proc.AddShutdownListener(func() {
fmt.Println("***1")
if s.health != nil {
s.health.Shutdown()
}
fmt.Println("***2")
server.GracefulStop()
// server.Stop()
fmt.Println("***3")
})
defer waitForCalled()

Expand Down

0 comments on commit b9b8cfd

Please sign in to comment.