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") //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 RowConfig struct { Query string Example any Keys []any RowCount int } type StructConfig struct { Query string Example 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(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 } //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 } } retType := reflect.TypeOf(r.Example) maxFieldCount := retType.NumField() columns := make([]string, 0, maxFieldCount) rows := make([][]driver.Value, 0) for y := 0; y < r.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], r.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(r.Query)), 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 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) args := make([]driver.Value, 0) //args for sqlmock WithArgs 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) dbTag := field.Tag.Get(DB_TAG) // no db tag, we skip if dbTag == "" { continue } //get random value 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() { 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)), Args: args, MockStructs: retStructs, Result: &sqlResult{ 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 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 }