Compare commits

...

14 Commits

Author SHA1 Message Date
5fb2f8a312 v0.0.139 2023-06-06 21:40:34 +02:00
2ad820be8d v0.0.138 2023-06-06 21:33:49 +02:00
555096102a v0.0.137 2023-06-06 21:30:22 +02:00
d76d7b5cb9 v0.0.136 2023-06-06 21:26:12 +02:00
6622c9003d v0.0.135 2023-06-06 21:24:13 +02:00
b02e1d2e85 v0.0.134 2023-06-06 21:22:44 +02:00
c338d23070 v0.0.133 2023-06-06 21:18:40 +02:00
1fbae343a4 Fix RFC3339 serialization 2023-06-06 11:26:46 +02:00
31418bf0e6 v0.0.130 2023-06-05 13:30:32 +02:00
6d45f6f667 v0.0.129 2023-06-05 13:24:52 +02:00
f610a2202c v0.0.128 2023-06-02 09:44:31 +02:00
2807299d46 v0.0.127 2023-05-28 22:55:06 +02:00
e872dbccec v0.0.126 2023-05-28 19:53:30 +02:00
9daf71e2ed v0.0.125 2023-05-28 19:41:24 +02:00
29 changed files with 1896 additions and 13 deletions

View File

@@ -3,7 +3,9 @@ run:
echo "This is a library - can't be run" && false echo "This is a library - can't be run" && false
test: test:
go test ./... # go test ./...
which gotestsum || go install gotest.tools/gotestsum@latest
gotestsum --format "testname" -- -tags="timetzdata sqlite_fts5 sqlite_foreign_keys" "./test"
version: version:
_data/version.sh _data/version.sh

View File

@@ -5,4 +5,36 @@ A collection of general & useful library methods
This should not have any heavy dependencies (gin, mongo, etc) and add missing basic language features... This should not have any heavy dependencies (gin, mongo, etc) and add missing basic language features...
Potentially needs `export GOPRIVATE="gogs.mikescher.com"` Potentially needs `export GOPRIVATE="gogs.mikescher.com"`
### Packages:
| Name | Maintainer | Description |
|-------------|------------|---------------------------------------------------------------------------------------------------------------|
| langext | Mike | General uttility/helper functions, (everything thats missing from go standard library) |
| mathext | Mike | Utility/Helper functions for math |
| cryptext | Mike | Utility/Helper functions for encryption |
| syncext | Mike | Utility/Helper funtions for multi-threading / mutex / channels |
| dataext | Mike | Various useful data structures |
| zipext | Mike | Utility for zip/gzip/tar etc |
| | | |
| mongoext | Mike | Utility/Helper functions for mongodb |
| cursortoken | Mike | MongoDB cursortoken implementation |
| | | |
| totpext | Mike | Implementation of TOTP (2-Factor-Auth) |
| termext | Mike | Utilities for terminals (mostly color output) |
| confext | Mike | Parses environment configuration into structs |
| cmdext | Mike | Runner for external commands/processes |
| | | |
| sq | Mike | Utility functions for sql based databases |
| tst | Mike | Utility functions for unit tests |
| | | |
| rfctime | Mike | Classes for time seriallization, with different marshallign method for mongo and json |
| gojson | Mike | Same interface for marshalling/unmarshalling as go/json, except with proper serialization of null arrays/maps |
| | | |
| bfcodegen | Mike | Various codegen tools (run via go generate) |
| | | |
| rext | Mike | Regex Wrapper, wraps regexp with a better interface |
| wmo | Mike | Mongo Wrapper, wraps mongodb with a better interface |
| | | |

13
TODO.md Normal file
View File

@@ -0,0 +1,13 @@
- cronext
- cursortoken
- typed/geenric mongo wrapper
- error package
- rfctime.DateOnly
- rfctime.HMSTimeOnly
- rfctime.NanoTimeOnly

318
bfcodegen/enum-generate.go Normal file
View File

@@ -0,0 +1,318 @@
package bfcodegen
import (
"errors"
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/cmdext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/rext"
"io"
"os"
"regexp"
"strings"
"time"
)
type EnumDefVal struct {
VarName string
Value string
Description *string
}
type EnumDef struct {
File string
EnumTypeName string
Type string
Values []EnumDefVal
}
var rexPackage = rext.W(regexp.MustCompile("^package\\s+(?P<name>[A-Za-z0-9_]+)\\s*$"))
var rexEnumDef = rext.W(regexp.MustCompile("^\\s*type\\s+(?P<name>[A-Za-z0-9_]+)\\s+(?P<type>[A-Za-z0-9_]+)\\s*//\\s*(@enum:type).*$"))
var rexValueDef = rext.W(regexp.MustCompile("^\\s*(?P<name>[A-Za-z0-9_]+)\\s+(?P<type>[A-Za-z0-9_]+)\\s*=\\s*(?P<value>(\"[A-Za-z0-9_:]+\"|[0-9]+))\\s*(//(?P<descr>.*))?.*$"))
func GenerateEnumSpecs(sourceDir string, destFile string) error {
files, err := os.ReadDir(sourceDir)
if err != nil {
return err
}
allEnums := make([]EnumDef, 0)
pkgname := ""
for _, f := range files {
if !strings.HasSuffix(f.Name(), ".go") {
continue
}
fmt.Printf("========= %s =========\n\n", f.Name())
fileEnums, pn, err := processFile(f.Name())
if err != nil {
return err
}
fmt.Printf("\n")
allEnums = append(allEnums, fileEnums...)
if pn != "" {
pkgname = pn
}
}
if pkgname == "" {
return errors.New("no package name found in any file")
}
err = os.WriteFile(destFile, []byte(fmtOutput(allEnums, pkgname)), 0o755)
if err != nil {
return err
}
res, err := cmdext.RunCommand("go", []string{"fmt", destFile}, langext.Ptr(2*time.Second))
if err != nil {
return err
}
if res.CommandTimedOut {
fmt.Println(res.StdCombined)
return errors.New("go fmt timed out")
}
if res.ExitCode != 0 {
fmt.Println(res.StdCombined)
return errors.New("go fmt did not succeed")
}
return nil
}
func processFile(fn string) ([]EnumDef, string, error) {
file, err := os.Open(fn)
if err != nil {
return nil, "", err
}
defer func() { _ = file.Close() }()
bin, err := io.ReadAll(file)
if err != nil {
return nil, "", err
}
lines := strings.Split(string(bin), "\n")
enums := make([]EnumDef, 0)
pkgname := ""
for i, line := range lines {
if i == 0 && strings.HasPrefix(line, "// Code generated by") {
break
}
if match, ok := rexPackage.MatchFirst(line); i == 0 && ok {
pkgname = match.GroupByName("name").Value()
continue
}
if match, ok := rexEnumDef.MatchFirst(line); ok {
def := EnumDef{
File: fn,
EnumTypeName: match.GroupByName("name").Value(),
Type: match.GroupByName("type").Value(),
Values: make([]EnumDefVal, 0),
}
enums = append(enums, def)
fmt.Printf("Found enum definition { '%s' -> '%s' }\n", def.EnumTypeName, def.Type)
}
if match, ok := rexValueDef.MatchFirst(line); ok {
typename := match.GroupByName("type").Value()
def := EnumDefVal{
VarName: match.GroupByName("name").Value(),
Value: match.GroupByName("value").Value(),
Description: match.GroupByNameOrEmpty("descr").ValueOrNil(),
}
found := false
for i, v := range enums {
if v.EnumTypeName == typename {
enums[i].Values = append(enums[i].Values, def)
found = true
if def.Description != nil {
fmt.Printf("Found enum value [%s] for '%s' ('%s')\n", def.Value, def.VarName, *def.Description)
} else {
fmt.Printf("Found enum value [%s] for '%s'\n", def.Value, def.VarName)
}
break
}
}
if !found {
fmt.Printf("Found non-enum value [%s] for '%s' ( looks like enum value, but no matching @enum:type )\n", def.Value, def.VarName)
}
}
}
return enums, pkgname, nil
}
func fmtOutput(enums []EnumDef, pkgname string) string {
str := "// Code generated by permissions_gen.sh DO NOT EDIT.\n"
str += "\n"
str += "package " + pkgname + "\n"
str += "\n"
str += "import \"gogs.mikescher.com/BlackForestBytes/goext/langext\"" + "\n"
str += "\n"
str += "type Enum interface {" + "\n"
str += " Valid() bool" + "\n"
str += " ValuesAny() []any" + "\n"
str += " ValuesMeta() []EnumMetaValue" + "\n"
str += " VarName() string" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "type StringEnum interface {" + "\n"
str += " Enum" + "\n"
str += " String() string" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "type DescriptionEnum interface {" + "\n"
str += " Enum" + "\n"
str += " Description() string" + "\n"
str += "}" + "\n"
str += "\n"
str += "type EnumMetaValue struct {" + "\n"
str += " VarName string `json:\"varName\"`" + "\n"
str += " Value any `json:\"value\"`" + "\n"
str += " Description *string `json:\"description\"`" + "\n"
str += "}" + "\n"
str += "\n"
for _, enumdef := range enums {
hasDescr := langext.ArrAll(enumdef.Values, func(val EnumDefVal) bool { return val.Description != nil })
hasStr := enumdef.Type == "string"
str += "// ================================ " + enumdef.EnumTypeName + " ================================" + "\n"
str += "//" + "\n"
str += "// File: " + enumdef.File + "\n"
str += "// StringEnum: " + langext.Conditional(hasStr, "true", "false") + "\n"
str += "// DescrEnum: " + langext.Conditional(hasDescr, "true", "false") + "\n"
str += "//" + "\n"
str += "" + "\n"
str += "var __" + enumdef.EnumTypeName + "Values = []" + enumdef.EnumTypeName + "{" + "\n"
for _, v := range enumdef.Values {
str += " " + v.VarName + "," + "\n"
}
str += "}" + "\n"
str += "" + "\n"
if hasDescr {
str += "var __" + enumdef.EnumTypeName + "Descriptions = map[" + enumdef.EnumTypeName + "]string{" + "\n"
for _, v := range enumdef.Values {
str += " " + v.VarName + ": \"" + strings.TrimSpace(*v.Description) + "\"," + "\n"
}
str += "}" + "\n"
str += "" + "\n"
}
str += "var __" + enumdef.EnumTypeName + "Varnames = map[" + enumdef.EnumTypeName + "]string{" + "\n"
for _, v := range enumdef.Values {
str += " " + v.VarName + ": \"" + v.VarName + "\"," + "\n"
}
str += "}" + "\n"
str += "" + "\n"
str += "func (e " + enumdef.EnumTypeName + ") Valid() bool {" + "\n"
str += " return langext.InArray(e, __" + enumdef.EnumTypeName + "Values)" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func (e " + enumdef.EnumTypeName + ") Values() []" + enumdef.EnumTypeName + " {" + "\n"
str += " return __" + enumdef.EnumTypeName + "Values" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func (e " + enumdef.EnumTypeName + ") ValuesAny() []any {" + "\n"
str += " return langext.ArrCastToAny(__" + enumdef.EnumTypeName + "Values)" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func (e " + enumdef.EnumTypeName + ") ValuesMeta() []EnumMetaValue {" + "\n"
str += " return []EnumMetaValue{" + "\n"
for _, v := range enumdef.Values {
if hasDescr {
str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: langext.Ptr(\"%s\")},", v.VarName, v.VarName, strings.TrimSpace(*v.Description)) + "\n"
} else {
str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: nil},", v.VarName, v.VarName) + "\n"
}
}
str += " }" + "\n"
str += "}" + "\n"
str += "" + "\n"
if hasStr {
str += "func (e " + enumdef.EnumTypeName + ") String() string {" + "\n"
str += " return string(e)" + "\n"
str += "}" + "\n"
str += "" + "\n"
}
if hasDescr {
str += "func (e " + enumdef.EnumTypeName + ") Description() string {" + "\n"
str += " if d, ok := __" + enumdef.EnumTypeName + "Descriptions[e]; ok {" + "\n"
str += " return d" + "\n"
str += " }" + "\n"
str += " return \"\"" + "\n"
str += "}" + "\n"
str += "" + "\n"
}
str += "func (e " + enumdef.EnumTypeName + ") VarName() string {" + "\n"
str += " if d, ok := __" + enumdef.EnumTypeName + "Varnames[e]; ok {" + "\n"
str += " return d" + "\n"
str += " }" + "\n"
str += " return \"\"" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func Parse" + enumdef.EnumTypeName + "(vv string) (" + enumdef.EnumTypeName + ", bool) {" + "\n"
str += " for _, ev := range __" + enumdef.EnumTypeName + "Values {" + "\n"
str += " if string(ev) == vv {" + "\n"
str += " return ev, true" + "\n"
str += " }" + "\n"
str += " }" + "\n"
str += " return \"\", false" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func " + enumdef.EnumTypeName + "Values() []" + enumdef.EnumTypeName + " {" + "\n"
str += " return __" + enumdef.EnumTypeName + "Values" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func " + enumdef.EnumTypeName + "ValuesMeta() []EnumMetaValue {" + "\n"
str += " return []EnumMetaValue{" + "\n"
for _, v := range enumdef.Values {
if hasDescr {
str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: langext.Ptr(\"%s\")},", v.VarName, v.VarName, strings.TrimSpace(*v.Description)) + "\n"
} else {
str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: nil},", v.VarName, v.VarName) + "\n"
}
}
str += " }" + "\n"
str += "}" + "\n"
str += "" + "\n"
}
return str
}

View File

@@ -8,6 +8,7 @@ import (
"os" "os"
"reflect" "reflect"
"strconv" "strconv"
"strings"
"time" "time"
) )
@@ -172,6 +173,20 @@ func parseEnvToValue(envval string, fullEnvKey string, rvtype reflect.Type) (ref
return envcvl, nil return envcvl, nil
} else if rvtype.ConvertibleTo(reflect.TypeOf(false)) {
if strings.TrimSpace(strings.ToLower(envval)) == "true" {
return reflect.ValueOf(true).Convert(rvtype), nil
} else if strings.TrimSpace(strings.ToLower(envval)) == "false" {
return reflect.ValueOf(true).Convert(rvtype), nil
} else if strings.TrimSpace(strings.ToLower(envval)) == "1" {
return reflect.ValueOf(false).Convert(rvtype), nil
} else if strings.TrimSpace(strings.ToLower(envval)) == "0" {
return reflect.ValueOf(false).Convert(rvtype), nil
} else {
return reflect.Value{}, errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to <%s, ,bool> (value := '%s')", rvtype.Name(), fullEnvKey, envval))
}
} else if rvtype.ConvertibleTo(reflect.TypeOf("")) { } else if rvtype.ConvertibleTo(reflect.TypeOf("")) {
envcvl := reflect.ValueOf(envval).Convert(rvtype) envcvl := reflect.ValueOf(envval).Convert(rvtype)

View File

@@ -68,6 +68,7 @@ func TestApplyEnvOverridesSimple(t *testing.T) {
V7 aliasstring `env:"TEST_V7"` V7 aliasstring `env:"TEST_V7"`
V8 time.Duration `env:"TEST_V8"` V8 time.Duration `env:"TEST_V8"`
V9 time.Time `env:"TEST_V9"` V9 time.Time `env:"TEST_V9"`
VA bool `env:"TEST_VA"`
} }
data := testdata{ data := testdata{
@@ -82,6 +83,7 @@ func TestApplyEnvOverridesSimple(t *testing.T) {
V7: "7", V7: "7",
V8: 9, V8: 9,
V9: time.Unix(1671102873, 0), V9: time.Unix(1671102873, 0),
VA: false,
} }
t.Setenv("TEST_V1", "846") t.Setenv("TEST_V1", "846")
@@ -93,6 +95,7 @@ func TestApplyEnvOverridesSimple(t *testing.T) {
t.Setenv("TEST_V7", "AAAAAA") t.Setenv("TEST_V7", "AAAAAA")
t.Setenv("TEST_V8", "1min4s") t.Setenv("TEST_V8", "1min4s")
t.Setenv("TEST_V9", "2009-11-10T23:00:00Z") t.Setenv("TEST_V9", "2009-11-10T23:00:00Z")
t.Setenv("TEST_VA", "true")
err := ApplyEnvOverrides("", &data, ".") err := ApplyEnvOverrides("", &data, ".")
if err != nil { if err != nil {
@@ -109,6 +112,7 @@ func TestApplyEnvOverridesSimple(t *testing.T) {
tst.AssertEqual(t, data.V7, "AAAAAA") tst.AssertEqual(t, data.V7, "AAAAAA")
tst.AssertEqual(t, data.V8, time.Second*64) tst.AssertEqual(t, data.V8, time.Second*64)
tst.AssertEqual(t, data.V9, time.Unix(1257894000, 0).UTC()) tst.AssertEqual(t, data.V9, time.Unix(1257894000, 0).UTC())
tst.AssertEqual(t, data.VA, true)
} }
func TestApplyEnvOverridesRecursive(t *testing.T) { func TestApplyEnvOverridesRecursive(t *testing.T) {

8
cursortoken/direction.go Normal file
View File

@@ -0,0 +1,8 @@
package cursortoken
type SortDirection string //@enum:type
const (
SortASC SortDirection = "ASC"
SortDESC SortDirection = "DESC"
)

10
cursortoken/filter.go Normal file
View File

@@ -0,0 +1,10 @@
package cursortoken
import (
"go.mongodb.org/mongo-driver/mongo"
)
type Filter interface {
FilterQuery() mongo.Pipeline
Pagination() (string, SortDirection, *string, *SortDirection)
}

184
cursortoken/token.go Normal file
View File

@@ -0,0 +1,184 @@
package cursortoken
import (
"encoding/base32"
"encoding/json"
"errors"
"go.mongodb.org/mongo-driver/bson/primitive"
"strings"
"time"
)
type Mode string
const (
CTMStart Mode = "START"
CTMNormal Mode = "NORMAL"
CTMEnd Mode = "END"
)
type Extra struct {
Timestamp *time.Time
Id *string
Page *int
PageSize *int
}
type CursorToken struct {
Mode Mode
ValuePrimary string
ValueSecondary string
Direction SortDirection
DirectionSecondary SortDirection
PageSize int
Extra Extra
}
type cursorTokenSerialize struct {
ValuePrimary *string `json:"v1,omitempty"`
ValueSecondary *string `json:"v2,omitempty"`
Direction *SortDirection `json:"dir,omitempty"`
DirectionSecondary *SortDirection `json:"dir2,omitempty"`
PageSize *int `json:"size,omitempty"`
ExtraTimestamp *time.Time `json:"ts,omitempty"`
ExtraId *string `json:"id,omitempty"`
ExtraPage *int `json:"pg,omitempty"`
ExtraPageSize *int `json:"sz,omitempty"`
}
func Start() CursorToken {
return CursorToken{
Mode: CTMStart,
ValuePrimary: "",
ValueSecondary: "",
Direction: "",
DirectionSecondary: "",
PageSize: 0,
Extra: Extra{},
}
}
func End() CursorToken {
return CursorToken{
Mode: CTMEnd,
ValuePrimary: "",
ValueSecondary: "",
Direction: "",
DirectionSecondary: "",
PageSize: 0,
Extra: Extra{},
}
}
func (c *CursorToken) Token() string {
if c.Mode == CTMStart {
return "@start"
}
if c.Mode == CTMEnd {
return "@end"
}
// We kinda manually implement omitempty for the CursorToken here
// because omitempty does not work for time.Time and otherwise we would always
// get weird time values when decoding a token that initially didn't have an Timestamp set
// For this usecase we treat Unix=0 as an empty timestamp
sertok := cursorTokenSerialize{}
if c.ValuePrimary != "" {
sertok.ValuePrimary = &c.ValuePrimary
}
if c.ValueSecondary != "" {
sertok.ValueSecondary = &c.ValueSecondary
}
if c.Direction != "" {
sertok.Direction = &c.Direction
}
if c.DirectionSecondary != "" {
sertok.DirectionSecondary = &c.DirectionSecondary
}
if c.PageSize != 0 {
sertok.PageSize = &c.PageSize
}
sertok.ExtraTimestamp = c.Extra.Timestamp
sertok.ExtraId = c.Extra.Id
sertok.ExtraPage = c.Extra.Page
sertok.ExtraPageSize = c.Extra.PageSize
body, err := json.Marshal(sertok)
if err != nil {
panic(err)
}
return "tok_" + base32.StdEncoding.EncodeToString(body)
}
func Decode(tok string) (CursorToken, error) {
if tok == "" {
return Start(), nil
}
if strings.ToLower(tok) == "@start" {
return Start(), nil
}
if strings.ToLower(tok) == "@end" {
return End(), nil
}
if !strings.HasPrefix(tok, "tok_") {
return CursorToken{}, errors.New("could not decode token, missing prefix")
}
body, err := base32.StdEncoding.DecodeString(tok[len("tok_"):])
if err != nil {
return CursorToken{}, err
}
var tokenDeserialize cursorTokenSerialize
err = json.Unmarshal(body, &tokenDeserialize)
if err != nil {
return CursorToken{}, err
}
token := CursorToken{Mode: CTMNormal}
if tokenDeserialize.ValuePrimary != nil {
token.ValuePrimary = *tokenDeserialize.ValuePrimary
}
if tokenDeserialize.ValueSecondary != nil {
token.ValueSecondary = *tokenDeserialize.ValueSecondary
}
if tokenDeserialize.Direction != nil {
token.Direction = *tokenDeserialize.Direction
}
if tokenDeserialize.DirectionSecondary != nil {
token.DirectionSecondary = *tokenDeserialize.DirectionSecondary
}
if tokenDeserialize.PageSize != nil {
token.PageSize = *tokenDeserialize.PageSize
}
token.Extra.Timestamp = tokenDeserialize.ExtraTimestamp
token.Extra.Id = tokenDeserialize.ExtraId
token.Extra.Page = tokenDeserialize.ExtraPage
token.Extra.PageSize = tokenDeserialize.ExtraPageSize
return token, nil
}
func (c *CursorToken) ValuePrimaryObjectId() (primitive.ObjectID, bool) {
if oid, err := primitive.ObjectIDFromHex(c.ValuePrimary); err == nil {
return oid, true
} else {
return primitive.ObjectID{}, false
}
}
func (c *CursorToken) ValueSecondaryObjectId() (primitive.ObjectID, bool) {
if oid, err := primitive.ObjectIDFromHex(c.ValueSecondary); err == nil {
return oid, true
} else {
return primitive.ObjectID{}, false
}
}

16
go.mod
View File

@@ -3,12 +3,22 @@ module gogs.mikescher.com/BlackForestBytes/goext
go 1.19 go 1.19
require ( require (
github.com/jmoiron/sqlx v1.3.5
go.mongodb.org/mongo-driver v1.11.1
golang.org/x/crypto v0.4.0
golang.org/x/sys v0.3.0 golang.org/x/sys v0.3.0
golang.org/x/term v0.3.0 golang.org/x/term v0.3.0
) )
require ( require (
github.com/jmoiron/sqlx v1.3.5 // indirect github.com/golang/snappy v0.0.1 // indirect
go.mongodb.org/mongo-driver v1.11.1 // indirect github.com/klauspost/compress v1.13.6 // indirect
golang.org/x/crypto v0.4.0 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.1 // indirect
github.com/xdg-go/stringprep v1.0.3 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/text v0.5.0 // indirect
) )

27
go.sum
View File

@@ -1,25 +1,43 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
go.mongodb.org/mongo-driver v1.11.1 h1:QP0znIRTuL0jf1oBQoAoM0C6ZJfBK4kx0Uumtv1A7w8= go.mongodb.org/mongo-driver v1.11.1 h1:QP0znIRTuL0jf1oBQoAoM0C6ZJfBK4kx0Uumtv1A7w8=
go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8=
@@ -27,24 +45,25 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -4,6 +4,12 @@ import (
"reflect" "reflect"
) )
// PTrue := &true
var PTrue = Ptr(true)
// PFalse := &false
var PFalse = Ptr(false)
func Ptr[T any](v T) *T { func Ptr[T any](v T) *T {
return &v return &v
} }

View File

@@ -4,7 +4,7 @@ import (
"reflect" "reflect"
) )
var reflectBasicTypes = []reflect.Type{ var reflectBasicTypes = map[reflect.Kind]reflect.Type{
reflect.Bool: reflect.TypeOf(false), reflect.Bool: reflect.TypeOf(false),
reflect.Int: reflect.TypeOf(int(0)), reflect.Int: reflect.TypeOf(int(0)),
reflect.Int8: reflect.TypeOf(int8(0)), reflect.Int8: reflect.TypeOf(int8(0)),
@@ -109,3 +109,28 @@ func TryCast[T any](v any) (T, bool) {
return r4, true return r4, true
} }
func TryCastType(v any, dest reflect.Type) (any, bool) {
underlying := Underlying(reflect.TypeOf(v))
if underlying != Underlying(dest) {
return nil, false
}
r1 := reflect.ValueOf(v)
if !r1.CanConvert(underlying) {
return nil, false
}
r2 := r1.Convert(underlying)
if !r2.CanConvert(dest) {
return nil, false
}
r4 := r2.Convert(dest)
return r4.Interface(), true
}

View File

@@ -1,6 +1,7 @@
package rfctime package rfctime
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"testing" "testing"
"time" "time"
) )

View File

@@ -81,7 +81,7 @@ func (t *RFC3339Time) UnmarshalBSONValue(bt bsontype.Type, data []byte) error {
return errors.New(fmt.Sprintf("cannot unmarshal %v into RFC3339Time", bt)) return errors.New(fmt.Sprintf("cannot unmarshal %v into RFC3339Time", bt))
} }
var tt time.Time var tt time.Time
err := bson.Unmarshal(data, &tt) err := bson.RawValue{Type: bt, Value: data}.Unmarshal(&tt)
if err != nil { if err != nil {
return err return err
} }
@@ -116,6 +116,12 @@ func (t RFC3339Time) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueRead
return err return err
} }
if val.Kind() == reflect.Ptr {
val.Set(reflect.ValueOf(&t))
} else {
val.Set(reflect.ValueOf(t))
}
return nil return nil
} }

91
sq/converter.go Normal file
View File

@@ -0,0 +1,91 @@
package sq
import (
"errors"
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"time"
)
//TODO UNFINISHED
// this is not finished
// idea was that we can register converter in the database struct
// they get inherited from the transactions
// and when marshallingunmarshaling (sq.Query | sq.QueryAll)
// or marshaling (sq.InsertSingle)
// the types get converter automatically...
type DBTypeConverter interface {
ModelTypeString() string
DBTypeString() string
ModelToDB(v any) (any, error)
DBToModel(v any) (any, error)
}
var ConverterBoolToBit = NewDBTypeConverter[bool, int](func(v bool) (int, error) {
return langext.Conditional(v, 1, 0), nil
}, func(v int) (bool, error) {
if v == 0 {
return false, nil
}
if v == 1 {
return true, nil
}
return false, errors.New(fmt.Sprintf("invalid valud for boolean: '%d'", v))
})
var ConverterTimeToUnixMillis = NewDBTypeConverter[time.Time, int64](func(v time.Time) (int64, error) {
return v.UnixMilli(), nil
}, func(v int64) (time.Time, error) {
return time.UnixMilli(v), nil
})
var ConverterOptTimeToUnixMillis = NewDBTypeConverter[*time.Time, *int64](func(v *time.Time) (*int64, error) {
if v == nil {
return nil, nil
}
return langext.Ptr(v.UnixMilli()), nil
}, func(v *int64) (*time.Time, error) {
if v == nil {
return nil, nil
}
return langext.Ptr(time.UnixMilli(*v)), nil
})
type dbTypeConverterImpl[TModelData any, TDBData any] struct {
dbTypeString string
modelTypeString string
todb func(v TModelData) (TDBData, error)
tomodel func(v TDBData) (TModelData, error)
}
func (t *dbTypeConverterImpl[TModelData, TDBData]) ModelTypeString() string {
return t.modelTypeString
}
func (t *dbTypeConverterImpl[TModelData, TDBData]) DBTypeString() string {
return t.dbTypeString
}
func (t *dbTypeConverterImpl[TModelData, TDBData]) ModelToDB(v any) (any, error) {
if vv, ok := v.(TModelData); ok {
return t.todb(vv)
}
return nil, errors.New(fmt.Sprintf("Unexpected value in DBTypeConverter, expected '%s', found '%T'", t.modelTypeString, v))
}
func (t *dbTypeConverterImpl[TModelData, TDBData]) DBToModel(v any) (any, error) {
if vv, ok := v.(TDBData); ok {
return t.tomodel(vv)
}
return nil, errors.New(fmt.Sprintf("Unexpected value in DBTypeConverter, expected '%s', found '%T'", t.dbTypeString, v))
}
func NewDBTypeConverter[TModelData any, TDBData any](todb func(v TModelData) (TDBData, error), tomodel func(v TDBData) (TModelData, error)) DBTypeConverter {
return &dbTypeConverterImpl[TModelData, TDBData]{
dbTypeString: fmt.Sprintf("%T", *new(TDBData)),
modelTypeString: fmt.Sprintf("%T", *new(TModelData)),
todb: todb,
tomodel: tomodel,
}
}

199
sq/hasher.go Normal file
View File

@@ -0,0 +1,199 @@
package sq
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/jmoiron/sqlx"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"os"
"path/filepath"
"strings"
)
func HashSqliteSchema(ctx context.Context, schemaStr string) (string, error) {
dbdir := os.TempDir()
dbfile1 := filepath.Join(dbdir, langext.MustHexUUID()+".sqlite3")
err := os.MkdirAll(dbdir, os.ModePerm)
if err != nil {
return "", err
}
url := fmt.Sprintf("file:%s?_journal=%s&_timeout=%d&_fk=%s&_busy_timeout=%d", dbfile1, "DELETE", 1000, "true", 1000)
xdb, err := sqlx.Open("sqlite3", url)
if err != nil {
return "", err
}
db := NewDB(xdb)
_, err = db.Exec(ctx, schemaStr, PP{})
if err != nil {
return "", err
}
return HashSqliteDatabase(ctx, db)
}
func HashSqliteDatabase(ctx context.Context, db DB) (string, error) {
ss, err := CreateSqliteDatabaseSchemaString(ctx, db)
if err != nil {
return "", err
}
cs := sha256.Sum256([]byte(ss))
return hex.EncodeToString(cs[:]), nil
}
func CreateSqliteDatabaseSchemaString(ctx context.Context, db DB) (string, error) {
type colInfo struct {
Name string `db:"name"`
Type string `db:"type"`
NotNull string `db:"notnull"`
Default *string `db:"dflt_value"`
PrimaryKey *string `db:"pk"`
}
type idxInfo struct {
Name string `json:"name" db:"name"`
Unique int `json:"unique" db:"unique"`
Origin string `json:"origin" db:"origin"`
Patial int `json:"partial" db:"partial"`
}
type fkyInfo struct {
TableDest string `json:"table_dest" db:"table"`
From string `json:"from" db:"from"`
To string `json:"to" db:"to"`
OnUpdate string `json:"on_update" db:"on_update"`
OnDelete string `json:"on_delete" db:"on_delete"`
Match string `json:"match" db:"match"`
}
type tabInfo struct {
Name string `json:"name" db:"name"`
Type string `json:"type" db:"type"`
NumCol int `json:"ncol" db:"ncol"`
Strict int `json:"strict" db:"strict"`
ColumnInfo []colInfo `json:"-"`
IndexInfo []idxInfo `json:"-"`
FKeyInfo []fkyInfo `json:"-"`
}
rowsTableList, err := db.Query(ctx, "PRAGMA table_list;", PP{})
if err != nil {
return "", err
}
tableList, err := ScanAll[tabInfo](rowsTableList, SModeFast, Unsafe, true)
if err != nil {
return "", err
}
langext.SortBy(tableList, func(v tabInfo) string { return v.Name })
result := make([]tabInfo, 0)
for i, tab := range tableList {
if strings.HasPrefix(tab.Name, "sqlite_") {
continue
}
{
rowsColumnList, err := db.Query(ctx, fmt.Sprintf("PRAGMA table_info(\"%s\");", tab.Name), PP{})
if err != nil {
return "", err
}
columnList, err := ScanAll[colInfo](rowsColumnList, SModeFast, Unsafe, true)
if err != nil {
return "", err
}
langext.SortBy(columnList, func(v colInfo) string { return v.Name })
tableList[i].ColumnInfo = columnList
}
{
rowsIdxList, err := db.Query(ctx, fmt.Sprintf("PRAGMA index_list(\"%s\");", tab.Name), PP{})
if err != nil {
return "", err
}
idxList, err := ScanAll[idxInfo](rowsIdxList, SModeFast, Unsafe, true)
if err != nil {
return "", err
}
langext.SortBy(idxList, func(v idxInfo) string { return v.Name })
tableList[i].IndexInfo = idxList
}
{
rowsIdxList, err := db.Query(ctx, fmt.Sprintf("PRAGMA foreign_key_list(\"%s\");", tab.Name), PP{})
if err != nil {
return "", err
}
fkyList, err := ScanAll[fkyInfo](rowsIdxList, SModeFast, Unsafe, true)
if err != nil {
return "", err
}
langext.SortBy(fkyList, func(v fkyInfo) string { return v.From })
tableList[i].FKeyInfo = fkyList
}
result = append(result, tableList[i])
}
strBuilderResult := ""
for _, vTab := range result {
jbinTable, err := json.Marshal(vTab)
if err != nil {
return "", err
}
strBuilderResult += fmt.Sprintf("#TABLE: %s\n{\n", string(jbinTable))
for _, vCol := range vTab.ColumnInfo {
jbinColumn, err := json.Marshal(vCol)
if err != nil {
return "", err
}
strBuilderResult += fmt.Sprintf(" COLUMN: %s\n", string(jbinColumn))
}
for _, vIdx := range vTab.IndexInfo {
jbinIndex, err := json.Marshal(vIdx)
if err != nil {
return "", err
}
strBuilderResult += fmt.Sprintf(" INDEX: %s\n", string(jbinIndex))
}
for _, vFky := range vTab.FKeyInfo {
jbinFKey, err := json.Marshal(vFky)
if err != nil {
return "", err
}
strBuilderResult += fmt.Sprintf(" FKEY: %s\n", string(jbinFKey))
}
strBuilderResult += "}\n\n"
}
return strBuilderResult, nil
}

View File

@@ -1,9 +1,13 @@
package sq package sq
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"reflect"
"strings"
) )
type StructScanMode string type StructScanMode string
@@ -16,10 +20,79 @@ const (
type StructScanSafety string type StructScanSafety string
const ( const (
Safe StructScanSafety = "SAFE" Safe StructScanSafety = "SAFE" // return error for missing fields
Unsafe StructScanSafety = "UNSAFE" Unsafe StructScanSafety = "UNSAFE" // ignore missing fields
) )
func InsertSingle[TData any](ctx context.Context, q Queryable, tableName string, v TData) (sql.Result, error) {
rval := reflect.ValueOf(v)
rtyp := rval.Type()
columns := make([]string, 0)
params := make([]string, 0)
pp := PP{}
for i := 0; i < rtyp.NumField(); i++ {
rsfield := rtyp.Field(i)
rvfield := rval.Field(i)
if !rsfield.IsExported() {
continue
}
columnName := rsfield.Tag.Get("db")
if columnName == "" || columnName == "-" {
continue
}
paramkey := fmt.Sprintf("_%s", columnName)
columns = append(columns, "\""+columnName+"\"")
params = append(params, ":"+paramkey)
pp[paramkey] = rvfield.Interface()
}
sqlstr := fmt.Sprintf("INSERT"+" INTO \"%s\" (%s) VALUES (%s)", tableName, strings.Join(columns, ", "), strings.Join(params, ", "))
sqlr, err := q.Exec(ctx, sqlstr, pp)
if err != nil {
return nil, err
}
return sqlr, nil
}
func QuerySingle[TData any](ctx context.Context, q Queryable, sql string, pp PP, mode StructScanMode, sec StructScanSafety) (TData, error) {
rows, err := q.Query(ctx, sql, pp)
if err != nil {
return *new(TData), err
}
data, err := ScanSingle[TData](rows, mode, sec, true)
if err != nil {
return *new(TData), err
}
return data, nil
}
func QueryAll[TData any](ctx context.Context, q Queryable, sql string, pp PP, mode StructScanMode, sec StructScanSafety) ([]TData, error) {
rows, err := q.Query(ctx, sql, pp)
if err != nil {
return nil, err
}
data, err := ScanAll[TData](rows, mode, sec, true)
if err != nil {
return nil, err
}
return data, nil
}
func ScanSingle[TData any](rows *sqlx.Rows, mode StructScanMode, sec StructScanSafety, close bool) (TData, error) { func ScanSingle[TData any](rows *sqlx.Rows, mode StructScanMode, sec StructScanSafety, close bool) (TData, error) {
if rows.Next() { if rows.Next() {
var strscan *StructScanner var strscan *StructScanner

66
wmo/collection.go Normal file
View File

@@ -0,0 +1,66 @@
package wmo
import (
"context"
"go.mongodb.org/mongo-driver/mongo"
ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"reflect"
)
type EntityID = any
type fullTypeRef[TData any] struct {
IsPointer bool
Kind reflect.Kind
Type reflect.Type
UnderlyingType reflect.Type
Name string
Index []int
}
type Coll[TData any] struct {
coll *mongo.Collection
dataTypeMap map[string]fullTypeRef[TData]
}
func (c *Coll[TData]) Collection() *mongo.Collection {
return c.coll
}
func (c *Coll[TData]) Name() string {
return c.coll.Name()
}
func (c *Coll[TData]) Indexes() mongo.IndexView {
return c.coll.Indexes()
}
func (c *Coll[TData]) Drop(ctx context.Context) error {
return c.coll.Drop(ctx)
}
func (c *Coll[TData]) createToken(fieldPrimary string, dirPrimary ct.SortDirection, fieldSecondary *string, dirSecondary *ct.SortDirection, lastEntity TData, pageSize *int) (ct.CursorToken, error) {
valuePrimary, err := c.getFieldValueAsTokenString(lastEntity, fieldPrimary)
if err != nil {
return ct.CursorToken{}, err
}
valueSeconary := ""
if fieldSecondary != nil && dirSecondary != nil {
valueSeconary, err = c.getFieldValueAsTokenString(lastEntity, *fieldSecondary)
if err != nil {
return ct.CursorToken{}, err
}
}
return ct.CursorToken{
Mode: ct.CTMNormal,
ValuePrimary: valuePrimary,
ValueSecondary: valueSeconary,
Direction: dirPrimary,
PageSize: langext.Coalesce(pageSize, 0),
Extra: ct.Extra{},
}, nil
}

11
wmo/mongo.go Normal file
View File

@@ -0,0 +1,11 @@
package wmo
import "go.mongodb.org/mongo-driver/mongo"
func W[TData any](collection *mongo.Collection) *Coll[TData] {
c := Coll[TData]{coll: collection}
c.init()
return &c
}

69
wmo/pagination.go Normal file
View File

@@ -0,0 +1,69 @@
package wmo
import (
"go.mongodb.org/mongo-driver/bson"
ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken"
)
func CreatePagination[TData any](coll *Coll[TData], token ct.CursorToken, fieldPrimary string, sortPrimary ct.SortDirection, fieldSecondary *string, sortSecondary *ct.SortDirection, pageSize *int) ([]bson.D, error) {
cond := bson.A{}
sort := bson.D{}
valuePrimary, err := coll.getTokenValueAsMongoType(token.ValuePrimary, fieldPrimary)
if err != nil {
return nil, err
}
if sortPrimary == ct.SortASC {
// We sort ASC on <field> - so we want all entries newer ($gt) than the $primary
cond = append(cond, bson.M{fieldPrimary: bson.M{"$gt": valuePrimary}})
sort = append(sort, bson.E{Key: fieldPrimary, Value: +1})
} else if sortPrimary == ct.SortDESC {
// We sort DESC on <field> - so we want all entries older ($lt) than the $primary
cond = append(cond, bson.M{fieldPrimary: bson.M{"$lt": valuePrimary}})
sort = append(sort, bson.E{Key: fieldPrimary, Value: -1})
}
if fieldSecondary != nil && sortSecondary != nil && *fieldSecondary != fieldPrimary {
valueSecondary, err := coll.getTokenValueAsMongoType(token.ValueSecondary, *fieldSecondary)
if err != nil {
return nil, err
}
if *sortSecondary == ct.SortASC {
// the conflict-resolution condition, for entries with the _same_ <field> as the $primary we take the ones with a greater $secondary (= newer)
cond = append(cond, bson.M{"$and": bson.A{
bson.M{fieldPrimary: valuePrimary},
bson.M{*fieldSecondary: bson.M{"$gt": valueSecondary}},
}})
sort = append(sort, bson.E{Key: fieldPrimary, Value: +1})
} else if *sortSecondary == ct.SortDESC {
// the conflict-resolution condition, for entries with the _same_ <field> as the $primary we take the ones with a smaller $secondary (= older)
cond = append(cond, bson.M{"$and": bson.A{
bson.M{fieldPrimary: valuePrimary},
bson.M{*fieldSecondary: bson.M{"$lt": valueSecondary}},
}})
sort = append(sort, bson.E{Key: fieldPrimary, Value: -1})
}
}
pipeline := make([]bson.D, 0, 3)
pipeline = append(pipeline, bson.D{{Key: "$match", Value: bson.M{"$or": cond}}})
pipeline = append(pipeline, bson.D{{Key: "$sort", Value: sort}})
if pageSize != nil {
pipeline = append(pipeline, bson.D{{Key: "$limit", Value: int64(*pageSize + 1)}})
}
return pipeline, nil
}

22
wmo/queryAggregate.go Normal file
View File

@@ -0,0 +1,22 @@
package wmo
import (
"context"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func (c *Coll[TData]) Aggregate(ctx context.Context, pipeline mongo.Pipeline, opts ...*options.AggregateOptions) ([]TData, error) {
cursor, err := c.coll.Aggregate(ctx, pipeline, opts...)
if err != nil {
return nil, err
}
res := make([]TData, 0, cursor.RemainingBatchLength())
err = cursor.All(ctx, &res)
if err != nil {
return nil, err
}
return res, nil
}

19
wmo/queryDelete.go Normal file
View File

@@ -0,0 +1,19 @@
package wmo
func (c *Coll[TData]) DeleteOne(ctx context.Context, id EntityID) error {
_, err := c.coll.DeleteOne(ctx, bson.M{"_id": id})
if err != nil {
return err
}
return nil
}
func (c *Coll[TData]) DeleteMany(ctx context.Context, filterQuery bson.M) (*mongo.DeleteResult, error) {
res, err := c.coll.DeleteMany(ctx, filterQuery)
if err != nil {
return nil, err
}
return res, nil
}

73
wmo/queryFind.go Normal file
View File

@@ -0,0 +1,73 @@
package wmo
import (
"context"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func (c *Coll[TData]) FindOne(ctx context.Context, filter bson.M) (TData, error) {
var res TData
err := c.coll.FindOne(ctx, filter).Decode(&res)
if err != nil {
return *new(TData), err
}
return res, nil
}
func (c *Coll[TData]) FindOneOpt(ctx context.Context, filter bson.M) (*TData, error) {
var res TData
err := c.coll.FindOne(ctx, filter).Decode(&res)
if err == mongo.ErrNoDocuments {
return nil, nil
}
if err != nil {
return nil, err
}
return &res, nil
}
func (c *Coll[TData]) FindOneByID(ctx context.Context, id EntityID) (TData, error) {
var res TData
err := c.coll.FindOne(ctx, bson.M{"_id": id}).Decode(&res)
if err != nil {
return *new(TData), err
}
return res, nil
}
func (c *Coll[TData]) FindOneOptByID(ctx context.Context, id EntityID) (*TData, error) {
var res TData
err := c.coll.FindOne(ctx, bson.M{"_id": id}).Decode(&res)
if err == mongo.ErrNoDocuments {
return nil, nil
}
if err != nil {
return nil, err
}
return &res, nil
}
func (c *Coll[TData]) Find(ctx context.Context, filter bson.M, opts ...*options.FindOptions) ([]TData, error) {
cursor, err := c.coll.Find(ctx, filter, opts...)
if err != nil {
return nil, err
}
res := make([]TData, 0, cursor.RemainingBatchLength())
err = cursor.All(ctx, &res)
if err != nil {
return nil, err
}
return res, nil
}

22
wmo/queryInsert.go Normal file
View File

@@ -0,0 +1,22 @@
package wmo
import (
"context"
"go.mongodb.org/mongo-driver/bson"
)
func (c *Coll[TData]) InsertOne(ctx context.Context, valueIn TData) (TData, error) {
insRes, err := c.coll.InsertOne(ctx, valueIn)
if err != nil {
return *new(TData), err
}
var res TData
err = c.coll.FindOne(ctx, bson.M{"_id": insRes.InsertedID}).Decode(&res)
if err != nil {
return *new(TData), err
}
return res, nil
}

75
wmo/queryList.go Normal file
View File

@@ -0,0 +1,75 @@
package wmo
import (
"context"
"go.mongodb.org/mongo-driver/bson"
ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken"
)
func (c *Coll[TData]) List(ctx context.Context, filter ct.Filter, pageSize *int, inTok ct.CursorToken) ([]TData, ct.CursorToken, error) {
if inTok.Mode == ct.CTMEnd {
return make([]TData, 0), ct.End(), nil
}
pipeline := filter.FilterQuery()
sortPrimary, sortDirPrimary, sortSecondary, sortDirSecondary := filter.Pagination()
paginationPipeline, err := CreatePagination(c, inTok, sortPrimary, sortDirPrimary, sortSecondary, sortDirSecondary, pageSize)
if err != nil {
return nil, ct.CursorToken{}, err
}
pipeline = append(pipeline, paginationPipeline...)
cursor, err := c.coll.Aggregate(ctx, pipeline)
if err != nil {
return nil, ct.CursorToken{}, err
}
entities := make([]TData, 0, cursor.RemainingBatchLength()+1)
for (pageSize == nil || len(entities) != *pageSize) && cursor.Next(ctx) {
var entry TData
err = cursor.Decode(&entry)
if err != nil {
return nil, ct.CursorToken{}, err
}
entities = append(entities, entry)
}
if pageSize == nil || len(entities) <= *pageSize || !cursor.TryNext(ctx) {
return entities, ct.End(), nil
}
last := entities[len(entities)-1]
nextToken, _ := c.createToken(sortPrimary, sortDirPrimary, sortSecondary, sortDirSecondary, last, pageSize)
return entities, nextToken, nil
}
func (c *Coll[TData]) Count(ctx context.Context, filter ct.Filter) (int64, error) {
pipeline := filter.FilterQuery()
pipeline = append(pipeline, bson.D{{Key: "$count", Value: "c"}})
cursor, err := c.coll.Aggregate(ctx, pipeline)
if err != nil {
return 0, err
}
type res struct {
Count int64 `bson:"c"`
}
if cursor.Next(ctx) {
v := res{}
err = cursor.Decode(&v)
if err != nil {
return 0, err
}
return v.Count, nil
}
return 0, nil
}

56
wmo/queryUpdate.go Normal file
View File

@@ -0,0 +1,56 @@
package wmo
import (
"context"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
func (c *Coll[TData]) FindOneAndUpdate(ctx context.Context, filterQuery bson.M, updateQuery bson.M) (TData, error) {
var res TData
err := c.coll.FindOneAndUpdate(ctx, filterQuery, updateQuery, options.FindOneAndUpdate().SetReturnDocument(options.After)).Decode(&res)
if err != nil {
return *new(TData), err
}
return res, nil
}
func (c *Coll[TData]) UpdateOne(ctx context.Context, filterQuery bson.M, updateQuery bson.M) error {
_, err := c.coll.UpdateOne(ctx, filterQuery, updateQuery)
if err != nil {
return err
}
return nil
}
func (c *Coll[TData]) UpdateOneByID(ctx context.Context, id EntityID, updateQuery bson.M) error {
_, err := c.coll.UpdateOne(ctx, bson.M{"_id": id}, updateQuery)
if err != nil {
return err
}
return nil
}
func (c *Coll[TData]) ReplaceOne(ctx context.Context, id EntityID, value TData) error {
_, err := c.coll.UpdateOne(ctx, bson.M{"_id": id}, value)
if err != nil {
return err
}
return nil
}
func (c *Coll[TData]) FindOneAndReplace(ctx context.Context, id EntityID, value TData) (TData, error) {
var res TData
err := c.coll.FindOneAndUpdate(ctx, bson.M{"_id": id}, value, options.FindOneAndUpdate().SetReturnDocument(options.After)).Decode(&res)
if err != nil {
return *new(TData), err
}
return res, nil
}

294
wmo/reflection.go Normal file
View File

@@ -0,0 +1,294 @@
package wmo
import (
"errors"
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"reflect"
"strconv"
"time"
)
func (c *Coll[TData]) init() {
c.dataTypeMap = make(map[string]fullTypeRef[TData])
example := *new(TData)
v := reflect.ValueOf(example)
c.initFields("", v, make([]int, 0))
}
func (c *Coll[TData]) initFields(prefix string, rval reflect.Value, idxarr []int) {
rtyp := rval.Type()
for i := 0; i < rtyp.NumField(); i++ {
rsfield := rtyp.Field(i)
rvfield := rval.Field(i)
if !rsfield.IsExported() {
continue
}
bsonkey, found := rsfield.Tag.Lookup("bson")
if !found || bsonkey == "-" {
continue
}
fullKey := prefix + bsonkey
newIdxArr := langext.ArrCopy(idxarr)
newIdxArr = append(newIdxArr, i)
if rvfield.Type().Kind() == reflect.Pointer {
c.dataTypeMap[fullKey] = fullTypeRef[TData]{
IsPointer: true,
Kind: rvfield.Type().Elem().Kind(),
Type: rvfield.Type().Elem(),
UnderlyingType: langext.Underlying(rvfield.Type().Elem()),
Name: rsfield.Name,
Index: newIdxArr,
}
} else {
c.dataTypeMap[fullKey] = fullTypeRef[TData]{
IsPointer: false,
Kind: rvfield.Type().Kind(),
Type: rvfield.Type(),
UnderlyingType: langext.Underlying(rvfield.Type()),
Name: rsfield.Name,
Index: newIdxArr,
}
}
if rvfield.Kind() == reflect.Struct {
c.initFields(fullKey+".", rvfield, newIdxArr)
}
}
}
func (c *Coll[TData]) getTokenValueAsMongoType(value string, fieldName string) (any, error) {
fref := c.dataTypeMap[fieldName]
if fref.IsPointer && value == "" {
pointertype := reflect.New(fref.Type).Type()
nilvalue := reflect.Zero(pointertype)
outvalue := nilvalue.Interface()
return outvalue, nil
}
pointerize := func(v any) any {
if !fref.IsPointer {
return v
}
rval1 := reflect.ValueOf(v)
rval2 := rval1.Convert(fref.Type)
rval3 := reflect.New(fref.Type)
rval3.Elem().Set(rval2)
return rval3.Interface()
}
if fref.UnderlyingType == reflect.TypeOf("") {
rt, ok := langext.TryCastType(value, fref.Type)
if !ok {
return nil, errors.New(fmt.Sprintf("failed to parse field '%s' from string to %s", fieldName, fref.Type.String()))
}
return pointerize(rt), nil
}
if fref.UnderlyingType == reflect.TypeOf(time.Time{}) {
t, err := time.Parse(time.RFC3339Nano, value)
if err != nil {
return nil, errors.New(fmt.Sprintf("failed to parse field '%s' as time.Time ('%s')", fieldName, value))
}
rt, ok := langext.TryCastType(t, fref.Type)
if !ok {
return nil, errors.New(fmt.Sprintf("failed to parse field '%s' from time.Time to %s", fieldName, fref.Type.String()))
}
return pointerize(rt), nil
}
if fref.UnderlyingType == reflect.TypeOf(int(0)) {
t, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return nil, errors.New(fmt.Sprintf("failed to parse field '%s' as int64 ('%s')", fieldName, value))
}
rt, ok := langext.TryCastType(int(t), fref.Type)
if !ok {
return nil, errors.New(fmt.Sprintf("failed to parse field '%s' from int to %s", fieldName, fref.Type.String()))
}
return pointerize(rt), nil
}
if fref.UnderlyingType == reflect.TypeOf(int32(0)) {
t, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return nil, errors.New(fmt.Sprintf("failed to parse field '%s' as int32 ('%s')", fieldName, value))
}
rt, ok := langext.TryCastType(int32(t), fref.Type)
if !ok {
return nil, errors.New(fmt.Sprintf("failed to parse field '%s' from int32 to %s", fieldName, fref.Type.String()))
}
return pointerize(rt), nil
}
if fref.UnderlyingType == reflect.TypeOf(int64(0)) {
t, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return nil, errors.New(fmt.Sprintf("failed to parse field '%s' as int64 ('%s')", fieldName, value))
}
rt, ok := langext.TryCastType(int64(t), fref.Type)
if !ok {
return nil, errors.New(fmt.Sprintf("failed to parse field '%s' from int64 to %s", fieldName, fref.Type.String()))
}
return pointerize(rt), nil
}
if fref.UnderlyingType == reflect.TypeOf(float32(0)) {
t, err := strconv.ParseFloat(value, 64)
if err != nil {
return nil, errors.New(fmt.Sprintf("failed to parse field '%s' as float32 ('%s')", fieldName, value))
}
rt, ok := langext.TryCastType(float32(t), fref.Type)
if !ok {
return nil, errors.New(fmt.Sprintf("failed to parse field '%s' from float32 to %s", fieldName, fref.Type.String()))
}
return pointerize(rt), nil
}
if fref.UnderlyingType == reflect.TypeOf(float64(0)) {
t, err := strconv.ParseFloat(value, 64)
if err != nil {
return nil, errors.New(fmt.Sprintf("failed to parse field '%s' as float64 ('%s')", fieldName, value))
}
rt, ok := langext.TryCastType(float64(t), fref.Type)
if !ok {
return nil, errors.New(fmt.Sprintf("failed to parse field '%s' from float64 to %s", fieldName, fref.Type.String()))
}
return pointerize(rt), nil
}
return nil, errors.New(fmt.Sprintf("failed to parse field '%s' of type %s (%s)", fieldName, fref.Type.String(), fref.UnderlyingType.String()))
}
func (c *Coll[TData]) getFieldValueAsTokenString(entity TData, fieldName string) (string, error) {
fref := c.dataTypeMap[fieldName]
realValue := c.getFieldValue(entity, fieldName)
if langext.IsNil(realValue) {
return "", nil
}
reflValue := reflect.ValueOf(realValue)
if reflValue.Kind() == reflect.Pointer {
reflValue = reflValue.Elem()
realValue = reflValue.Interface()
}
if fref.UnderlyingType == reflect.TypeOf("") {
rt, ok := langext.TryCastType(realValue, reflect.TypeOf(""))
if !ok {
return "", errors.New(fmt.Sprintf("failed to cast field '%s' from %s to string", fieldName, fref.Type.String()))
}
return rt.(string), nil
}
if fref.UnderlyingType == reflect.TypeOf(time.Time{}) {
rt, ok := langext.TryCastType(realValue, reflect.TypeOf(time.Time{}))
if !ok {
return "", errors.New(fmt.Sprintf("failed to cast field '%s' from %s to time.Time", fieldName, fref.Type.String()))
}
return rt.(time.Time).Format(time.RFC3339Nano), nil
}
if fref.UnderlyingType == reflect.TypeOf(int(0)) {
rt, ok := langext.TryCastType(realValue, reflect.TypeOf(int(0)))
if !ok {
return "", errors.New(fmt.Sprintf("failed to cast field '%s' from %s to int", fieldName, fref.Type.String()))
}
return strconv.Itoa(rt.(int)), nil
}
if fref.UnderlyingType == reflect.TypeOf(int32(0)) {
rt, ok := langext.TryCastType(realValue, reflect.TypeOf(int32(0)))
if !ok {
return "", errors.New(fmt.Sprintf("failed to cast field '%s' from %s to int32", fieldName, fref.Type.String()))
}
return strconv.FormatInt(int64(rt.(int32)), 10), nil
}
if fref.UnderlyingType == reflect.TypeOf(int64(0)) {
rt, ok := langext.TryCastType(realValue, reflect.TypeOf(int64(0)))
if !ok {
return "", errors.New(fmt.Sprintf("failed to cast field '%s' from %s to int64", fieldName, fref.Type.String()))
}
return strconv.FormatInt(rt.(int64), 10), nil
}
if fref.UnderlyingType == reflect.TypeOf(float32(0)) {
rt, ok := langext.TryCastType(realValue, reflect.TypeOf(float32(0)))
if !ok {
return "", errors.New(fmt.Sprintf("failed to cast field '%s' from %s to float32", fieldName, fref.Type.String()))
}
return strconv.FormatFloat(float64(rt.(float32)), 'f', -1, 32), nil
}
if fref.UnderlyingType == reflect.TypeOf(float64(0)) {
rt, ok := langext.TryCastType(realValue, reflect.TypeOf(float64(0)))
if !ok {
return "", errors.New(fmt.Sprintf("failed to cast field '%s' from %s to float64", fieldName, fref.Type.String()))
}
return strconv.FormatFloat(rt.(float64), 'f', -1, 64), nil
}
return "", errors.New(fmt.Sprintf("failed to parse field '%s' of type %s (%s)", fieldName, fref.Type.String(), fref.UnderlyingType.String()))
}
func (c *Coll[TData]) getFieldType(fieldName string) fullTypeRef[TData] {
return c.dataTypeMap[fieldName]
}
func (c *Coll[TData]) getFieldValue(data TData, fieldName string) any {
fref := c.dataTypeMap[fieldName]
rval := reflect.ValueOf(data)
return rval.FieldByIndex(fref.Index).Interface()
}

160
wmo/reflection_test.go Normal file
View File

@@ -0,0 +1,160 @@
package wmo
import (
"go.mongodb.org/mongo-driver/mongo"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"testing"
"time"
)
func TestReflectionGetFieldType(t *testing.T) {
type IDType string
type TestData struct {
ID IDType `bson:"_id"`
CDate time.Time `bson:"cdate"`
Sub struct {
A string `bson:"a"`
} `bson:"sub"`
Str string `bson:"str"`
Ptr *int `bson:"ptr"`
}
coll := W[TestData](&mongo.Collection{})
coll.init()
t0 := time.Now()
d := TestData{
ID: "1",
CDate: t0,
Sub: struct {
A string `bson:"a"`
}{
A: "2",
},
Str: "3",
Ptr: langext.Ptr(4),
}
tst.AssertEqual(t, coll.getFieldType("_id").Kind.String(), "string")
tst.AssertEqual(t, coll.getFieldType("_id").Type.String(), "wmo.IDType")
tst.AssertEqual(t, coll.getFieldType("_id").Name, "ID")
tst.AssertEqual(t, coll.getFieldType("_id").IsPointer, false)
tst.AssertEqual(t, coll.getFieldValue(d, "_id").(IDType), "1")
tst.AssertEqual(t, coll.getFieldType("cdate").Kind.String(), "struct")
tst.AssertEqual(t, coll.getFieldType("cdate").Type.String(), "time.Time")
tst.AssertEqual(t, coll.getFieldType("cdate").Name, "CDate")
tst.AssertEqual(t, coll.getFieldType("cdate").IsPointer, false)
tst.AssertEqual(t, coll.getFieldValue(d, "cdate").(time.Time), t0)
tst.AssertEqual(t, coll.getFieldType("sub.a").Kind.String(), "string")
tst.AssertEqual(t, coll.getFieldType("sub.a").Type.String(), "string")
tst.AssertEqual(t, coll.getFieldType("sub.a").Name, "A")
tst.AssertEqual(t, coll.getFieldType("sub.a").IsPointer, false)
tst.AssertEqual(t, coll.getFieldValue(d, "sub.a").(string), "2")
tst.AssertEqual(t, coll.getFieldType("str").Kind.String(), "string")
tst.AssertEqual(t, coll.getFieldType("str").Type.String(), "string")
tst.AssertEqual(t, coll.getFieldType("str").Name, "Str")
tst.AssertEqual(t, coll.getFieldType("str").IsPointer, false)
tst.AssertEqual(t, coll.getFieldValue(d, "str").(string), "3")
tst.AssertEqual(t, coll.getFieldType("ptr").Kind.String(), "int")
tst.AssertEqual(t, coll.getFieldType("ptr").Type.String(), "int")
tst.AssertEqual(t, coll.getFieldType("ptr").Name, "Ptr")
tst.AssertEqual(t, coll.getFieldType("ptr").IsPointer, true)
tst.AssertEqual(t, *coll.getFieldValue(d, "ptr").(*int), 4)
}
func TestReflectionGetTokenValueAsMongoType(t *testing.T) {
type IDType string
type TestData struct {
ID IDType `bson:"_id"`
CDate time.Time `bson:"cdate"`
Sub struct {
A string `bson:"a"`
} `bson:"sub"`
Str string `bson:"str"`
Ptr *int `bson:"ptr"`
Num int `bson:"num"`
}
coll := W[TestData](&mongo.Collection{})
coll.init()
gtvasmt := func(value string, fieldName string) any {
v, err := coll.getTokenValueAsMongoType(value, fieldName)
if err != nil {
t.Errorf("%s", "failed to getTokenValueAsMongoType")
}
return v
}
tst.AssertEqual(t, gtvasmt("hello", "str").(string), "hello")
tst.AssertEqual(t, gtvasmt("4", "num").(int), 4)
tst.AssertEqual(t, gtvasmt("asdf", "_id").(IDType), "asdf")
tst.AssertEqual(t, gtvasmt("", "ptr").(*int), nil)
tst.AssertEqual(t, *(gtvasmt("123", "ptr").(*int)), 123)
}
func TestReflectionGetFieldValueAsTokenString(t *testing.T) {
type IDType string
type TestData struct {
ID IDType `bson:"_id"`
CDate time.Time `bson:"cdate"`
Sub struct {
A string `bson:"a"`
} `bson:"sub"`
Str string `bson:"str"`
Ptr *int `bson:"ptr"`
Num int `bson:"num"`
Ptr2 *int `bson:"ptr2"`
FFF float64 `bson:"fff"`
}
coll := W[TestData](&mongo.Collection{})
coll.init()
t0 := time.Now()
d := TestData{
ID: "1",
CDate: t0,
Sub: struct {
A string `bson:"a"`
}{
A: "2",
},
Str: "3",
Ptr: langext.Ptr(4),
Num: 22,
FFF: 22.5,
Ptr2: nil,
}
gfvats := func(value TestData, fieldName string) string {
v, err := coll.getFieldValueAsTokenString(value, fieldName)
if err != nil {
t.Errorf("%s", "failed to getTokenValueAsMongoType")
}
return v
}
tst.AssertEqual(t, gfvats(d, "str"), "3")
tst.AssertEqual(t, gfvats(d, "num"), "22")
tst.AssertEqual(t, gfvats(d, "_id"), "1")
tst.AssertEqual(t, gfvats(d, "ptr"), "4")
tst.AssertEqual(t, gfvats(d, "ptr2"), "")
tst.AssertEqual(t, gfvats(d, "fff"), "22.5")
}