Files
lazy/lazy.go

307 lines
7.4 KiB
Go
Raw Normal View History

2025-09-26 15:32:27 -04:00
package lazy
import (
"database/sql/driver"
"errors"
"fmt"
"math/rand/v2"
"reflect"
"regexp"
"github.com/jmoiron/sqlx"
)
const (
DB_TAG = "db" //tag used to parse sql fields in example struct
LAZY_TAG = "lazy" //tag label for KEY_VALUE
KEY_VALUE = "key" //tag value for LAZY_TAG
)
var (
2025-09-30 11:12:47 -04:00
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
type RowMock struct {
2025-09-26 15:32:27 -04:00
Query string
Columns []string
Rows [][]driver.Value
2025-09-26 15:32:27 -04:00
}
// configuration for RandomGenerate
type RowConfig struct {
Query string
Example any
2025-09-30 11:12:47 -04:00
Keys []any
RowCount int
}
type StructConfig struct {
Query string
Example any
RowCount int
}
type StructMock struct {
2025-09-30 11:12:47 -04:00
Query string
2025-09-30 11:26:54 -04:00
Args []driver.Value
2025-09-30 11:47:38 -04:00
MockStructs []any
2025-09-30 11:12:47 -04:00
Result driver.Result
}
// generates mock data based on configuration to be used for sqlmock
func RandomRows(r RowConfig) (*RowMock, error) {
//example struct cannot be nil and must be a struct
if r.Example == nil {
return nil, ErrBadExample
}
if reflect.ValueOf(r.Example).Kind() != reflect.Struct {
return nil, ErrBadExample
2025-09-26 15:32:27 -04:00
}
//any weirdness, just pull one row
if r.RowCount <= 0 {
r.RowCount = 1
}
//if keys are set, ensure there are enough to populate requested rows
primaryKey := false
if len(r.Keys) > 0 {
primaryKey = true
if len(r.Keys) != r.RowCount {
return nil, ErrMissingKeys
2025-09-26 16:29:36 -04:00
}
}
2025-09-26 15:32:27 -04:00
retType := reflect.TypeOf(r.Example)
2025-09-26 15:32:27 -04:00
maxFieldCount := retType.NumField()
columns := make([]string, 0, maxFieldCount)
2025-09-26 16:18:27 -04:00
rows := make([][]driver.Value, 0)
for y := 0; y < r.RowCount; y++ {
2025-09-26 16:25:46 -04:00
rows = append(rows, make([]driver.Value, 0))
for x := 0; x < maxFieldCount; x++ {
field := retType.Field(x)
dbTag := field.Tag.Get(DB_TAG)
if dbTag == "" {
continue
}
//track columns only once
if y == 0 {
columns = append(columns, dbTag)
}
//if field has lazy:"key" tag and tags are present inject primary key
if field.Tag.Get(LAZY_TAG) == KEY_VALUE && primaryKey {
rows[y] = append(rows[y], r.Keys[y])
continue
}
//generate random values
nv := kindToRandom(field)
if nv == nil {
2025-09-30 11:12:47 -04:00
return nil, fmt.Errorf(ErrUnsupportedType.Error(), field.Type.Name())
}
rows[y] = append(rows[y], nv)
2025-09-26 15:32:27 -04:00
}
}
return &RowMock{
//sql is rebound and escaped for sqlmock.ExpectQuery
Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(r.Query)),
2025-09-26 15:32:27 -04:00
Columns: columns,
Rows: rows,
}, nil
}
// generates mock data for insert statements to be used for sqlmock, it also returns a slice of
// example structs with mock data
func RandomStruct(c StructConfig) (*StructMock, error) {
//make sure we have an example struct
if c.Example == nil {
return nil, ErrBadExample
}
if reflect.ValueOf(c.Example).Kind() != reflect.Struct {
return nil, ErrBadExample
}
//we need at least one row
2025-09-30 11:12:47 -04:00
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
2025-09-30 11:12:47 -04:00
ft := reflect.SliceOf(retType)
2025-09-30 11:41:28 -04:00
filled := reflect.MakeSlice(ft, 0, c.RowCount)
args := make([]driver.Value, 0) //args for sqlmock WithArgs
2025-09-30 11:12:47 -04:00
for x := 0; x < c.RowCount; x++ {
//add empty example struct
filled = reflect.Append(filled, reflect.ValueOf(c.Example))
2025-09-30 11:12:47 -04:00
for y := 0; y < maxFieldCount; y++ {
field := retType.Field(y)
dbTag := field.Tag.Get(DB_TAG)
// no db tag, we skip
2025-09-30 11:12:47 -04:00
if dbTag == "" {
continue
}
//get random value
2025-09-30 11:12:47 -04:00
nv := kindToRandom(field)
if nv == nil {
return nil, fmt.Errorf(ErrUnsupportedType.Error(), field.Type.Name())
}
//add to sqlmock args
args = append(args, nv)
if filled.Index(x).Field(y).CanSet() {
2025-09-30 16:25:54 -04:00
filled.Index(x).Field(y).Set(reflect.ValueOf(nv))
2025-09-30 11:12:47 -04:00
}
}
}
//convert reflect.Value to []any
retStructs := make([]any, 0)
for x := 0; x < filled.Len(); x++ {
retStructs = append(retStructs, filled.Index(x))
2025-09-30 11:47:38 -04:00
}
return &StructMock{
2025-09-30 11:12:47 -04:00
Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(c.Query)),
Args: args,
MockStructs: retStructs,
Result: &sqlResult{
2025-09-30 11:12:47 -04:00
rowsAffected: int64(c.RowCount),
err: nil,
},
}, 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
2025-09-26 15:32:27 -04:00
func kindToRandom(field reflect.StructField) any {
kind := field.Type.Kind()
switch kind {
case reflect.Int:
return rand.Int()
case reflect.Int32:
return rand.Int32()
case reflect.Int64:
return rand.Int64()
case reflect.Float32:
return rand.Float32()
case reflect.Float64:
return rand.Float64()
case reflect.String:
return fmt.Sprintf("%d", rand.Int())
case reflect.Bool:
return rand.Int()%2 == 0
case reflect.Array:
underlying := field.Type.Elem().Kind()
count := reflect.ValueOf(field).Len() //fill entire length
switch underlying {
case reflect.Int:
var intslice []int
for x := 0; x < count; x++ {
intslice = append(intslice, rand.Int())
}
return intslice
case reflect.Int32:
var int32slice []int32
for x := 0; x < count; x++ {
int32slice = append(int32slice, rand.Int32())
}
return int32slice
case reflect.Int64:
var int64slice []int64
for x := 0; x < count; x++ {
int64slice = append(int64slice, rand.Int64())
}
return int64slice
case reflect.Float32:
var float32slice []float32
for x := 0; x < count; x++ {
float32slice = append(float32slice, rand.Float32())
}
return float32slice
case reflect.Float64:
var float64slice []float64
for x := 0; x < count; x++ {
float64slice = append(float64slice, rand.Float64())
}
return float64slice
case reflect.String:
var strslice []string
for x := 0; x < count; x++ {
strslice = append(strslice, fmt.Sprintf("%d", rand.Int()))
}
return strslice
case reflect.Bool:
var boolslice []bool
for x := 0; x < count; x++ {
boolslice = append(boolslice, (rand.Int()%2 == 0))
}
return boolslice
}
2025-09-26 15:32:27 -04:00
case reflect.Slice:
underlying := field.Type.Elem().Kind()
count := (rand.Int() % 10) + 1 //amount of entries to append to slice
2025-09-26 15:32:27 -04:00
switch underlying {
case reflect.Int:
var intslice []int
for x := 0; x < count; x++ {
intslice = append(intslice, rand.Int())
}
return intslice
2025-09-26 15:32:27 -04:00
case reflect.Int32:
var int32slice []int32
for x := 0; x < count; x++ {
int32slice = append(int32slice, rand.Int32())
}
return int32slice
2025-09-26 15:32:27 -04:00
case reflect.Int64:
var int64slice []int64
for x := 0; x < count; x++ {
int64slice = append(int64slice, rand.Int64())
}
return int64slice
2025-09-26 15:32:27 -04:00
case reflect.Float32:
var float32slice []float32
for x := 0; x < count; x++ {
float32slice = append(float32slice, rand.Float32())
}
return float32slice
2025-09-26 15:32:27 -04:00
case reflect.Float64:
var float64slice []float64
for x := 0; x < count; x++ {
float64slice = append(float64slice, rand.Float64())
}
return float64slice
2025-09-26 15:32:27 -04:00
case reflect.String:
var strslice []string
for x := 0; x < count; x++ {
strslice = append(strslice, fmt.Sprintf("%d", rand.Int()))
}
return strslice
2025-09-26 15:32:27 -04:00
case reflect.Bool:
var boolslice []bool
for x := 0; x < count; x++ {
boolslice = append(boolslice, (rand.Int()%2 == 0))
}
return boolslice
2025-09-26 15:32:27 -04:00
}
}
return nil
}