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 ( 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") ErrTypeCast = errors.New("unable to type cast the mock structs") //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 { Query string Columns []string Rows [][]driver.Value } // configuration for RandomGenerate type Config struct { Query string Example any Keys []any RowCount int } type StructMock struct { Query string Args []driver.Value MockStructs []any Result driver.Result } // generates mock data based on configuration to be used for sqlmock func RandomRows(m Config) (*RowMock, error) { //example struct cannot be nil and must be a struct if m.Example == nil { return nil, ErrBadExample } if reflect.ValueOf(m.Example).Kind() != reflect.Struct { return nil, ErrBadExample } //any weirdness, just pull one row if m.RowCount <= 0 { m.RowCount = 1 } //if keys are set, ensure there are enough to populate requested rows primaryKey := false if len(m.Keys) > 0 { primaryKey = true if len(m.Keys) != m.RowCount { return nil, ErrMissingKeys } } retType := reflect.TypeOf(m.Example) maxFieldCount := retType.NumField() columns := make([]string, 0, maxFieldCount) rows := make([][]driver.Value, 0) for y := 0; y < m.RowCount; y++ { 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], m.Keys[y]) continue } //generate random values nv := kindToRandom(field) if nv == nil { return nil, fmt.Errorf(ErrUnsupportedType.Error(), field.Type.Name()) } rows[y] = append(rows[y], nv) } } return &RowMock{ //sql is rebound and escaped for sqlmock.ExpectQuery Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(m.Query)), Columns: columns, Rows: rows, }, nil } func RandomStruct(c Config) (*StructMock, error) { if c.Example == nil { return nil, ErrBadExample } if reflect.ValueOf(c.Example).Kind() != reflect.Struct { return nil, ErrBadExample } if c.RowCount <= 0 { c.RowCount = 1 } retType := reflect.TypeOf(c.Example) maxFieldCount := retType.NumField() //create slice of structs ft := reflect.SliceOf(retType) filled := reflect.MakeSlice(ft, 0, c.RowCount) // filled := reflect.MakeSlice(ft, 0, c.RowCount).Elem() args := make([]driver.Value, 0, maxFieldCount) 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 } nv := kindToRandom(field) if nv == nil { return nil, fmt.Errorf(ErrUnsupportedType.Error(), field.Type.Name()) } args[y] = nv if x == 0 { nf := filled.Index(x).Field(y) if nf.CanSet() { filled.Index(x).Field(y).Set(reflect.ValueOf(nv)) } } } } ms, ok := reflect.ValueOf(filled).Interface().([]any) if !ok { return nil, ErrTypeCast } return &StructMock{ Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(c.Query)), Args: args, MockStructs: ms, Result: &sqlResult{ rowsAffected: int64(c.RowCount), err: nil, }, }, nil } // converts basic reflect Kind's to psuedo-random data, slices are given a random amount of entries 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 } case reflect.Slice: underlying := field.Type.Elem().Kind() count := (rand.Int() % 10) + 1 //amount of entries to append to slice 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 } } return nil }