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"
|
|
|
|
)
|
|
|
|
|
2025-09-26 16:46:40 -04:00
|
|
|
const (
|
2025-09-26 18:57:53 -04:00
|
|
|
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
|
2025-09-26 16:46:40 -04:00
|
|
|
)
|
|
|
|
|
2025-09-29 15:47:51 -04:00
|
|
|
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")
|
2025-09-30 11:47:38 -04:00
|
|
|
ErrTypeCast = errors.New("unable to type cast the mock structs")
|
2025-09-30 11:12:47 -04:00
|
|
|
//contains placeholder to inject type on runtime
|
|
|
|
ErrUnsupportedType = errors.New("type: %s is not yet supported")
|
2025-09-29 15:47:51 -04:00
|
|
|
)
|
|
|
|
|
2025-09-26 18:57:53 -04:00
|
|
|
// mock data generated based on config
|
2025-09-29 15:47:51 -04:00
|
|
|
type RowMock struct {
|
2025-09-26 15:32:27 -04:00
|
|
|
Query string
|
|
|
|
Columns []string
|
2025-09-26 16:16:53 -04:00
|
|
|
Rows [][]driver.Value
|
2025-09-26 15:32:27 -04:00
|
|
|
}
|
|
|
|
|
2025-09-26 18:57:53 -04:00
|
|
|
// configuration for RandomGenerate
|
2025-09-30 11:20:30 -04:00
|
|
|
type Config struct {
|
2025-09-26 16:46:40 -04:00
|
|
|
Query string
|
|
|
|
Example any
|
2025-09-30 11:12:47 -04:00
|
|
|
Keys []any
|
2025-09-26 16:46:40 -04:00
|
|
|
RowCount int
|
|
|
|
}
|
|
|
|
|
2025-09-29 15:47:51 -04:00
|
|
|
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
|
2025-09-29 15:47:51 -04:00
|
|
|
}
|
|
|
|
|
2025-09-26 18:57:53 -04:00
|
|
|
// generates mock data based on configuration to be used for sqlmock
|
2025-09-30 11:20:30 -04:00
|
|
|
func RandomRows(m Config) (*RowMock, error) {
|
2025-09-26 18:57:53 -04:00
|
|
|
//example struct cannot be nil and must be a struct
|
2025-09-26 16:46:40 -04:00
|
|
|
if m.Example == nil {
|
2025-09-29 15:47:51 -04:00
|
|
|
return nil, ErrBadExample
|
2025-09-26 16:46:40 -04:00
|
|
|
}
|
|
|
|
if reflect.ValueOf(m.Example).Kind() != reflect.Struct {
|
2025-09-29 15:47:51 -04:00
|
|
|
return nil, ErrBadExample
|
2025-09-26 15:32:27 -04:00
|
|
|
}
|
2025-09-26 18:57:53 -04:00
|
|
|
|
|
|
|
//any weirdness, just pull one row
|
|
|
|
if m.RowCount <= 0 {
|
2025-09-26 16:46:40 -04:00
|
|
|
m.RowCount = 1
|
2025-09-26 16:16:53 -04:00
|
|
|
}
|
2025-09-26 18:57:53 -04:00
|
|
|
|
|
|
|
//if keys are set, ensure there are enough to populate requested rows
|
|
|
|
primaryKey := false
|
|
|
|
if len(m.Keys) > 0 {
|
|
|
|
primaryKey = true
|
2025-09-26 16:46:40 -04:00
|
|
|
if len(m.Keys) != m.RowCount {
|
2025-09-29 15:47:51 -04:00
|
|
|
return nil, ErrMissingKeys
|
2025-09-26 16:29:36 -04:00
|
|
|
}
|
|
|
|
}
|
2025-09-26 15:32:27 -04:00
|
|
|
|
2025-09-26 16:46:40 -04:00
|
|
|
retType := reflect.TypeOf(m.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)
|
2025-09-26 16:16:53 -04:00
|
|
|
|
2025-09-26 16:46:40 -04:00
|
|
|
for y := 0; y < m.RowCount; y++ {
|
2025-09-26 16:25:46 -04:00
|
|
|
rows = append(rows, make([]driver.Value, 0))
|
2025-09-26 16:16:53 -04:00
|
|
|
for x := 0; x < maxFieldCount; x++ {
|
|
|
|
field := retType.Field(x)
|
2025-09-26 16:46:40 -04:00
|
|
|
dbTag := field.Tag.Get(DB_TAG)
|
2025-09-26 16:16:53 -04:00
|
|
|
if dbTag == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2025-09-26 18:57:53 -04:00
|
|
|
//track columns only once
|
2025-09-26 16:16:53 -04:00
|
|
|
if y == 0 {
|
|
|
|
columns = append(columns, dbTag)
|
|
|
|
}
|
|
|
|
|
2025-09-26 18:57:53 -04:00
|
|
|
//if field has lazy:"key" tag and tags are present inject primary key
|
|
|
|
if field.Tag.Get(LAZY_TAG) == KEY_VALUE && primaryKey {
|
2025-09-26 16:46:40 -04:00
|
|
|
rows[y] = append(rows[y], m.Keys[y])
|
2025-09-26 16:16:53 -04:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2025-09-26 18:57:53 -04:00
|
|
|
//generate random values
|
2025-09-26 16:16:53 -04:00
|
|
|
nv := kindToRandom(field)
|
|
|
|
if nv == nil {
|
2025-09-30 11:12:47 -04:00
|
|
|
return nil, fmt.Errorf(ErrUnsupportedType.Error(), field.Type.Name())
|
2025-09-26 16:16:53 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
rows[y] = append(rows[y], nv)
|
2025-09-26 15:32:27 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-09-29 15:47:51 -04:00
|
|
|
return &RowMock{
|
2025-09-26 18:57:53 -04:00
|
|
|
//sql is rebound and escaped for sqlmock.ExpectQuery
|
2025-09-26 16:46:40 -04:00
|
|
|
Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(m.Query)),
|
2025-09-26 15:32:27 -04:00
|
|
|
Columns: columns,
|
|
|
|
Rows: rows,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2025-09-30 11:20:30 -04:00
|
|
|
func RandomStruct(c Config) (*StructMock, error) {
|
2025-09-29 15:47:51 -04:00
|
|
|
if c.Example == nil {
|
|
|
|
return nil, ErrBadExample
|
|
|
|
}
|
|
|
|
if reflect.ValueOf(c.Example).Kind() != reflect.Struct {
|
|
|
|
return nil, ErrBadExample
|
|
|
|
}
|
|
|
|
|
2025-09-30 11:12:47 -04:00
|
|
|
if c.RowCount <= 0 {
|
|
|
|
c.RowCount = 1
|
|
|
|
}
|
|
|
|
|
2025-09-29 15:47:51 -04:00
|
|
|
retType := reflect.TypeOf(c.Example)
|
|
|
|
maxFieldCount := retType.NumField()
|
2025-09-30 11:12:47 -04:00
|
|
|
//create slice of structs
|
|
|
|
ft := reflect.SliceOf(retType)
|
2025-09-30 11:41:28 -04:00
|
|
|
filled := reflect.MakeSlice(ft, 0, c.RowCount)
|
|
|
|
// filled := reflect.MakeSlice(ft, 0, c.RowCount).Elem()
|
2025-09-30 11:26:54 -04:00
|
|
|
args := make([]driver.Value, 0, maxFieldCount)
|
2025-09-29 15:47:51 -04:00
|
|
|
|
2025-09-30 11:12:47 -04:00
|
|
|
for x := 0; x < c.RowCount; x++ {
|
|
|
|
for y := 0; y < maxFieldCount; y++ {
|
|
|
|
field := retType.Field(y)
|
|
|
|
dbTag := field.Tag.Get(DB_TAG)
|
|
|
|
if dbTag == "" {
|
|
|
|
continue
|
|
|
|
}
|
2025-09-29 15:47:51 -04:00
|
|
|
|
2025-09-30 11:12:47 -04:00
|
|
|
nv := kindToRandom(field)
|
|
|
|
if nv == nil {
|
|
|
|
return nil, fmt.Errorf(ErrUnsupportedType.Error(), field.Type.Name())
|
|
|
|
}
|
2025-09-29 15:47:51 -04:00
|
|
|
|
2025-09-30 11:12:47 -04:00
|
|
|
args[y] = nv
|
|
|
|
if x == 0 {
|
|
|
|
nf := filled.Index(x).Field(y)
|
|
|
|
if nf.CanSet() {
|
|
|
|
filled.Index(x).Field(y).Set(reflect.ValueOf(nv))
|
|
|
|
}
|
|
|
|
}
|
2025-09-29 15:47:51 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-09-30 11:47:38 -04:00
|
|
|
ms, ok := reflect.ValueOf(filled).Interface().([]any)
|
|
|
|
if !ok {
|
|
|
|
return nil, ErrTypeCast
|
|
|
|
}
|
|
|
|
|
2025-09-29 15:47:51 -04:00
|
|
|
return &StructMock{
|
2025-09-30 11:12:47 -04:00
|
|
|
Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(c.Query)),
|
|
|
|
Args: args,
|
2025-09-30 11:47:38 -04:00
|
|
|
MockStructs: ms,
|
2025-09-29 15:47:51 -04:00
|
|
|
Result: &sqlResult{
|
2025-09-30 11:12:47 -04:00
|
|
|
rowsAffected: int64(c.RowCount),
|
2025-09-29 15:47:51 -04:00
|
|
|
err: nil,
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2025-09-26 18:57:53 -04:00
|
|
|
// converts basic reflect Kind's to psuedo-random data, slices are given a random amount of entries
|
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
|
2025-09-26 18:57:53 -04:00
|
|
|
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()
|
2025-09-26 18:57:53 -04:00
|
|
|
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:
|
2025-09-26 18:57:53 -04:00
|
|
|
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:
|
2025-09-26 18:57:53 -04:00
|
|
|
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:
|
2025-09-26 18:57:53 -04:00
|
|
|
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:
|
2025-09-26 18:57:53 -04:00
|
|
|
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:
|
2025-09-26 18:57:53 -04:00
|
|
|
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:
|
2025-09-26 18:57:53 -04:00
|
|
|
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:
|
2025-09-26 18:57:53 -04:00
|
|
|
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
|
|
|
|
}
|