18 Commits

Author SHA1 Message Date
77b8bdf218 removing driver, just generating mock struct args would need an order 2025-09-30 17:48:22 -04:00
2161c201bc reimplementing db tag 2025-09-30 17:39:40 -04:00
935285a9f3 testing without db tags 2025-09-30 17:39:15 -04:00
5f24529730 proper slice allocation 2025-09-30 17:32:27 -04:00
5563e93efd working locally 2025-09-30 17:23:31 -04:00
d0ec9465e2 testing new length 2025-09-30 17:17:45 -04:00
0c4a1abb9a testing any typecast 2025-09-30 17:15:45 -04:00
791ffad0e3 testing 2025-09-30 17:13:52 -04:00
2415af38b2 Fixing struct generation
- ready to test
- slice size bugfixes
- appending empty struct before filling it out
2025-09-30 17:09:19 -04:00
053f5b5fd8 no more index check 2025-09-30 16:25:54 -04:00
6326711424 adding ok check to prevent panics 2025-09-30 11:47:38 -04:00
6a9cdafcf1 testing better types 2025-09-30 11:43:40 -04:00
5e2ddecdcc dont elem slice 2025-09-30 11:41:28 -04:00
9033530a55 testing any type for resturned structs 2025-09-30 11:33:59 -04:00
6bbb452b03 argument type fix 2025-09-30 11:26:54 -04:00
21b1291dbc struct name revert 2025-09-30 11:20:30 -04:00
c62da47a31 ready for tests 2025-09-30 11:12:47 -04:00
ff62b371c9 adding new functionality for mocks
- working through insert logic
- ideally return struct with 'good' fields
- also return list of args to prevent relooping
2025-09-29 15:47:51 -04:00
3 changed files with 107 additions and 113 deletions

10
.gitignore vendored
View File

@@ -15,7 +15,15 @@
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file # env file
.env .env
cmd/* #ignore test files
cmd/

View File

@@ -13,7 +13,7 @@ Lazy is a helper tool when working with [sqlx](https://github.com/jmoiron/sqlx)
<hr /> <hr />
## RandomGenerate ## RandomGenerate
Generates random data for basic types and slices to be used with [sqlmock](https://github.com/DATA-DOG/go-sqlmock). Slices are given an arbitrary amount entries See [Mocks](#mocks) for more information. Lazy supports many bind types using sqlx's Rebind method, however, due to this conversion queries that use operators like postgres' jsonb "?" operator cannot be paired with the BindQuestion option as sqlx will greedily replace all "?" causing syntax errors Generates random data for basic types and slices to be used with [sqlmock](https://github.com/DATA-DOG/go-sqlmock). Slices are given an arbitrary amount entries See [Mocks](#mocks) for more information.
```go ```go
func RandomGenerate(m Config) (*Mock, error) func RandomGenerate(m Config) (*Mock, error)
``` ```
@@ -74,7 +74,6 @@ func ExampleTest() {
Example: SqlResultsExample{}, Example: SqlResultsExample{},
Keys: []any{ 1, 6 }, Keys: []any{ 1, 6 },
RowCount: 2, RowCount: 2,
BindType: lazy.BindQuestion,
} }
mock, err := lazy.GenerateRandom(cfg) mock, err := lazy.GenerateRandom(cfg)
if err != nil { if err != nil {

205
lazy.go
View File

@@ -1,14 +1,12 @@
package lazy package lazy
import ( import (
"database/sql"
"database/sql/driver" "database/sql/driver"
"errors" "errors"
"fmt" "fmt"
"math/rand/v2" "math/rand/v2"
"reflect" "reflect"
"regexp" "regexp"
"time"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
@@ -17,61 +15,71 @@ const (
DB_TAG = "db" //tag used to parse sql fields in example struct DB_TAG = "db" //tag used to parse sql fields in example struct
LAZY_TAG = "lazy" //tag label for KEY_VALUE LAZY_TAG = "lazy" //tag label for KEY_VALUE
KEY_VALUE = "key" //tag value for LAZY_TAG KEY_VALUE = "key" //tag value for LAZY_TAG
//bind values
BindQuestion Binds = 1
BindDollar Binds = 2
BindNamed Binds = 3
BindAt Binds = 4
) )
type Binds int var (
ErrBadExample = errors.New("example field must be a non-nil struct")
ErrMissingKeys = errors.New("there must be a key for each row requested with RowCount")
//contains placeholder to inject type on runtime
ErrUnsupportedType = errors.New("type: %s is not yet supported")
)
// mock data generated based on config // mock data generated based on config
type Mock struct { type RowMock struct {
Query string Query string
Columns []string Columns []string
Rows [][]driver.Value Rows [][]driver.Value
} }
// configuration for RandomGenerate // configuration for RandomGenerate
type Config struct { type RowConfig struct {
Query string Query string
Example any Example any
Keys []any //length of keys must = RowCount, if set Keys []any
RowCount int RowCount int
BindType Binds }
type StructConfig struct {
Query string
Example any
RowCount int
}
type StructMock struct {
Query string
MockStructs []any
} }
// generates mock data based on configuration to be used for sqlmock // generates mock data based on configuration to be used for sqlmock
func RandomGenerate(m Config) (*Mock, error) { func RandomRows(r RowConfig) (*RowMock, error) {
//example struct cannot be nil and must be a struct //example struct cannot be nil and must be a struct
if m.Example == nil { if r.Example == nil {
return nil, errors.New("example value cannot be nil") return nil, ErrBadExample
} }
if reflect.ValueOf(m.Example).Kind() != reflect.Struct { if reflect.ValueOf(r.Example).Kind() != reflect.Struct {
return nil, errors.New("example value must be a struct") return nil, ErrBadExample
} }
//any weirdness, just pull one row //any weirdness, just pull one row
if m.RowCount <= 0 { if r.RowCount <= 0 {
m.RowCount = 1 r.RowCount = 1
} }
//if keys are set, ensure there are enough to populate requested rows //if keys are set, ensure there are enough to populate requested rows
primaryKey := false primaryKey := false
if len(m.Keys) > 0 { if len(r.Keys) > 0 {
primaryKey = true primaryKey = true
if len(m.Keys) != m.RowCount { if len(r.Keys) != r.RowCount {
return nil, errors.New("you must provide a key for each row") return nil, ErrMissingKeys
} }
} }
retType := reflect.TypeOf(m.Example) retType := reflect.TypeOf(r.Example)
maxFieldCount := retType.NumField() maxFieldCount := retType.NumField()
columns := make([]string, 0, maxFieldCount) columns := make([]string, 0, maxFieldCount)
rows := make([][]driver.Value, 0) rows := make([][]driver.Value, 0)
for y := 0; y < m.RowCount; y++ { for y := 0; y < r.RowCount; y++ {
rows = append(rows, make([]driver.Value, 0)) rows = append(rows, make([]driver.Value, 0))
for x := 0; x < maxFieldCount; x++ { for x := 0; x < maxFieldCount; x++ {
field := retType.Field(x) field := retType.Field(x)
@@ -87,94 +95,86 @@ func RandomGenerate(m Config) (*Mock, error) {
//if field has lazy:"key" tag and tags are present inject primary key //if field has lazy:"key" tag and tags are present inject primary key
if field.Tag.Get(LAZY_TAG) == KEY_VALUE && primaryKey { if field.Tag.Get(LAZY_TAG) == KEY_VALUE && primaryKey {
rows[y] = append(rows[y], m.Keys[y]) rows[y] = append(rows[y], r.Keys[y])
continue continue
} }
//generate random values
nv := kindToRandom(field) nv := kindToRandom(field)
if nv == nil { if nv == nil {
return nil, fmt.Errorf("could not match type: %s", retType.Name()) return nil, fmt.Errorf(ErrUnsupportedType.Error(), field.Type.Name())
} }
rows[y] = append(rows[y], nv) rows[y] = append(rows[y], nv)
} }
} }
return &Mock{ return &RowMock{
//sql is rebound and escaped for sqlmock.ExpectQuery //sql is rebound and escaped for sqlmock.ExpectQuery
Query: sqlx.Rebind(int(m.BindType), regexp.QuoteMeta(m.Query)), Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(r.Query)),
Columns: columns, Columns: columns,
Rows: rows, Rows: rows,
}, nil }, nil
} }
// converts basic reflect Kind's to psuedo-random data, slices are given a random amount of entries // generates mock data for insert statements to be used for sqlmock, it also returns a slice of
func kindToRandom(field reflect.StructField) any { // example structs with mock data
//this isn't ideal, but since the sql types are structs they were being filled in a weird func RandomStruct(c StructConfig) (*StructMock, error) {
//manner. For now we check those types first, and will clean this up later. //make sure we have an example struct
switch field.Type { if c.Example == nil {
case reflect.TypeOf(sql.NullTime{}): return nil, ErrBadExample
var temp sql.NullTime
if !isNull() {
temp.Valid = true
temp.Time = time.Now()
}
return temp
case reflect.TypeOf(sql.NullInt16{}):
var temp sql.NullInt16
if isNull() {
temp.Valid = false
} else {
temp.Valid = true
temp.Int16 = int16(rand.Int())
}
return temp
case reflect.TypeOf(sql.NullInt32{}):
var temp sql.NullInt32
if isNull() {
temp.Valid = false
} else {
temp.Valid = true
temp.Int32 = rand.Int32()
}
return temp
case reflect.TypeOf(sql.NullInt64{}):
var temp sql.NullInt64
if isNull() {
temp.Valid = false
} else {
temp.Valid = true
temp.Int64 = rand.Int64()
}
return temp
case reflect.TypeOf(sql.NullBool{}):
var temp sql.NullBool
if isNull() {
temp.Valid = false
} else {
temp.Valid = true
temp.Bool = (rand.Int()%2 == 0)
}
return temp
case reflect.TypeOf(sql.NullFloat64{}):
var temp sql.NullFloat64
if isNull() {
temp.Valid = false
} else {
temp.Valid = true
temp.Float64 = rand.Float64()
}
return temp
case reflect.TypeOf(sql.NullString{}):
var temp sql.NullString
if isNull() {
temp.Valid = false
} else {
temp.Valid = true
temp.String = fmt.Sprintf("random %d", rand.Int())
}
return temp
} }
if reflect.ValueOf(c.Example).Kind() != reflect.Struct {
return nil, ErrBadExample
}
//we need at least one row
if c.RowCount <= 0 {
c.RowCount = 1
}
//get example type and number of fields
retType := reflect.TypeOf(c.Example)
maxFieldCount := retType.NumField()
//create slice of example structs
ft := reflect.SliceOf(retType)
filled := reflect.MakeSlice(ft, 0, c.RowCount)
for x := 0; x < c.RowCount; x++ {
//add empty example struct
filled = reflect.Append(filled, reflect.ValueOf(c.Example))
for y := 0; y < maxFieldCount; y++ {
field := retType.Field(y)
//get random value
nv := kindToRandom(field)
if nv == nil {
return nil, fmt.Errorf(ErrUnsupportedType.Error(), field.Type.Name())
}
if filled.Index(x).Field(y).CanSet() {
filled.Index(x).Field(y).Set(reflect.ValueOf(nv))
}
}
}
//convert reflect.Value to []any
fl := filled.Len()
retStructs := make([]any, fl)
for x := 0; x < fl; x++ {
retStructs[x] = filled.Index(x).Interface()
}
return &StructMock{
Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(c.Query)),
MockStructs: retStructs,
}, nil
}
// converts basic reflect Kind's to psuedo-random data, it is super basic and supports very basic types. Slices and arrays
// are supported, slices will get 1-10 entries and arrays will fill length
func kindToRandom(field reflect.StructField) any {
kind := field.Type.Kind() kind := field.Type.Kind()
switch kind { switch kind {
case reflect.Int: case reflect.Int:
@@ -188,11 +188,9 @@ func kindToRandom(field reflect.StructField) any {
case reflect.Float64: case reflect.Float64:
return rand.Float64() return rand.Float64()
case reflect.String: case reflect.String:
return fmt.Sprintf("random %d", rand.Int()) return fmt.Sprintf("%d", rand.Int())
case reflect.Bool: case reflect.Bool:
return rand.Int()%2 == 0 return rand.Int()%2 == 0
case reflect.TypeOf(time.Time{}).Kind():
return time.Now()
case reflect.Array: case reflect.Array:
underlying := field.Type.Elem().Kind() underlying := field.Type.Elem().Kind()
count := reflect.ValueOf(field).Len() //fill entire length count := reflect.ValueOf(field).Len() //fill entire length
@@ -277,7 +275,7 @@ func kindToRandom(field reflect.StructField) any {
case reflect.String: case reflect.String:
var strslice []string var strslice []string
for x := 0; x < count; x++ { for x := 0; x < count; x++ {
strslice = append(strslice, fmt.Sprintf("random %d", rand.Int())) strslice = append(strslice, fmt.Sprintf("%d", rand.Int()))
} }
return strslice return strslice
case reflect.Bool: case reflect.Bool:
@@ -291,14 +289,3 @@ func kindToRandom(field reflect.StructField) any {
return nil return nil
} }
// a quick and dirty rng function to determine if nulltypes are null or
// if we will generate a random value
func isNull() bool {
x := rand.IntN(2)
if x%2 == 0 {
return true
} else {
return false
}
}