Compare commits

...

13 Commits

Author SHA1 Message Date
14f39a9162 v0.0.416
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m43s
2024-03-18 11:19:01 +01:00
dcd106c1cd v0.0.415 add 'tagkey' to gojson.Decoder
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m45s
2024-03-18 10:42:00 +01:00
b704e2a362 v0.0.414 fix rfctime.Date bson marshalling for zero value
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m28s
2024-03-16 19:42:59 +01:00
6b4bd5a6f8 v0.0.413 fix tests
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m33s
2024-03-11 21:00:30 +01:00
6df4f5f2a1 v0.0.412 fix GenerateIDSpecs accepting nil for opt
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m32s
2024-03-11 20:58:06 +01:00
780905ba35 v0.0.411
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m33s
2024-03-11 20:43:37 +01:00
c679797765 v0.0.410 add ginext.SuppressGinLogs
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-03-11 20:42:12 +01:00
401aad9fa4 v0.0.409
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m41s
2024-03-11 17:05:10 +01:00
645113d553 v0.0.408
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m43s
2024-03-11 16:41:47 +01:00
4a33986b6a v0.0.407 sq.Iterate
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-03-11 16:40:41 +01:00
c1c8c64c76 v0.0.406 bf
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m34s
2024-03-10 16:44:21 +01:00
0927fdc4d7 v0.0.405
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m36s
2024-03-10 15:28:26 +01:00
102a280dda v0.0.404
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2024-03-10 15:25:30 +01:00
17 changed files with 372 additions and 100 deletions

View File

@@ -38,7 +38,7 @@ var rexIDChecksumConst = rext.W(regexp.MustCompile(`const ChecksumIDGenerator =
//go:embed id-generate.template //go:embed id-generate.template
var templateIDGenerateText string var templateIDGenerateText string
func GenerateIDSpecs(sourceDir string, destFile string, opt *IDGenOptions) error { func GenerateIDSpecs(sourceDir string, destFile string, opt IDGenOptions) error {
debugOutput := langext.Coalesce(opt.DebugOutput, false) debugOutput := langext.Coalesce(opt.DebugOutput, false)

View File

@@ -34,10 +34,10 @@ func TestGenerateIDSpecs(t *testing.T) {
_, err = cmdext.Runner("tar").Arg("-xvzf").Arg(tmpFile).Arg("-C").Arg(tmpDir).FailOnExitCode().FailOnTimeout().Timeout(time.Minute).Run() _, err = cmdext.Runner("tar").Arg("-xvzf").Arg(tmpFile).Arg("-C").Arg(tmpDir).FailOnExitCode().FailOnTimeout().Timeout(time.Minute).Run()
tst.AssertNoErr(t, err) tst.AssertNoErr(t, err)
err = GenerateIDSpecs(tmpDir, tmpDir+"/id_gen.go", &IDGenOptions{DebugOutput: langext.PTrue}) err = GenerateIDSpecs(tmpDir, tmpDir+"/id_gen.go", IDGenOptions{DebugOutput: langext.PTrue})
tst.AssertNoErr(t, err) tst.AssertNoErr(t, err)
err = GenerateIDSpecs(tmpDir, tmpDir+"/id_gen.go", &IDGenOptions{DebugOutput: langext.PTrue}) err = GenerateIDSpecs(tmpDir, tmpDir+"/id_gen.go", IDGenOptions{DebugOutput: langext.PTrue})
tst.AssertNoErr(t, err) tst.AssertNoErr(t, err)
fmt.Println() fmt.Println()

View File

@@ -39,6 +39,7 @@ type ginRouteSpec struct {
type Options struct { type Options struct {
AllowCors *bool // Add cors handler to allow all CORS requests on the default http methods AllowCors *bool // Add cors handler to allow all CORS requests on the default http methods
GinDebug *bool // Set gin.debug to true (adds more logs) GinDebug *bool // Set gin.debug to true (adds more logs)
SuppressGinLogs *bool // Suppress our custom gin logs (even if GinDebug == true)
BufferBody *bool // Buffers the input body stream, this way the ginext error handler can later include the whole request body BufferBody *bool // Buffers the input body stream, this way the ginext error handler can later include the whole request body
Timeout *time.Duration // The default handler timeout Timeout *time.Duration // The default handler timeout
ListenerBeforeRequest []func(g *gin.Context) // Register listener that are called before the handler method ListenerBeforeRequest []func(g *gin.Context) // Register listener that are called before the handler method
@@ -51,7 +52,7 @@ func NewEngine(opt Options) *GinWrapper {
wrapper := &GinWrapper{ wrapper := &GinWrapper{
engine: engine, engine: engine,
suppressGinLogs: false, suppressGinLogs: langext.Coalesce(opt.SuppressGinLogs, false),
allowCors: langext.Coalesce(opt.AllowCors, false), allowCors: langext.Coalesce(opt.AllowCors, false),
ginDebug: langext.Coalesce(opt.GinDebug, true), ginDebug: langext.Coalesce(opt.GinDebug, true),
bufferBody: langext.Coalesce(opt.BufferBody, false), bufferBody: langext.Coalesce(opt.BufferBody, false),
@@ -73,12 +74,12 @@ func NewEngine(opt Options) *GinWrapper {
if !wrapper.ginDebug { if !wrapper.ginDebug {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
ginlogger := gin.Logger() if !wrapper.suppressGinLogs {
engine.Use(func(context *gin.Context) { ginlogger := gin.Logger()
if !wrapper.suppressGinLogs { engine.Use(func(context *gin.Context) {
ginlogger(context) ginlogger(context)
} })
}) }
} else { } else {
gin.SetMode(gin.DebugMode) gin.SetMode(gin.DebugMode)
} }

2
go.mod
View File

@@ -15,7 +15,7 @@ require (
) )
require ( require (
github.com/bytedance/sonic v1.11.2 // indirect github.com/bytedance/sonic v1.11.3 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect

2
go.sum
View File

@@ -8,6 +8,8 @@ github.com/bytedance/sonic v1.11.1 h1:JC0+6c9FoWYYxakaoa+c5QTtJeiSZNeByOBhXtAFSn
github.com/bytedance/sonic v1.11.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/bytedance/sonic v1.11.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/bytedance/sonic v1.11.2 h1:ywfwo0a/3j9HR8wsYGWsIWl2mvRsI950HyoxiBERw5A= github.com/bytedance/sonic v1.11.2 h1:ywfwo0a/3j9HR8wsYGWsIWl2mvRsI950HyoxiBERw5A=
github.com/bytedance/sonic v1.11.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/bytedance/sonic v1.11.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA=
github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=

View File

@@ -1,5 +1,5 @@
package goext package goext
const GoextVersion = "0.0.403" const GoextVersion = "0.0.416"
const GoextVersionTimestamp = "2024-03-10T12:58:59+0100" const GoextVersionTimestamp = "2024-03-18T11:19:01+0100"

View File

@@ -217,6 +217,7 @@ type decodeState struct {
savedError error savedError error
useNumber bool useNumber bool
disallowUnknownFields bool disallowUnknownFields bool
tagkey *string
} }
// readIndex returns the position of the last byte read. // readIndex returns the position of the last byte read.
@@ -652,7 +653,11 @@ func (d *decodeState) object(v reflect.Value) error {
v.Set(reflect.MakeMap(t)) v.Set(reflect.MakeMap(t))
} }
case reflect.Struct: case reflect.Struct:
fields = cachedTypeFields(t) tagkey := "json"
if d.tagkey != nil {
tagkey = *d.tagkey
}
fields = cachedTypeFields(t, tagkey)
// ok // ok
default: default:
d.saveError(&UnmarshalTypeError{Value: "object", Type: t, Offset: int64(d.off)}) d.saveError(&UnmarshalTypeError{Value: "object", Type: t, Offset: int64(d.off)})

View File

@@ -382,7 +382,12 @@ func isEmptyValue(v reflect.Value) bool {
} }
func (e *encodeState) reflectValue(v reflect.Value, opts encOpts) { func (e *encodeState) reflectValue(v reflect.Value, opts encOpts) {
valueEncoder(v)(e, v, opts) tagkey := "json"
if opts.tagkey != nil {
tagkey = *opts.tagkey
}
valueEncoder(v, tagkey)(e, v, opts)
} }
type encOpts struct { type encOpts struct {
@@ -397,20 +402,22 @@ type encOpts struct {
// filter matches jsonfilter tag of struct // filter matches jsonfilter tag of struct
// marshals if no jsonfilter is set or otherwise if jsonfilter has the filter value // marshals if no jsonfilter is set or otherwise if jsonfilter has the filter value
filter *string filter *string
// use different tag instead of "json"
tagkey *string
} }
type encoderFunc func(e *encodeState, v reflect.Value, opts encOpts) type encoderFunc func(e *encodeState, v reflect.Value, opts encOpts)
var encoderCache sync.Map // map[reflect.Type]encoderFunc var encoderCache sync.Map // map[reflect.Type]encoderFunc
func valueEncoder(v reflect.Value) encoderFunc { func valueEncoder(v reflect.Value, tagkey string) encoderFunc {
if !v.IsValid() { if !v.IsValid() {
return invalidValueEncoder return invalidValueEncoder
} }
return typeEncoder(v.Type()) return typeEncoder(v.Type(), tagkey)
} }
func typeEncoder(t reflect.Type) encoderFunc { func typeEncoder(t reflect.Type, tagkey string) encoderFunc {
if fi, ok := encoderCache.Load(t); ok { if fi, ok := encoderCache.Load(t); ok {
return fi.(encoderFunc) return fi.(encoderFunc)
} }
@@ -433,7 +440,7 @@ func typeEncoder(t reflect.Type) encoderFunc {
} }
// Compute the real encoder and replace the indirect func with it. // Compute the real encoder and replace the indirect func with it.
f = newTypeEncoder(t, true) f = newTypeEncoder(t, true, tagkey)
wg.Done() wg.Done()
encoderCache.Store(t, f) encoderCache.Store(t, f)
return f return f
@@ -446,19 +453,19 @@ var (
// newTypeEncoder constructs an encoderFunc for a type. // newTypeEncoder constructs an encoderFunc for a type.
// The returned encoder only checks CanAddr when allowAddr is true. // The returned encoder only checks CanAddr when allowAddr is true.
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc { func newTypeEncoder(t reflect.Type, allowAddr bool, tagkey string) encoderFunc {
// If we have a non-pointer value whose type implements // If we have a non-pointer value whose type implements
// Marshaler with a value receiver, then we're better off taking // Marshaler with a value receiver, then we're better off taking
// the address of the value - otherwise we end up with an // the address of the value - otherwise we end up with an
// allocation as we cast the value to an interface. // allocation as we cast the value to an interface.
if t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(marshalerType) { if t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(marshalerType) {
return newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false)) return newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false, tagkey))
} }
if t.Implements(marshalerType) { if t.Implements(marshalerType) {
return marshalerEncoder return marshalerEncoder
} }
if t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(textMarshalerType) { if t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(textMarshalerType) {
return newCondAddrEncoder(addrTextMarshalerEncoder, newTypeEncoder(t, false)) return newCondAddrEncoder(addrTextMarshalerEncoder, newTypeEncoder(t, false, tagkey))
} }
if t.Implements(textMarshalerType) { if t.Implements(textMarshalerType) {
return textMarshalerEncoder return textMarshalerEncoder
@@ -480,15 +487,15 @@ func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
case reflect.Interface: case reflect.Interface:
return interfaceEncoder return interfaceEncoder
case reflect.Struct: case reflect.Struct:
return newStructEncoder(t) return newStructEncoder(t, tagkey)
case reflect.Map: case reflect.Map:
return newMapEncoder(t) return newMapEncoder(t, tagkey)
case reflect.Slice: case reflect.Slice:
return newSliceEncoder(t) return newSliceEncoder(t, tagkey)
case reflect.Array: case reflect.Array:
return newArrayEncoder(t) return newArrayEncoder(t, tagkey)
case reflect.Pointer: case reflect.Pointer:
return newPtrEncoder(t) return newPtrEncoder(t, tagkey)
default: default:
return unsupportedTypeEncoder return unsupportedTypeEncoder
} }
@@ -801,8 +808,8 @@ FieldLoop:
} }
} }
func newStructEncoder(t reflect.Type) encoderFunc { func newStructEncoder(t reflect.Type, tagkey string) encoderFunc {
se := structEncoder{fields: cachedTypeFields(t)} se := structEncoder{fields: cachedTypeFields(t, tagkey)}
return se.encode return se.encode
} }
@@ -855,7 +862,7 @@ func (me mapEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
e.ptrLevel-- e.ptrLevel--
} }
func newMapEncoder(t reflect.Type) encoderFunc { func newMapEncoder(t reflect.Type, tagkey string) encoderFunc {
switch t.Key().Kind() { switch t.Key().Kind() {
case reflect.String, case reflect.String,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
@@ -865,7 +872,7 @@ func newMapEncoder(t reflect.Type) encoderFunc {
return unsupportedTypeEncoder return unsupportedTypeEncoder
} }
} }
me := mapEncoder{typeEncoder(t.Elem())} me := mapEncoder{typeEncoder(t.Elem(), tagkey)}
return me.encode return me.encode
} }
@@ -936,7 +943,7 @@ func (se sliceEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
e.ptrLevel-- e.ptrLevel--
} }
func newSliceEncoder(t reflect.Type) encoderFunc { func newSliceEncoder(t reflect.Type, tagkey string) encoderFunc {
// Byte slices get special treatment; arrays don't. // Byte slices get special treatment; arrays don't.
if t.Elem().Kind() == reflect.Uint8 { if t.Elem().Kind() == reflect.Uint8 {
p := reflect.PointerTo(t.Elem()) p := reflect.PointerTo(t.Elem())
@@ -944,7 +951,7 @@ func newSliceEncoder(t reflect.Type) encoderFunc {
return encodeByteSlice return encodeByteSlice
} }
} }
enc := sliceEncoder{newArrayEncoder(t)} enc := sliceEncoder{newArrayEncoder(t, tagkey)}
return enc.encode return enc.encode
} }
@@ -964,8 +971,8 @@ func (ae arrayEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
e.WriteByte(']') e.WriteByte(']')
} }
func newArrayEncoder(t reflect.Type) encoderFunc { func newArrayEncoder(t reflect.Type, tagkey string) encoderFunc {
enc := arrayEncoder{typeEncoder(t.Elem())} enc := arrayEncoder{typeEncoder(t.Elem(), tagkey)}
return enc.encode return enc.encode
} }
@@ -992,8 +999,8 @@ func (pe ptrEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
e.ptrLevel-- e.ptrLevel--
} }
func newPtrEncoder(t reflect.Type) encoderFunc { func newPtrEncoder(t reflect.Type, tagkey string) encoderFunc {
enc := ptrEncoder{typeEncoder(t.Elem())} enc := ptrEncoder{typeEncoder(t.Elem(), tagkey)}
return enc.encode return enc.encode
} }
@@ -1270,7 +1277,7 @@ func (x byIndex) Less(i, j int) bool {
// typeFields returns a list of fields that JSON should recognize for the given type. // typeFields returns a list of fields that JSON should recognize for the given type.
// The algorithm is breadth-first search over the set of structs to include - the top struct // The algorithm is breadth-first search over the set of structs to include - the top struct
// and then any reachable anonymous structs. // and then any reachable anonymous structs.
func typeFields(t reflect.Type) structFields { func typeFields(t reflect.Type, tagkey string) structFields {
// Anonymous fields to explore at the current level and the next. // Anonymous fields to explore at the current level and the next.
current := []field{} current := []field{}
next := []field{{typ: t}} next := []field{{typ: t}}
@@ -1315,7 +1322,7 @@ func typeFields(t reflect.Type) structFields {
// Ignore unexported non-embedded fields. // Ignore unexported non-embedded fields.
continue continue
} }
tag := sf.Tag.Get("json") tag := sf.Tag.Get(tagkey)
if tag == "-" { if tag == "-" {
continue continue
} }
@@ -1449,7 +1456,7 @@ func typeFields(t reflect.Type) structFields {
for i := range fields { for i := range fields {
f := &fields[i] f := &fields[i]
f.encoder = typeEncoder(typeByIndex(t, f.index)) f.encoder = typeEncoder(typeByIndex(t, f.index), tagkey)
} }
nameIndex := make(map[string]int, len(fields)) nameIndex := make(map[string]int, len(fields))
for i, field := range fields { for i, field := range fields {
@@ -1474,13 +1481,26 @@ func dominantField(fields []field) (field, bool) {
return fields[0], true return fields[0], true
} }
var fieldCache sync.Map // map[reflect.Type]structFields var fieldCache sync.Map // map[string]map[reflect.Type]structFields
// cachedTypeFields is like typeFields but uses a cache to avoid repeated work. // cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
func cachedTypeFields(t reflect.Type) structFields { func cachedTypeFields(t reflect.Type, tagkey string) structFields {
if f, ok := fieldCache.Load(t); ok { if m0, ok := fieldCache.Load(tagkey); ok {
if f, ok := m0.(*sync.Map).Load(t); ok {
return f.(structFields)
}
f, _ := m0.(*sync.Map).LoadOrStore(t, typeFields(t, tagkey))
return f.(structFields)
} else {
m0 := &sync.Map{}
f, _ := m0.LoadOrStore(t, typeFields(t, tagkey))
fieldCache.Store(tagkey, m0)
return f.(structFields) return f.(structFields)
} }
f, _ := fieldCache.LoadOrStore(t, typeFields(t))
return f.(structFields)
} }

View File

@@ -41,6 +41,9 @@ func (dec *Decoder) UseNumber() { dec.d.useNumber = true }
// non-ignored, exported fields in the destination. // non-ignored, exported fields in the destination.
func (dec *Decoder) DisallowUnknownFields() { dec.d.disallowUnknownFields = true } func (dec *Decoder) DisallowUnknownFields() { dec.d.disallowUnknownFields = true }
// TagKey sets a different TagKey (instead of "json")
func (dec *Decoder) TagKey(v string) { dec.d.tagkey = &v }
// Decode reads the next JSON-encoded value from its // Decode reads the next JSON-encoded value from its
// input and stores it in the value pointed to by v. // input and stores it in the value pointed to by v.
// //

View File

@@ -5,12 +5,76 @@ import (
"time" "time"
) )
func Coalesce[T any](v *T, def T) T { func Coalesce[T any](v1 *T, def T) T {
if v == nil { if v1 != nil {
return def return *v1
} else {
return *v
} }
return def
}
func CoalesceOpt[T any](v1 *T, v2 *T) *T {
if v1 != nil {
return v1
}
return v2
}
func Coalesce3[T any](v1 *T, v2 *T, def T) T {
if v1 != nil {
return *v1
}
if v2 != nil {
return *v2
}
return def
}
func Coalesce3Opt[T any](v1 *T, v2 *T, v3 *T) *T {
if v1 != nil {
return v1
}
if v2 != nil {
return v2
}
return v3
}
func Coalesce4[T any](v1 *T, v2 *T, v3 *T, def T) T {
if v1 != nil {
return *v1
}
if v2 != nil {
return *v2
}
if v3 != nil {
return *v3
}
return def
}
func Coalesce4Opt[T any](v1 *T, v2 *T, v3 *T, v4 *T) *T {
if v1 != nil {
return v1
}
if v2 != nil {
return v2
}
if v3 != nil {
return v3
}
return v4
} }
func CoalesceString(s *string, def string) string { func CoalesceString(s *string, def string) string {

View File

@@ -1,19 +1,30 @@
package reflectext package reflectext
import "reflect" import (
"encoding/json"
"reflect"
)
func ConvertStructToMap(v any) any { type ConvertStructToMapOpt struct {
return reflectToMap(reflect.ValueOf(v)) KeepJsonMarshalTypes bool
} }
func reflectToMap(fv reflect.Value) any { func ConvertStructToMap(v any, opts ...ConvertStructToMapOpt) any {
opt := ConvertStructToMapOpt{}
if len(opts) > 0 {
opt = opts[0]
}
return reflectToMap(reflect.ValueOf(v), opt)
}
func reflectToMap(fv reflect.Value, opt ConvertStructToMapOpt) any {
if fv.Kind() == reflect.Ptr { if fv.Kind() == reflect.Ptr {
if fv.IsNil() { if fv.IsNil() {
return nil return nil
} else { } else {
return reflectToMap(fv.Elem()) return reflectToMap(fv.Elem(), opt)
} }
} }
@@ -30,7 +41,7 @@ func reflectToMap(fv reflect.Value) any {
arrlen := fv.Len() arrlen := fv.Len()
arr := make([]any, arrlen) arr := make([]any, arrlen)
for i := 0; i < arrlen; i++ { for i := 0; i < arrlen; i++ {
arr[i] = reflectToMap(fv.Index(i)) arr[i] = reflectToMap(fv.Index(i), opt)
} }
return arr return arr
@@ -41,7 +52,7 @@ func reflectToMap(fv reflect.Value) any {
arrlen := fv.Len() arrlen := fv.Len()
arr := make([]any, arrlen) arr := make([]any, arrlen)
for i := 0; i < arrlen; i++ { for i := 0; i < arrlen; i++ {
arr[i] = reflectToMap(fv.Index(i)) arr[i] = reflectToMap(fv.Index(i), opt)
} }
return arr return arr
@@ -56,11 +67,15 @@ func reflectToMap(fv reflect.Value) any {
if fv.Kind() == reflect.Struct { if fv.Kind() == reflect.Struct {
if opt.KeepJsonMarshalTypes && fv.Type().Implements(reflect.TypeFor[json.Marshaler]()) {
return fv.Interface()
}
res := make(map[string]any) res := make(map[string]any)
for i := 0; i < fv.NumField(); i++ { for i := 0; i < fv.NumField(); i++ {
if fv.Type().Field(i).IsExported() { if fv.Type().Field(i).IsExported() {
res[fv.Type().Field(i).Name] = reflectToMap(fv.Field(i)) res[fv.Type().Field(i).Name] = reflectToMap(fv.Field(i), opt)
} }
} }

View File

@@ -9,6 +9,8 @@ import (
"go.mongodb.org/mongo-driver/bson/bsonrw" "go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/bsontype" "go.mongodb.org/mongo-driver/bson/bsontype"
"reflect" "reflect"
"strconv"
"strings"
"time" "time"
) )
@@ -65,36 +67,20 @@ func (t *Date) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &str); err != nil { if err := json.Unmarshal(data, &str); err != nil {
return err return err
} }
t0, err := time.Parse(t.FormatStr(), str) return t.ParseString(str)
if err != nil {
return err
}
t.Year = t0.Year()
t.Month = int(t0.Month())
t.Day = t0.Day()
return nil
} }
func (t Date) MarshalJSON() ([]byte, error) { func (t Date) MarshalJSON() ([]byte, error) {
str := t.TimeUTC().Format(t.FormatStr()) str := t.String()
return json.Marshal(str) return json.Marshal(str)
} }
func (t Date) MarshalText() ([]byte, error) { func (t Date) MarshalText() ([]byte, error) {
b := make([]byte, 0, len(t.FormatStr())) return []byte(t.String()), nil
return t.TimeUTC().AppendFormat(b, t.FormatStr()), nil
} }
func (t *Date) UnmarshalText(data []byte) error { func (t *Date) UnmarshalText(data []byte) error {
var err error return t.ParseString(string(data))
v, err := time.Parse(t.FormatStr(), string(data))
if err != nil {
return err
}
t.Year = v.Year()
t.Month = int(v.Month())
t.Day = v.Day()
return nil
} }
func (t *Date) UnmarshalBSONValue(bt bsontype.Type, data []byte) error { func (t *Date) UnmarshalBSONValue(bt bsontype.Type, data []byte) error {
@@ -116,6 +102,13 @@ func (t *Date) UnmarshalBSONValue(bt bsontype.Type, data []byte) error {
return err return err
} }
if tt == "" {
t.Year = 0
t.Month = 0
t.Day = 0
return nil
}
v, err := time.Parse(t.FormatStr(), tt) v, err := time.Parse(t.FormatStr(), tt)
if err != nil { if err != nil {
return err return err
@@ -128,7 +121,10 @@ func (t *Date) UnmarshalBSONValue(bt bsontype.Type, data []byte) error {
} }
func (t Date) MarshalBSONValue() (bsontype.Type, []byte, error) { func (t Date) MarshalBSONValue() (bsontype.Type, []byte, error) {
return bson.MarshalValue(t.TimeUTC().Format(t.FormatStr())) if t.IsZero() {
return bson.MarshalValue("")
}
return bson.MarshalValue(t.String())
} }
func (t Date) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { func (t Date) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
@@ -164,7 +160,7 @@ func (t Date) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val
} }
func (t Date) Serialize() string { func (t Date) Serialize() string {
return t.TimeUTC().Format(t.FormatStr()) return t.String()
} }
func (t Date) FormatStr() string { func (t Date) FormatStr() string {
@@ -212,11 +208,52 @@ func (t Date) Format(layout string) string {
} }
func (t Date) GoString() string { func (t Date) GoString() string {
return t.TimeUTC().GoString() return fmt.Sprintf("rfctime.Date{Year: %d, Month: %d, Day: %d}", t.Year, t.Month, t.Day)
} }
func (t Date) String() string { func (t Date) String() string {
return t.TimeUTC().String() return fmt.Sprintf("%04d-%02d-%02d", t.Year, t.Month, t.Day)
}
func (t *Date) ParseString(v string) error {
split := strings.Split(v, "-")
if len(split) != 3 {
return errors.New("invalid date format: " + v)
}
year, err := strconv.ParseInt(split[0], 10, 32)
if err != nil {
return errors.New("invalid date format: " + v + ": " + err.Error())
}
month, err := strconv.ParseInt(split[1], 10, 32)
if err != nil {
return errors.New("invalid date format: " + v + ": " + err.Error())
}
day, err := strconv.ParseInt(split[2], 10, 32)
if err != nil {
return errors.New("invalid date format: " + v + ": " + err.Error())
}
if year < 0 {
return errors.New("invalid date format: " + v + ": year is negative")
}
if month < 1 || month > 12 {
return errors.New("invalid date format: " + v + ": month is out of range")
}
if day < 1 || day > 31 {
return errors.New("invalid date format: " + v + ": day is out of range")
}
t.Year = int(year)
t.Month = int(month)
t.Day = int(day)
return nil
}
func (t Date) IsZero() bool {
return t.Year == 0 && t.Month == 0 && t.Day == 0
} }
func NewDate(t time.Time) Date { func NewDate(t time.Time) Date {

View File

@@ -7,8 +7,6 @@ import (
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/rfctime" "gogs.mikescher.com/BlackForestBytes/goext/rfctime"
"gogs.mikescher.com/BlackForestBytes/goext/timeext" "gogs.mikescher.com/BlackForestBytes/goext/timeext"
"strconv"
"strings"
"time" "time"
) )
@@ -79,24 +77,12 @@ var ConverterRFC339NanoTimeToString = NewDBTypeConverter[rfctime.RFC3339NanoTime
var ConverterRFCDateToString = NewDBTypeConverter[rfctime.Date, string](func(v rfctime.Date) (string, error) { var ConverterRFCDateToString = NewDBTypeConverter[rfctime.Date, string](func(v rfctime.Date) (string, error) {
return fmt.Sprintf("%04d-%02d-%02d", v.Year, v.Month, v.Day), nil return fmt.Sprintf("%04d-%02d-%02d", v.Year, v.Month, v.Day), nil
}, func(v string) (rfctime.Date, error) { }, func(v string) (rfctime.Date, error) {
split := strings.Split(v, "-") d := rfctime.Date{}
if len(split) != 3 { if err := d.ParseString(v); err != nil {
return rfctime.Date{}, errors.New("invalid date format: " + v) return rfctime.Date{}, err
} else {
return d, nil
} }
year, err := strconv.ParseInt(split[0], 10, 32)
if err != nil {
return rfctime.Date{}, errors.New("invalid date format: " + v + ": " + err.Error())
}
month, err := strconv.ParseInt(split[0], 10, 32)
if err != nil {
return rfctime.Date{}, errors.New("invalid date format: " + v + ": " + err.Error())
}
day, err := strconv.ParseInt(split[0], 10, 32)
if err != nil {
return rfctime.Date{}, errors.New("invalid date format: " + v + ": " + err.Error())
}
return rfctime.Date{Year: int(year), Month: int(month), Day: int(day)}, nil
}) })
var ConverterRFCTimeToString = NewDBTypeConverter[rfctime.Time, string](func(v rfctime.Time) (string, error) { var ConverterRFCTimeToString = NewDBTypeConverter[rfctime.Time, string](func(v rfctime.Time) (string, error) {

View File

@@ -47,3 +47,10 @@ func NewSimplePaginateFilter(filterClause string, filterParams PP, sort []Filter
}, },
} }
} }
func NewEmptyPaginateFilter() PaginateFilter {
return genericPaginateFilter{
sql: func(params PP) (string, string, []string) { return "1=1", "", nil },
sort: func() []FilterSort { return make([]FilterSort, 0) },
}
}

48
sq/list.go Normal file
View File

@@ -0,0 +1,48 @@
package sq
import (
"context"
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
)
func Iterate[TData any](ctx context.Context, q Queryable, table string, filter PaginateFilter, scanMode StructScanMode, scanSec StructScanSafety, page int, limit *int, consumer func(ctx context.Context, v TData) error) (int, error) {
if filter == nil {
filter = NewEmptyPaginateFilter()
}
prepParams := PP{}
sortOrder := filter.Sort()
sortCond := ""
if len(sortOrder) > 0 {
sortCond = "ORDER BY "
for i, v := range sortOrder {
if i > 0 {
sortCond += ", "
}
sortCond += v.Field + " " + string(v.Direction)
}
}
pageCond := ""
if limit != nil {
pageCond += fmt.Sprintf("LIMIT :%s OFFSET :%s", prepParams.Add(*limit+1), prepParams.Add(*limit*(page-1)))
}
filterCond, joinCond, joinTables := filter.SQL(prepParams)
selectCond := table + ".*"
for _, v := range joinTables {
selectCond += ", " + v + ".*"
}
sqlQueryData := "SELECT " + selectCond + " FROM " + table + " " + joinCond + " WHERE ( " + filterCond + " ) " + sortCond + " " + pageCond
rows, err := q.Query(ctx, sqlQueryData, prepParams)
if err != nil {
return 0, exerr.Wrap(err, "failed to list paginated entries from DB").Str("table", table).Any("filter", filter).Int("page", page).Any("limit", limit).Build()
}
return IterateAll[TData](ctx, q, rows, scanMode, scanSec, true, consumer)
}

View File

@@ -9,6 +9,10 @@ import (
) )
func Paginate[TData any](ctx context.Context, q Queryable, table string, filter PaginateFilter, scanMode StructScanMode, scanSec StructScanSafety, page int, limit *int) ([]TData, pag.Pagination, error) { func Paginate[TData any](ctx context.Context, q Queryable, table string, filter PaginateFilter, scanMode StructScanMode, scanSec StructScanSafety, page int, limit *int) ([]TData, pag.Pagination, error) {
if filter == nil {
filter = NewEmptyPaginateFilter()
}
prepParams := PP{} prepParams := PP{}
sortOrder := filter.Sort() sortOrder := filter.Sort()
@@ -90,6 +94,10 @@ func Paginate[TData any](ctx context.Context, q Queryable, table string, filter
} }
func Count(ctx context.Context, q Queryable, table string, filter PaginateFilter) (int, error) { func Count(ctx context.Context, q Queryable, table string, filter PaginateFilter) (int, error) {
if filter == nil {
filter = NewEmptyPaginateFilter()
}
prepParams := PP{} prepParams := PP{}
filterCond, joinCond, _ := filter.SQL(prepParams) filterCond, joinCond, _ := filter.SQL(prepParams)

View File

@@ -333,3 +333,79 @@ func ScanAll[TData any](ctx context.Context, q Queryable, rows *sqlx.Rows, mode
} }
return res, nil return res, nil
} }
func IterateAll[TData any](ctx context.Context, q Queryable, rows *sqlx.Rows, mode StructScanMode, sec StructScanSafety, close bool, consumer func(ctx context.Context, v TData) error) (int, error) {
var strscan *StructScanner
if sec == Safe {
strscan = NewStructScanner(rows, false)
var data TData
err := strscan.Start(&data)
if err != nil {
return 0, err
}
} else if sec == Unsafe {
strscan = NewStructScanner(rows, true)
var data TData
err := strscan.Start(&data)
if err != nil {
return 0, err
}
} else {
return 0, errors.New("unknown value for <sec>")
}
rcount := 0
for rows.Next() {
if err := ctx.Err(); err != nil {
return rcount, err
}
if mode == SModeFast {
var data TData
err := strscan.StructScanBase(&data)
if err != nil {
return rcount, err
}
err = consumer(ctx, data)
if err != nil {
return rcount, exerr.Wrap(err, "").Build()
}
rcount++
} else if mode == SModeExtended {
var data TData
err := strscan.StructScanExt(q, &data)
if err != nil {
return rcount, err
}
err = consumer(ctx, data)
if err != nil {
return rcount, exerr.Wrap(err, "").Build()
}
rcount++
} else {
return rcount, errors.New("unknown value for <mode>")
}
}
if close {
err := strscan.rows.Close()
if err != nil {
return rcount, err
}
}
if err := rows.Err(); err != nil {
return rcount, err
}
return rcount, nil
}