Merge branch 'master' into master

This commit is contained in:
Maizi 2025-01-03 12:14:43 +08:00 committed by GitHub
commit 575f883f19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 694 additions and 140 deletions

View File

@ -18,6 +18,7 @@ import (
) )
const ( const (
comma = ","
defaultKeyName = "key" defaultKeyName = "key"
delimiter = '.' delimiter = '.'
ignoreKey = "-" ignoreKey = "-"
@ -36,6 +37,7 @@ var (
defaultCacheLock sync.Mutex defaultCacheLock sync.Mutex
emptyMap = map[string]any{} emptyMap = map[string]any{}
emptyValue = reflect.ValueOf(lang.Placeholder) emptyValue = reflect.ValueOf(lang.Placeholder)
stringSliceType = reflect.TypeOf([]string{})
) )
type ( type (
@ -80,40 +82,11 @@ func (u *Unmarshaler) Unmarshal(i, v any) error {
return u.unmarshal(i, v, "") return u.unmarshal(i, v, "")
} }
func (u *Unmarshaler) unmarshal(i, v any, fullName string) error {
valueType := reflect.TypeOf(v)
if valueType.Kind() != reflect.Ptr {
return errValueNotSettable
}
elemType := Deref(valueType)
switch iv := i.(type) {
case map[string]any:
if elemType.Kind() != reflect.Struct {
return errTypeMismatch
}
return u.unmarshalValuer(mapValuer(iv), v, fullName)
case []any:
if elemType.Kind() != reflect.Slice {
return errTypeMismatch
}
return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName)
default:
return errUnsupportedType
}
}
// UnmarshalValuer unmarshals m into v. // UnmarshalValuer unmarshals m into v.
func (u *Unmarshaler) UnmarshalValuer(m Valuer, v any) error { func (u *Unmarshaler) UnmarshalValuer(m Valuer, v any) error {
return u.unmarshalValuer(simpleValuer{current: m}, v, "") return u.unmarshalValuer(simpleValuer{current: m}, v, "")
} }
func (u *Unmarshaler) unmarshalValuer(m Valuer, v any, fullName string) error {
return u.unmarshalWithFullName(simpleValuer{current: m}, v, fullName)
}
func (u *Unmarshaler) fillMap(fieldType reflect.Type, value reflect.Value, func (u *Unmarshaler) fillMap(fieldType reflect.Type, value reflect.Value,
mapValue any, fullName string) error { mapValue any, fullName string) error {
if !value.CanSet() { if !value.CanSet() {
@ -173,13 +146,18 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
baseType := fieldType.Elem() baseType := fieldType.Elem()
dereffedBaseType := Deref(baseType) dereffedBaseType := Deref(baseType)
dereffedBaseKind := dereffedBaseType.Kind() dereffedBaseKind := dereffedBaseType.Kind()
conv := reflect.MakeSlice(reflect.SliceOf(baseType), refValue.Len(), refValue.Cap())
if refValue.Len() == 0 { if refValue.Len() == 0 {
value.Set(conv) value.Set(reflect.MakeSlice(reflect.SliceOf(baseType), 0, 0))
return nil return nil
} }
if u.opts.fromArray {
refValue = makeStringSlice(refValue)
}
var valid bool var valid bool
conv := reflect.MakeSlice(reflect.SliceOf(baseType), refValue.Len(), refValue.Cap())
for i := 0; i < refValue.Len(); i++ { for i := 0; i < refValue.Len(); i++ {
ithValue := refValue.Index(i).Interface() ithValue := refValue.Index(i).Interface()
if ithValue == nil { if ithValue == nil {
@ -191,17 +169,9 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
switch dereffedBaseKind { switch dereffedBaseKind {
case reflect.Struct: case reflect.Struct:
target := reflect.New(dereffedBaseType) if err := u.fillStructElement(baseType, conv.Index(i), ithValue, sliceFullName); err != nil {
val, ok := ithValue.(map[string]any)
if !ok {
return errTypeMismatch
}
if err := u.unmarshal(val, target.Interface(), sliceFullName); err != nil {
return err return err
} }
SetValue(fieldType.Elem(), conv.Index(i), target.Elem())
case reflect.Slice: case reflect.Slice:
if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName); err != nil { if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName); err != nil {
return err return err
@ -236,7 +206,7 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
return errUnsupportedType return errUnsupportedType
} }
baseFieldType := Deref(fieldType.Elem()) baseFieldType := fieldType.Elem()
baseFieldKind := baseFieldType.Kind() baseFieldKind := baseFieldType.Kind()
conv := reflect.MakeSlice(reflect.SliceOf(baseFieldType), len(slice), cap(slice)) conv := reflect.MakeSlice(reflect.SliceOf(baseFieldType), len(slice), cap(slice))
@ -257,29 +227,39 @@ func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
} }
ithVal := slice.Index(index) ithVal := slice.Index(index)
ithValType := ithVal.Type()
switch v := value.(type) { switch v := value.(type) {
case fmt.Stringer: case fmt.Stringer:
return setValueFromString(baseKind, ithVal, v.String()) return setValueFromString(baseKind, ithVal, v.String())
case string: case string:
return setValueFromString(baseKind, ithVal, v) return setValueFromString(baseKind, ithVal, v)
case map[string]any: case map[string]any:
return u.fillMap(ithVal.Type(), ithVal, value, fullName) // deref to handle both pointer and non-pointer types.
switch Deref(ithValType).Kind() {
case reflect.Struct:
return u.fillStructElement(ithValType, ithVal, v, fullName)
case reflect.Map:
return u.fillMap(ithValType, ithVal, value, fullName)
default:
return errTypeMismatch
}
default: default:
// don't need to consider the difference between int, int8, int16, int32, int64, // don't need to consider the difference between int, int8, int16, int32, int64,
// uint, uint8, uint16, uint32, uint64, because they're handled as json.Number. // uint, uint8, uint16, uint32, uint64, because they're handled as json.Number.
if ithVal.Kind() == reflect.Ptr { if ithVal.Kind() == reflect.Ptr {
baseType := Deref(ithVal.Type()) baseType := Deref(ithValType)
if !reflect.TypeOf(value).AssignableTo(baseType) { if !reflect.TypeOf(value).AssignableTo(baseType) {
return errTypeMismatch return errTypeMismatch
} }
target := reflect.New(baseType).Elem() target := reflect.New(baseType).Elem()
target.Set(reflect.ValueOf(value)) target.Set(reflect.ValueOf(value))
SetValue(ithVal.Type(), ithVal, target) SetValue(ithValType, ithVal, target)
return nil return nil
} }
if !reflect.TypeOf(value).AssignableTo(ithVal.Type()) { if !reflect.TypeOf(value).AssignableTo(ithValType) {
return errTypeMismatch return errTypeMismatch
} }
@ -310,6 +290,23 @@ func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value refle
return u.fillSlice(derefedType, value, slice, fullName) return u.fillSlice(derefedType, value, slice, fullName)
} }
func (u *Unmarshaler) fillStructElement(baseType reflect.Type, target reflect.Value,
value any, fullName string) error {
val, ok := value.(map[string]any)
if !ok {
return errTypeMismatch
}
// use Deref(baseType) to get the base type in case the type is a pointer type.
ptr := reflect.New(Deref(baseType))
if err := u.unmarshal(val, ptr.Interface(), fullName); err != nil {
return err
}
SetValue(baseType, target, ptr.Elem())
return nil
}
func (u *Unmarshaler) fillUnmarshalerStruct(fieldType reflect.Type, func (u *Unmarshaler) fillUnmarshalerStruct(fieldType reflect.Type,
value reflect.Value, targetValue string) error { value reflect.Value, targetValue string) error {
if !value.CanSet() { if !value.CanSet() {
@ -952,6 +949,35 @@ func (u *Unmarshaler) processNamedFieldWithoutValue(fieldType reflect.Type, valu
return nil return nil
} }
func (u *Unmarshaler) unmarshal(i, v any, fullName string) error {
valueType := reflect.TypeOf(v)
if valueType.Kind() != reflect.Ptr {
return errValueNotSettable
}
elemType := Deref(valueType)
switch iv := i.(type) {
case map[string]any:
if elemType.Kind() != reflect.Struct {
return errTypeMismatch
}
return u.unmarshalValuer(mapValuer(iv), v, fullName)
case []any:
if elemType.Kind() != reflect.Slice {
return errTypeMismatch
}
return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName)
default:
return errUnsupportedType
}
}
func (u *Unmarshaler) unmarshalValuer(m Valuer, v any, fullName string) error {
return u.unmarshalWithFullName(simpleValuer{current: m}, v, fullName)
}
func (u *Unmarshaler) unmarshalWithFullName(m valuerWithParent, v any, fullName string) error { func (u *Unmarshaler) unmarshalWithFullName(m valuerWithParent, v any, fullName string) error {
rv := reflect.ValueOf(v) rv := reflect.ValueOf(v)
if err := ValidatePtr(rv); err != nil { if err := ValidatePtr(rv); err != nil {
@ -1146,6 +1172,35 @@ func join(elem ...string) string {
return builder.String() return builder.String()
} }
func makeStringSlice(refValue reflect.Value) reflect.Value {
if refValue.Len() != 1 {
return refValue
}
element := refValue.Index(0)
if element.Kind() != reflect.String {
return refValue
}
val, ok := element.Interface().(string)
if !ok {
return refValue
}
splits := strings.Split(val, comma)
if len(splits) <= 1 {
return refValue
}
slice := reflect.MakeSlice(stringSliceType, len(splits), len(splits))
for i, split := range splits {
// allow empty strings
slice.Index(i).Set(reflect.ValueOf(split))
}
return slice
}
func newInitError(name string) error { func newInitError(name string) error {
return fmt.Errorf("field %q is not set", name) return fmt.Errorf("field %q is not set", name)
} }

View File

@ -351,7 +351,7 @@ func TestUnmarshalIntSliceOfPtr(t *testing.T) {
assert.Error(t, UnmarshalKey(m, &in)) assert.Error(t, UnmarshalKey(m, &in))
}) })
t.Run("int slice with nil", func(t *testing.T) { t.Run("int slice with nil element", func(t *testing.T) {
type inner struct { type inner struct {
Ints []int `key:"ints"` Ints []int `key:"ints"`
} }
@ -365,6 +365,21 @@ func TestUnmarshalIntSliceOfPtr(t *testing.T) {
assert.Empty(t, in.Ints) assert.Empty(t, in.Ints)
} }
}) })
t.Run("int slice with nil", func(t *testing.T) {
type inner struct {
Ints []int `key:"ints"`
}
m := map[string]any{
"ints": []any(nil),
}
var in inner
if assert.NoError(t, UnmarshalKey(m, &in)) {
assert.Empty(t, in.Ints)
}
})
} }
func TestUnmarshalIntWithDefault(t *testing.T) { func TestUnmarshalIntWithDefault(t *testing.T) {
@ -1374,20 +1389,82 @@ func TestUnmarshalWithFloatPtr(t *testing.T) {
} }
func TestUnmarshalIntSlice(t *testing.T) { func TestUnmarshalIntSlice(t *testing.T) {
var v struct { t.Run("int slice from int", func(t *testing.T) {
Ages []int `key:"ages"` var v struct {
Slice []int `key:"slice"` Ages []int `key:"ages"`
} Slice []int `key:"slice"`
m := map[string]any{ }
"ages": []int{1, 2}, m := map[string]any{
"slice": []any{}, "ages": []int{1, 2},
} "slice": []any{},
}
ast := assert.New(t) ast := assert.New(t)
if ast.NoError(UnmarshalKey(m, &v)) { if ast.NoError(UnmarshalKey(m, &v)) {
ast.ElementsMatch([]int{1, 2}, v.Ages) ast.ElementsMatch([]int{1, 2}, v.Ages)
ast.Equal([]int{}, v.Slice) ast.Equal([]int{}, v.Slice)
} }
})
t.Run("int slice from one int", func(t *testing.T) {
var v struct {
Ages []int `key:"ages"`
}
m := map[string]any{
"ages": []int{2},
}
ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
ast.ElementsMatch([]int{2}, v.Ages)
}
})
t.Run("int slice from one int string", func(t *testing.T) {
var v struct {
Ages []int `key:"ages"`
}
m := map[string]any{
"ages": []string{"2"},
}
ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
ast.ElementsMatch([]int{2}, v.Ages)
}
})
t.Run("int slice from one json.Number", func(t *testing.T) {
var v struct {
Ages []int `key:"ages"`
}
m := map[string]any{
"ages": []json.Number{"2"},
}
ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
ast.ElementsMatch([]int{2}, v.Ages)
}
})
t.Run("int slice from one int strings", func(t *testing.T) {
var v struct {
Ages []int `key:"ages"`
}
m := map[string]any{
"ages": []string{"1,2"},
}
ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
ast.ElementsMatch([]int{1, 2}, v.Ages)
}
})
} }
func TestUnmarshalString(t *testing.T) { func TestUnmarshalString(t *testing.T) {
@ -1442,6 +1519,36 @@ func TestUnmarshalStringSliceFromString(t *testing.T) {
} }
}) })
t.Run("slice from empty string", func(t *testing.T) {
var v struct {
Names []string `key:"names"`
}
m := map[string]any{
"names": []string{""},
}
ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
ast.ElementsMatch([]string{""}, v.Names)
}
})
t.Run("slice from empty and valid string", func(t *testing.T) {
var v struct {
Names []string `key:"names"`
}
m := map[string]any{
"names": []string{","},
}
ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
ast.ElementsMatch([]string{"", ""}, v.Names)
}
})
t.Run("slice from string with slice error", func(t *testing.T) { t.Run("slice from string with slice error", func(t *testing.T) {
var v struct { var v struct {
Names []int `key:"names"` Names []int `key:"names"`
@ -5862,6 +5969,38 @@ func TestUnmarshal_Unmarshaler(t *testing.T) {
}) })
} }
func TestParseJsonStringValue(t *testing.T) {
t.Run("string", func(t *testing.T) {
type GoodsInfo struct {
Sku int64 `json:"sku,optional"`
}
type GetReq struct {
GoodsList []*GoodsInfo `json:"goods_list"`
}
input := map[string]any{"goods_list": "[{\"sku\":11},{\"sku\":22}]"}
var v GetReq
assert.NotPanics(t, func() {
assert.NoError(t, UnmarshalJsonMap(input, &v))
assert.Equal(t, 2, len(v.GoodsList))
assert.ElementsMatch(t, []int64{11, 22}, []int64{v.GoodsList[0].Sku, v.GoodsList[1].Sku})
})
})
t.Run("string with invalid type", func(t *testing.T) {
type GetReq struct {
GoodsList []*int `json:"goods_list"`
}
input := map[string]any{"goods_list": "[{\"sku\":11},{\"sku\":22}]"}
var v GetReq
assert.NotPanics(t, func() {
assert.Error(t, UnmarshalJsonMap(input, &v))
})
})
}
func BenchmarkDefaultValue(b *testing.B) { func BenchmarkDefaultValue(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
var a struct { var a struct {

View File

@ -4,6 +4,9 @@ package proc
import "time" import "time"
// ShutdownConf is empty on windows.
type ShutdownConf struct{}
// AddShutdownListener returns fn itself on windows, lets callers call fn on their own. // AddShutdownListener returns fn itself on windows, lets callers call fn on their own.
func AddShutdownListener(fn func()) func() { func AddShutdownListener(fn func()) func() {
return fn return fn
@ -18,6 +21,10 @@ func AddWrapUpListener(fn func()) func() {
func SetTimeToForceQuit(duration time.Duration) { func SetTimeToForceQuit(duration time.Duration) {
} }
// Setup does nothing on windows.
func Setup(conf ShutdownConf) {
}
// Shutdown does nothing on windows. // Shutdown does nothing on windows.
func Shutdown() { func Shutdown() {
} }

View File

@ -14,17 +14,29 @@ import (
) )
const ( const (
wrapUpTime = time.Second // defaultWrapUpTime is the default time to wait before calling wrap up listeners.
// why we use 5500 milliseconds is because most of our queue are blocking mode with 5 seconds defaultWrapUpTime = time.Second
waitTime = 5500 * time.Millisecond // defaultWaitTime is the default time to wait before force quitting.
// why we use 5500 milliseconds is because most of our queues are blocking mode with 5 seconds
defaultWaitTime = 5500 * time.Millisecond
) )
var ( var (
wrapUpListeners = new(listenerManager) wrapUpListeners = new(listenerManager)
shutdownListeners = new(listenerManager) shutdownListeners = new(listenerManager)
delayTimeBeforeForceQuit = waitTime wrapUpTime = defaultWrapUpTime
waitTime = defaultWaitTime
shutdownLock sync.Mutex
) )
// ShutdownConf defines the shutdown configuration for the process.
type ShutdownConf struct {
// WrapUpTime is the time to wait before calling shutdown listeners.
WrapUpTime time.Duration `json:",default=1s"`
// WaitTime is the time to wait before force quitting.
WaitTime time.Duration `json:",default=5.5s"`
}
// AddShutdownListener adds fn as a shutdown listener. // AddShutdownListener adds fn as a shutdown listener.
// The returned func can be used to wait for fn getting called. // The returned func can be used to wait for fn getting called.
func AddShutdownListener(fn func()) (waitForCalled func()) { func AddShutdownListener(fn func()) (waitForCalled func()) {
@ -39,7 +51,21 @@ func AddWrapUpListener(fn func()) (waitForCalled func()) {
// SetTimeToForceQuit sets the waiting time before force quitting. // SetTimeToForceQuit sets the waiting time before force quitting.
func SetTimeToForceQuit(duration time.Duration) { func SetTimeToForceQuit(duration time.Duration) {
delayTimeBeforeForceQuit = duration shutdownLock.Lock()
defer shutdownLock.Unlock()
waitTime = duration
}
func Setup(conf ShutdownConf) {
shutdownLock.Lock()
defer shutdownLock.Unlock()
if conf.WrapUpTime > 0 {
wrapUpTime = conf.WrapUpTime
}
if conf.WaitTime > 0 {
waitTime = conf.WaitTime
}
} }
// Shutdown calls the registered shutdown listeners, only for test purpose. // Shutdown calls the registered shutdown listeners, only for test purpose.
@ -61,8 +87,12 @@ func gracefulStop(signals chan os.Signal, sig syscall.Signal) {
time.Sleep(wrapUpTime) time.Sleep(wrapUpTime)
go shutdownListeners.notifyListeners() go shutdownListeners.notifyListeners()
time.Sleep(delayTimeBeforeForceQuit - wrapUpTime) shutdownLock.Lock()
logx.Infof("Still alive after %v, going to force kill the process...", delayTimeBeforeForceQuit) remainingTime := waitTime - wrapUpTime
shutdownLock.Unlock()
time.Sleep(remainingTime)
logx.Infof("Still alive after %v, going to force kill the process...", waitTime)
_ = syscall.Kill(syscall.Getpid(), sig) _ = syscall.Kill(syscall.Getpid(), sig)
} }
@ -82,6 +112,9 @@ func (lm *listenerManager) addListener(fn func()) (waitForCalled func()) {
}) })
lm.lock.Unlock() lm.lock.Unlock()
// we can return lm.waitGroup.Wait directly,
// but we want to make the returned func more readable.
// creating an extra closure would be negligible in practice.
return func() { return func() {
lm.waitGroup.Wait() lm.waitGroup.Wait()
} }

View File

@ -3,6 +3,7 @@
package proc package proc
import ( import (
"sync/atomic"
"testing" "testing"
"time" "time"
@ -10,8 +11,12 @@ import (
) )
func TestShutdown(t *testing.T) { func TestShutdown(t *testing.T) {
t.Cleanup(restoreSettings)
SetTimeToForceQuit(time.Hour) SetTimeToForceQuit(time.Hour)
assert.Equal(t, time.Hour, delayTimeBeforeForceQuit) shutdownLock.Lock()
assert.Equal(t, time.Hour, waitTime)
shutdownLock.Unlock()
var val int var val int
called := AddWrapUpListener(func() { called := AddWrapUpListener(func() {
@ -29,7 +34,53 @@ func TestShutdown(t *testing.T) {
assert.Equal(t, 3, val) assert.Equal(t, 3, val)
} }
func TestShutdownWithMultipleServices(t *testing.T) {
t.Cleanup(restoreSettings)
SetTimeToForceQuit(time.Hour)
shutdownLock.Lock()
assert.Equal(t, time.Hour, waitTime)
shutdownLock.Unlock()
var val int32
called1 := AddShutdownListener(func() {
atomic.AddInt32(&val, 1)
})
called2 := AddShutdownListener(func() {
atomic.AddInt32(&val, 2)
})
Shutdown()
called1()
called2()
assert.Equal(t, int32(3), atomic.LoadInt32(&val))
}
func TestWrapUpWithMultipleServices(t *testing.T) {
t.Cleanup(restoreSettings)
SetTimeToForceQuit(time.Hour)
shutdownLock.Lock()
assert.Equal(t, time.Hour, waitTime)
shutdownLock.Unlock()
var val int32
called1 := AddWrapUpListener(func() {
atomic.AddInt32(&val, 1)
})
called2 := AddWrapUpListener(func() {
atomic.AddInt32(&val, 2)
})
WrapUp()
called1()
called2()
assert.Equal(t, int32(3), atomic.LoadInt32(&val))
}
func TestNotifyMoreThanOnce(t *testing.T) { func TestNotifyMoreThanOnce(t *testing.T) {
t.Cleanup(restoreSettings)
ch := make(chan struct{}, 1) ch := make(chan struct{}, 1)
go func() { go func() {
@ -58,3 +109,38 @@ func TestNotifyMoreThanOnce(t *testing.T) {
t.Fatal("timeout, check error logs") t.Fatal("timeout, check error logs")
} }
} }
func TestSetup(t *testing.T) {
t.Run("valid time", func(t *testing.T) {
defer restoreSettings()
Setup(ShutdownConf{
WrapUpTime: time.Second * 2,
WaitTime: time.Second * 30,
})
shutdownLock.Lock()
assert.Equal(t, time.Second*2, wrapUpTime)
assert.Equal(t, time.Second*30, waitTime)
shutdownLock.Unlock()
})
t.Run("valid time", func(t *testing.T) {
defer restoreSettings()
Setup(ShutdownConf{})
shutdownLock.Lock()
assert.Equal(t, defaultWrapUpTime, wrapUpTime)
assert.Equal(t, defaultWaitTime, waitTime)
shutdownLock.Unlock()
})
}
func restoreSettings() {
shutdownLock.Lock()
defer shutdownLock.Unlock()
wrapUpTime = defaultWrapUpTime
waitTime = defaultWaitTime
}

View File

@ -37,6 +37,7 @@ type (
Prometheus prometheus.Config `json:",optional"` Prometheus prometheus.Config `json:",optional"`
Telemetry trace.Config `json:",optional"` Telemetry trace.Config `json:",optional"`
DevServer DevServerConfig `json:",optional"` DevServer DevServerConfig `json:",optional"`
Shutdown proc.ShutdownConf `json:",optional"`
} }
) )
@ -61,6 +62,7 @@ func (sc ServiceConf) SetUp() error {
sc.Telemetry.Name = sc.Name sc.Telemetry.Name = sc.Name
} }
trace.StartAgent(sc.Telemetry) trace.StartAgent(sc.Telemetry)
proc.Setup(sc.Shutdown)
proc.AddShutdownListener(func() { proc.AddShutdownListener(func() {
trace.StopAgent() trace.StopAgent()
}) })

View File

@ -76,9 +76,14 @@ func (sg *ServiceGroup) doStart() {
} }
func (sg *ServiceGroup) doStop() { func (sg *ServiceGroup) doStop() {
group := threading.NewRoutineGroup()
for _, service := range sg.services { 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. // WithStart wraps a start func as a Service.

View File

@ -11,6 +11,7 @@ import (
"github.com/jhump/protoreflect/grpcreflect" "github.com/jhump/protoreflect/grpcreflect"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/mr" "github.com/zeromicro/go-zero/core/mr"
"github.com/zeromicro/go-zero/core/threading"
"github.com/zeromicro/go-zero/gateway/internal" "github.com/zeromicro/go-zero/gateway/internal"
"github.com/zeromicro/go-zero/rest" "github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/rest/httpx" "github.com/zeromicro/go-zero/rest/httpx"
@ -23,6 +24,7 @@ type (
Server struct { Server struct {
*rest.Server *rest.Server
upstreams []Upstream upstreams []Upstream
conns []zrpc.Client
processHeader func(http.Header) []string processHeader func(http.Header) []string
dialer func(conf zrpc.RpcClientConf) zrpc.Client dialer func(conf zrpc.RpcClientConf) zrpc.Client
} }
@ -51,8 +53,24 @@ func (s *Server) Start() {
} }
// Stop stops the gateway server. // Stop stops the gateway server.
// To get a graceful shutdown, it stops the HTTP server first, then closes gRPC connections.
func (s *Server) Stop() { func (s *Server) Stop() {
// stop the HTTP server first, then close gRPC connections.
// in case the gRPC server is stopped first,
// the HTTP server may still be running to accept requests.
s.Server.Stop() s.Server.Stop()
group := threading.NewRoutineGroup()
for _, conn := range s.conns {
// new variable to avoid closure problems, can be removed after go 1.22
// see https://golang.org/doc/faq#closures_and_goroutines
conn := conn
group.Run(func() {
// ignore the error when closing the connection
_ = conn.Conn().Close()
})
}
group.Wait()
} }
func (s *Server) build() error { func (s *Server) build() error {
@ -71,6 +89,7 @@ func (s *Server) build() error {
} else { } else {
cli = zrpc.MustNewClient(up.Grpc) cli = zrpc.MustNewClient(up.Grpc)
} }
s.conns = append(s.conns, cli)
source, err := s.createDescriptorSource(cli, up) source, err := s.createDescriptorSource(cli, up)
if err != nil { if err != nil {

View File

@ -46,7 +46,7 @@ func dialer() func(context.Context, string) (net.Conn, error) {
func TestMustNewServer(t *testing.T) { func TestMustNewServer(t *testing.T) {
var c GatewayConf var c GatewayConf
assert.NoError(t, conf.FillDefault(&c)) assert.NoError(t, conf.FillDefault(&c))
// avoid popup alert on macos for asking permissions // avoid popup alert on MacOS for asking permissions
c.DevServer.Host = "localhost" c.DevServer.Host = "localhost"
c.Host = "localhost" c.Host = "localhost"
c.Port = 18881 c.Port = 18881

6
go.mod
View File

@ -4,7 +4,7 @@ go 1.20
require ( require (
github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/alicebob/miniredis/v2 v2.33.0 github.com/alicebob/miniredis/v2 v2.34.0
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
github.com/fullstorydev/grpcurl v1.9.2 github.com/fullstorydev/grpcurl v1.9.2
github.com/go-sql-driver/mysql v1.8.1 github.com/go-sql-driver/mysql v1.8.1
@ -33,12 +33,12 @@ require (
go.opentelemetry.io/otel/trace v1.24.0 go.opentelemetry.io/otel/trace v1.24.0
go.uber.org/automaxprocs v1.6.0 go.uber.org/automaxprocs v1.6.0
go.uber.org/goleak v1.3.0 go.uber.org/goleak v1.3.0
golang.org/x/net v0.32.0 golang.org/x/net v0.33.0
golang.org/x/sys v0.28.0 golang.org/x/sys v0.28.0
golang.org/x/time v0.8.0 golang.org/x/time v0.8.0
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d
google.golang.org/grpc v1.65.0 google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.36.0 google.golang.org/protobuf v1.36.1
gopkg.in/cheggaaa/pb.v1 v1.0.28 gopkg.in/cheggaaa/pb.v1 v1.0.28
gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/h2non/gock.v1 v1.1.2
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0

12
go.sum
View File

@ -4,8 +4,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA= github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0=
github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0= github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
@ -242,8 +242,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -295,8 +295,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@ -300,6 +300,7 @@ go-zero 已被许多公司用于生产部署,接入场景如在线教育、电
>101. 上海巨瓴科技有限公司 >101. 上海巨瓴科技有限公司
>102. 深圳市兴海物联科技有限公司 >102. 深圳市兴海物联科技有限公司
>103. 爱芯元智半导体股份有限公司 >103. 爱芯元智半导体股份有限公司
>104. 杭州升恒科技有限公司
如果贵公司也已使用 go-zero欢迎在 [登记地址](https://github.com/zeromicro/go-zero/issues/602) 登记,仅仅为了推广,不做其它用途。 如果贵公司也已使用 go-zero欢迎在 [登记地址](https://github.com/zeromicro/go-zero/issues/602) 登记,仅仅为了推广,不做其它用途。

View File

@ -158,9 +158,9 @@ func logDetails(r *http.Request, response *detailLoggedResponseWriter, timer *ut
logger := logx.WithContext(r.Context()) logger := logx.WithContext(r.Context())
buf.WriteString(fmt.Sprintf("[HTTP] %s - %d - %s - %s\n=> %s\n", buf.WriteString(fmt.Sprintf("[HTTP] %s - %d - %s - %s\n=> %s\n",
r.Method, code, r.RemoteAddr, timex.ReprOfDuration(duration), dumpRequest(r))) r.Method, code, r.RemoteAddr, timex.ReprOfDuration(duration), dumpRequest(r)))
if duration > defaultSlowThreshold { if duration > slowThreshold.Load() {
logger.Slowf("[HTTP] %s - %d - %s - slowcall(%s)\n=> %s\n", r.Method, code, r.RemoteAddr, logger.Slowf("[HTTP] %s - %d - %s - slowcall(%s)\n=> %s\n", r.Method, code, r.RemoteAddr,
fmt.Sprintf("slowcall(%s)", timex.ReprOfDuration(duration)), dumpRequest(r)) timex.ReprOfDuration(duration), dumpRequest(r))
} }
body := logs.Flush() body := logs.Flush()

View File

@ -88,6 +88,36 @@ func TestParseFormArray(t *testing.T) {
} }
}) })
t.Run("slice with empty", func(t *testing.T) {
var v struct {
Name []string `form:"name,optional"`
}
r, err := http.NewRequest(
http.MethodGet,
"/a",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{}, v.Name)
}
})
t.Run("slice with empty", func(t *testing.T) {
var v struct {
Name []string `form:"name,optional"`
}
r, err := http.NewRequest(
http.MethodGet,
"/a?name=",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{""}, v.Name)
}
})
t.Run("slice with empty and non-empty", func(t *testing.T) { t.Run("slice with empty and non-empty", func(t *testing.T) {
var v struct { var v struct {
Name []string `form:"name"` Name []string `form:"name"`
@ -99,7 +129,67 @@ func TestParseFormArray(t *testing.T) {
http.NoBody) http.NoBody)
assert.NoError(t, err) assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) { if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"1"}, v.Name) assert.ElementsMatch(t, []string{"", "1"}, v.Name)
}
})
t.Run("slice with one value on array format", func(t *testing.T) {
var v struct {
Names []string `form:"names"`
}
r, err := http.NewRequest(
http.MethodGet,
"/a?names=1,2,3",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"1", "2", "3"}, v.Names)
}
})
t.Run("slice with one value on combined array format", func(t *testing.T) {
var v struct {
Names []string `form:"names"`
}
r, err := http.NewRequest(
http.MethodGet,
"/a?names=[1,2,3]&names=4",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"[1,2,3]", "4"}, v.Names)
}
})
t.Run("slice with one value on integer array format", func(t *testing.T) {
var v struct {
Numbers []int `form:"numbers"`
}
r, err := http.NewRequest(
http.MethodGet,
"/a?numbers=1,2,3",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []int{1, 2, 3}, v.Numbers)
}
})
t.Run("slice with one value on array format brackets", func(t *testing.T) {
var v struct {
Names []string `form:"names"`
}
r, err := http.NewRequest(
http.MethodGet,
"/a?names[]=1&names[]=2&names[]=3",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"1", "2", "3"}, v.Names)
} }
}) })
} }
@ -528,6 +618,26 @@ func TestCustomUnmarshalerStructRequest(t *testing.T) {
assert.Equal(t, "hello", v.Foo.Name) assert.Equal(t, "hello", v.Foo.Name)
} }
func TestParseJsonStringRequest(t *testing.T) {
type GoodsInfo struct {
Sku int64 `json:"sku,optional"`
}
type GetReq struct {
GoodsList []*GoodsInfo `json:"goods_list"`
}
input := `{"goods_list":"[{\"sku\":11},{\"sku\":22}]"}`
r := httptest.NewRequest(http.MethodPost, "/a", strings.NewReader(input))
r.Header.Set(ContentType, JsonContentType)
var v GetReq
assert.NotPanics(t, func() {
assert.NoError(t, Parse(r, &v))
assert.Equal(t, 2, len(v.GoodsList))
assert.ElementsMatch(t, []int64{11, 22}, []int64{v.GoodsList[0].Sku, v.GoodsList[1].Sku})
})
}
func BenchmarkParseRaw(b *testing.B) { func BenchmarkParseRaw(b *testing.B) {
r, err := http.NewRequest(http.MethodGet, "http://hello.com/a?name=hello&age=18&percent=3.4", http.NoBody) r, err := http.NewRequest(http.MethodGet, "http://hello.com/a?name=hello&age=18&percent=3.4", http.NoBody)
if err != nil { if err != nil {

View File

@ -2,12 +2,23 @@ package httpx
import ( import (
"errors" "errors"
"fmt"
"net/http" "net/http"
"strings"
) )
const xForwardedFor = "X-Forwarded-For" const (
xForwardedFor = "X-Forwarded-For"
arraySuffix = "[]"
// most servers and clients have a limit of 8192 bytes (8 KB)
// one parameter at least take 4 chars, for example `?a=b&c=d`
maxFormParamCount = 2048
)
// GetFormValues returns the form values. // GetFormValues returns the form values supporting three array notation formats:
// 1. Standard notation: /api?names=alice&names=bob
// 2. Comma notation: /api?names=alice,bob
// 3. Bracket notation: /api?names[]=alice&names[]=bob
func GetFormValues(r *http.Request) (map[string]any, error) { func GetFormValues(r *http.Request) (map[string]any, error) {
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
return nil, err return nil, err
@ -19,16 +30,23 @@ func GetFormValues(r *http.Request) (map[string]any, error) {
} }
} }
var n int
params := make(map[string]any, len(r.Form)) params := make(map[string]any, len(r.Form))
for name, values := range r.Form { for name, values := range r.Form {
filtered := make([]string, 0, len(values)) filtered := make([]string, 0, len(values))
for _, v := range values { for _, v := range values {
if len(v) > 0 { if n < maxFormParamCount {
filtered = append(filtered, v) filtered = append(filtered, v)
n++
} else {
return nil, fmt.Errorf("too many form values, error: %s", r.Form.Encode())
} }
} }
if len(filtered) > 0 { if len(filtered) > 0 {
if strings.HasSuffix(name, arraySuffix) {
name = name[:len(name)-2]
}
params[name] = filtered params[name] = filtered
} }
} }

View File

@ -1,7 +1,9 @@
package httpx package httpx
import ( import (
"fmt"
"net/http" "net/http"
"net/url"
"strings" "strings"
"testing" "testing"
@ -23,3 +25,23 @@ func TestGetRemoteAddrNoHeader(t *testing.T) {
assert.True(t, len(GetRemoteAddr(r)) == 0) assert.True(t, len(GetRemoteAddr(r)) == 0)
} }
func TestGetFormValues_TooManyValues(t *testing.T) {
form := url.Values{}
// Add more values than the limit
for i := 0; i < maxFormParamCount+10; i++ {
form.Add("param", fmt.Sprintf("value%d", i))
}
// Create a new request with the form data
req, err := http.NewRequest("POST", "/test", strings.NewReader(form.Encode()))
assert.NoError(t, err)
// Set the content type for form data
req.Header.Set(ContentType, "application/x-www-form-urlencoded")
_, err = GetFormValues(req)
assert.Error(t, err)
assert.Contains(t, err.Error(), "too many form values")
}

View File

@ -516,28 +516,55 @@ func TestParsePtrInRequestEmpty(t *testing.T) {
} }
func TestParseQueryOptional(t *testing.T) { func TestParseQueryOptional(t *testing.T) {
r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=", nil) t.Run("optional with string", func(t *testing.T) {
assert.Nil(t, err) r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=", nil)
assert.Nil(t, err)
router := NewRouter() router := NewRouter()
err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc( err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
v := struct { v := struct {
Nickname string `form:"nickname"` Nickname string `form:"nickname"`
Zipcode int64 `form:"zipcode,optional"` Zipcode string `form:"zipcode,optional"`
}{} }{}
err = httpx.Parse(r, &v) err = httpx.Parse(r, &v)
assert.Nil(t, err) assert.Nil(t, err)
_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Nickname, v.Zipcode)) _, err = io.WriteString(w, fmt.Sprintf("%s:%s", v.Nickname, v.Zipcode))
assert.Nil(t, err) assert.Nil(t, err)
})) }))
assert.Nil(t, err) assert.Nil(t, err)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
router.ServeHTTP(rr, r) router.ServeHTTP(rr, r)
assert.Equal(t, "whatever:0", rr.Body.String()) assert.Equal(t, "whatever:", rr.Body.String())
})
t.Run("optional with int", func(t *testing.T) {
r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever", nil)
assert.Nil(t, err)
router := NewRouter()
err = router.Handle(http.MethodGet, "/:name/:year", http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
v := struct {
Nickname string `form:"nickname"`
Zipcode int `form:"zipcode,optional"`
}{}
err = httpx.Parse(r, &v)
assert.Nil(t, err)
_, err = io.WriteString(w, fmt.Sprintf("%s:%d", v.Nickname, v.Zipcode))
assert.Nil(t, err)
}))
assert.Nil(t, err)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, r)
assert.Equal(t, "whatever:0", rr.Body.String())
})
} }
func TestParse(t *testing.T) { func TestParse(t *testing.T) {

View File

@ -139,12 +139,7 @@ rest.WithPrefix("%s"),`, g.prefix)
return err return err
} }
// why we check this, maybe some users set value 1, it's 1ns, not 1s. timeout = fmt.Sprintf("\n rest.WithTimeout(%s),", formatDuration(duration))
if duration < timeoutThreshold {
return fmt.Errorf("timeout should not less than 1ms, now %v", duration)
}
timeout = fmt.Sprintf("\n rest.WithTimeout(%d * time.Millisecond),", duration.Milliseconds())
hasTimeout = true hasTimeout = true
} }
@ -211,6 +206,16 @@ rest.WithPrefix("%s"),`, g.prefix)
}) })
} }
func formatDuration(duration time.Duration) string {
if duration < time.Microsecond {
return fmt.Sprintf("%d * time.Nanosecond", duration.Nanoseconds())
}
if duration < time.Millisecond {
return fmt.Sprintf("%d * time.Microsecond", duration.Microseconds())
}
return fmt.Sprintf("%d * time.Millisecond", duration.Milliseconds())
}
func genRouteImports(parentPkg string, api *spec.ApiSpec) string { func genRouteImports(parentPkg string, api *spec.ApiSpec) string {
importSet := collection.NewSet() importSet := collection.NewSet()
importSet.AddStr(fmt.Sprintf("\"%s\"", pathx.JoinPackages(parentPkg, contextDir))) importSet.AddStr(fmt.Sprintf("\"%s\"", pathx.JoinPackages(parentPkg, contextDir)))

View File

@ -0,0 +1,27 @@
package gogen
import (
"testing"
"time"
)
func Test_formatDuration(t *testing.T) {
tests := []struct {
duration time.Duration
expected string
}{
{0, "0 * time.Nanosecond"},
{time.Nanosecond, "1 * time.Nanosecond"},
{100 * time.Nanosecond, "100 * time.Nanosecond"},
{500 * time.Microsecond, "500 * time.Microsecond"},
{2 * time.Millisecond, "2 * time.Millisecond"},
{time.Second, "1000 * time.Millisecond"},
}
for _, test := range tests {
result := formatDuration(test.duration)
if result != test.expected {
t.Errorf("formatDuration(%v) = %v; want %v", test.duration, result, test.expected)
}
}
}

View File

@ -6,4 +6,4 @@ import (
) )
// Cmd describes a bug command. // Cmd describes a bug command.
var Cmd = cobrax.NewCommand("bug", cobrax.WithRunE(cobra.NoArgs), cobrax.WithArgs(cobra.NoArgs)) var Cmd = cobrax.NewCommand("bug", cobrax.WithRunE(runE), cobrax.WithArgs(cobra.NoArgs))

View File

@ -4,7 +4,7 @@ go 1.20
require ( require (
github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/emicklei/proto v1.13.4 github.com/emicklei/proto v1.14.0
github.com/fatih/structtag v1.2.0 github.com/fatih/structtag v1.2.0
github.com/go-sql-driver/mysql v1.8.1 github.com/go-sql-driver/mysql v1.8.1
github.com/gookit/color v1.5.4 github.com/gookit/color v1.5.4
@ -15,17 +15,17 @@ require (
github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1 github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1
github.com/zeromicro/antlr v0.0.1 github.com/zeromicro/antlr v0.0.1
github.com/zeromicro/ddl-parser v1.0.5 github.com/zeromicro/ddl-parser v1.0.5
github.com/zeromicro/go-zero v1.7.4 github.com/zeromicro/go-zero v1.7.6
golang.org/x/text v0.21.0 golang.org/x/text v0.21.0
google.golang.org/grpc v1.65.0 google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.36.0 google.golang.org/protobuf v1.36.1
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect
github.com/alicebob/miniredis/v2 v2.33.0 // indirect github.com/alicebob/miniredis/v2 v2.34.0 // indirect
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec // indirect github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
@ -94,7 +94,7 @@ require (
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
go.uber.org/zap v1.24.0 // indirect go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.31.0 // indirect golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.31.0 // indirect golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.28.0 // indirect

View File

@ -4,8 +4,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA= github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0=
github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0= github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8=
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec h1:EEyRvzmpEUZ+I8WmD5cw/vY8EqhambkOqy5iFr0908A= github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec h1:EEyRvzmpEUZ+I8WmD5cw/vY8EqhambkOqy5iFr0908A=
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
@ -30,8 +30,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/proto v1.13.4 h1:myn1fyf8t7tAqIzV91Tj9qXpvyXXGXk8OS2H6IBSc9g= github.com/emicklei/proto v1.14.0 h1:WYxC0OrBuuC+FUCTZvb8+fzEHdZMwLEF+OnVfZA3LXU=
github.com/emicklei/proto v1.13.4/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/emicklei/proto v1.14.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
@ -174,8 +174,8 @@ github.com/zeromicro/antlr v0.0.1 h1:CQpIn/dc0pUjgGQ81y98s/NGOm2Hfru2NNio2I9mQgk
github.com/zeromicro/antlr v0.0.1/go.mod h1:nfpjEwFR6Q4xGDJMcZnCL9tEfQRgszMwu3rDz2Z+p5M= github.com/zeromicro/antlr v0.0.1/go.mod h1:nfpjEwFR6Q4xGDJMcZnCL9tEfQRgszMwu3rDz2Z+p5M=
github.com/zeromicro/ddl-parser v1.0.5 h1:LaVqHdzMTjasua1yYpIYaksxKqRzFrEukj2Wi2EbWaQ= github.com/zeromicro/ddl-parser v1.0.5 h1:LaVqHdzMTjasua1yYpIYaksxKqRzFrEukj2Wi2EbWaQ=
github.com/zeromicro/ddl-parser v1.0.5/go.mod h1:ISU/8NuPyEpl9pa17Py9TBPetMjtsiHrb9f5XGiYbo8= github.com/zeromicro/ddl-parser v1.0.5/go.mod h1:ISU/8NuPyEpl9pa17Py9TBPetMjtsiHrb9f5XGiYbo8=
github.com/zeromicro/go-zero v1.7.4 h1:lyIUsqbpVRzM4NmXu5pRM3XrdRdUuWOkQmHiNmJF0VU= github.com/zeromicro/go-zero v1.7.6 h1:SArK4xecdrpVY3ZFJcbc0IZCx+NuWyHNjCv9f1+Gwrc=
github.com/zeromicro/go-zero v1.7.4/go.mod h1:jmv4hTdUBkDn6kxgI+WrKQw0q6LKxDElGPMfCLOeeEY= github.com/zeromicro/go-zero v1.7.6/go.mod h1:SmGykRm5e0Z4CGNj+GaSKDffaHzQV56fel0FkymTLlE=
go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk= go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM= go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM=
go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA= go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA=
@ -226,8 +226,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -271,8 +271,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@ -6,7 +6,7 @@ import (
) )
// BuildVersion is the version of goctl. // BuildVersion is the version of goctl.
const BuildVersion = "1.7.3" const BuildVersion = "1.7.5"
var tag = map[string]int{"pre-alpha": 0, "alpha": 1, "pre-bata": 2, "beta": 3, "released": 4, "": 5} var tag = map[string]int{"pre-alpha": 0, "alpha": 1, "pre-bata": 2, "beta": 3, "released": 4, "": 5}

View File

@ -13,13 +13,13 @@ import (
) )
const ( const (
idAPI = "api" idAPI = "api"
groupKeyText = "group" groupKeyText = "group"
infoTitleKey = "Title" infoTitleKey = "Title"
infoDescKey = "Desc" infoDescKey = "Desc"
infoVersionKey = "Version" infoVersionKey = "Version"
infoAuthorKey = "Author" infoAuthorKey = "Author"
infoEmailKey = "Email" infoEmailKey = "Email"
) )
// Parser is the parser for api file. // Parser is the parser for api file.

View File

@ -305,7 +305,7 @@ func TestParser_Parse_atServerStmt(t *testing.T) {
"prefix3:": "v1/v2_", "prefix3:": "v1/v2_",
"prefix4:": "a-b-c", "prefix4:": "a-b-c",
"summary:": `"test"`, "summary:": `"test"`,
"key:": `"bar"`, "key:": `"bar"`,
} }
p := New("foo.api", atServerTestAPI) p := New("foo.api", atServerTestAPI)

View File

@ -29,8 +29,6 @@ const (
// string mode end // string mode end
) )
var missingInput = errors.New("missing input")
type mode int type mode int
// Scanner is a lexical scanner. // Scanner is a lexical scanner.
@ -629,7 +627,7 @@ func NewScanner(filename string, src interface{}) (*Scanner, error) {
} }
if len(data) == 0 { if len(data) == 0 {
return nil, missingInput return nil, fmt.Errorf("filename: %s,missing input", filename)
} }
var runeList []rune var runeList []rune

View File

@ -62,13 +62,13 @@ func TestNewScanner(t *testing.T) {
{ {
filename: "foo", filename: "foo",
src: "", src: "",
expected: missingInput, expected: "missing input",
}, },
} }
for _, v := range testData { for _, v := range testData {
s, err := NewScanner(v.filename, v.src) s, err := NewScanner(v.filename, v.src)
if err != nil { if err != nil {
assert.Equal(t, v.expected.(error).Error(), err.Error()) assert.Contains(t, err.Error(), v.expected)
} else { } else {
assert.Equal(t, v.expected, s.filename) assert.Equal(t, v.expected, s.filename)
} }