examples/ogx/dbfixture/fixture.go

446 lines
8.9 KiB
Go

package dbfixture
import (
"bytes"
"context"
"database/sql"
"fmt"
"io/fs"
"reflect"
"regexp"
"strconv"
"strings"
"text/template"
"text/template/parse"
"time"
"gopkg.in/yaml.v3"
"gitee.com/chentanyang/ogx"
"gitee.com/chentanyang/ogx/schema"
)
var (
funcNameRE = regexp.MustCompile(`^\{\{ (\w+) \}\}$`)
tplRE = regexp.MustCompile(`\{\{ .+ \}\}`)
)
type FixtureOption func(l *Fixture)
func WithRecreateTables() FixtureOption {
return func(l *Fixture) {
if l.truncateTables {
panic("don't use WithDropTables together with WithTruncateTables")
}
l.recreateTables = true
l.seenTables = make(map[string]struct{})
}
}
func WithTruncateTables() FixtureOption {
return func(l *Fixture) {
if l.truncateTables {
panic("don't use WithTruncateTables together with WithRecreateTables")
}
l.truncateTables = true
l.seenTables = make(map[string]struct{})
}
}
func WithTemplateFuncs(funcMap template.FuncMap) FixtureOption {
return func(l *Fixture) {
for k, v := range funcMap {
l.funcMap[k] = v
}
}
}
type BeforeInsertData struct {
Query *ogx.InsertQuery
Model interface{}
}
type BeforeInsertFunc func(ctx context.Context, data *BeforeInsertData) error
func WithBeforeInsert(fn BeforeInsertFunc) FixtureOption {
return func(f *Fixture) {
f.beforeInsert = append(f.beforeInsert, fn)
}
}
type Fixture struct {
db *ogx.DB
recreateTables bool
truncateTables bool
beforeInsert []BeforeInsertFunc
seenTables map[string]struct{}
funcMap template.FuncMap
modelRows map[string]map[string]interface{}
}
func New(db *ogx.DB, opts ...FixtureOption) *Fixture {
f := &Fixture{
db: db,
funcMap: defaultFuncs(),
modelRows: make(map[string]map[string]interface{}),
}
for _, opt := range opts {
opt(f)
}
return f
}
func (f *Fixture) Row(id string) (interface{}, error) {
ss := strings.Split(id, ".")
if len(ss) != 2 {
return nil, fmt.Errorf("fixture: invalid row id: %q", id)
}
model, rowID := ss[0], ss[1]
rows, ok := f.modelRows[model]
if !ok {
return nil, fmt.Errorf("fixture: unknown model=%q", model)
}
row, ok := rows[rowID]
if !ok {
return nil, fmt.Errorf("fixture: can't find row=%q for model=%q", rowID, model)
}
return row, nil
}
func (f *Fixture) MustRow(id string) interface{} {
row, err := f.Row(id)
if err != nil {
panic(err)
}
return row
}
func (f *Fixture) Load(ctx context.Context, fsys fs.FS, names ...string) error {
for _, name := range names {
if err := f.load(ctx, fsys, name); err != nil {
return err
}
}
return nil
}
func (f *Fixture) load(ctx context.Context, fsys fs.FS, name string) error {
fh, err := fsys.Open(name)
if err != nil {
return err
}
var fixtures []fixtureData
dec := yaml.NewDecoder(fh)
if err := dec.Decode(&fixtures); err != nil {
return err
}
for i := range fixtures {
if err := f.addFixture(ctx, &fixtures[i]); err != nil {
return err
}
}
return nil
}
func (f *Fixture) addFixture(ctx context.Context, data *fixtureData) error {
table := f.db.Dialect().Tables().ByModel(data.Model)
if table == nil {
return fmt.Errorf("fixture: can't find model=%q (use db.RegisterModel)", data.Model)
}
if f.recreateTables {
if err := f.dropTable(ctx, table); err != nil {
return err
}
} else if f.truncateTables {
if err := f.truncateTable(ctx, table); err != nil {
return err
}
}
for _, row := range data.Rows {
if err := f.addRow(ctx, table, row); err != nil {
return err
}
}
return nil
}
func (f *Fixture) addRow(ctx context.Context, table *schema.Table, row row) error {
var rowID string
strct := reflect.New(table.Type).Elem()
for key, value := range row {
if key == "_id" {
if err := value.Decode(&rowID); err != nil {
return err
}
continue
}
field, err := table.Field(key)
if err != nil {
return err
}
if err := f.decodeField(strct, field, &value); err != nil {
return fmt.Errorf("dbfixture: decoding %s failed: %w", key, err)
}
}
model := strct.Addr().Interface()
q := f.db.NewInsert().Model(model)
data := &BeforeInsertData{
Query: q,
Model: model,
}
for _, fn := range f.beforeInsert {
if err := fn(ctx, data); err != nil {
return err
}
}
if _, err := q.Exec(ctx); err != nil {
return err
}
if rowID == "" && len(table.PKs) == 1 {
pk := table.PKs[0]
fv := pk.Value(strct)
rowID = "pk" + asString(fv)
}
if rowID != "" {
rows, ok := f.modelRows[table.TypeName]
if !ok {
rows = make(map[string]interface{})
f.modelRows[table.TypeName] = rows
}
rows[rowID] = model
}
return nil
}
func (f *Fixture) decodeField(strct reflect.Value, field *schema.Field, value *yaml.Node) error {
fv := field.Value(strct)
iface := fv.Addr().Interface()
if value.Tag != "!!str" {
return value.Decode(iface)
}
if ss := funcNameRE.FindStringSubmatch(value.Value); len(ss) > 0 {
if fn, ok := f.funcMap[ss[1]].(func() interface{}); ok {
return scanFieldValue(strct, field, fn())
}
}
if tplRE.MatchString(value.Value) {
src, err := f.eval(value.Value)
if err != nil {
return err
}
return scanFieldValue(strct, field, src)
}
if v, ok := iface.(yaml.Unmarshaler); ok {
return v.UnmarshalYAML(value)
}
if _, ok := iface.(sql.Scanner); ok {
var str string
if err := value.Decode(&str); err != nil {
return err
}
return field.ScanValue(strct, str)
}
return value.Decode(iface)
}
func (f *Fixture) dropTable(ctx context.Context, table *schema.Table) error {
if _, ok := f.seenTables[table.Name]; ok {
return nil
}
f.seenTables[table.Name] = struct{}{}
if _, err := f.db.NewDropTable().
Model(table.ZeroIface).
IfExists().
Cascade().
Exec(ctx); err != nil {
return err
}
if _, err := f.db.NewCreateTable().
Model(table.ZeroIface).
Exec(ctx); err != nil {
return err
}
return nil
}
func (f *Fixture) truncateTable(ctx context.Context, table *schema.Table) error {
if _, ok := f.seenTables[table.Name]; ok {
return nil
}
f.seenTables[table.Name] = struct{}{}
if _, err := f.db.NewTruncateTable().
Model(table.ZeroIface).
Cascade().
Exec(ctx); err != nil {
return err
}
return nil
}
func (f *Fixture) eval(templ string) (interface{}, error) {
if v, ok := f.evalFuncCall(templ); ok {
return v, nil
}
tpl, err := template.New("").Funcs(f.funcMap).Parse(templ)
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err := tpl.Execute(&buf, f.modelRows); err != nil {
return nil, err
}
return buf.String(), nil
}
func (f *Fixture) evalFuncCall(templ string) (interface{}, bool) {
tree, err := parse.Parse("", templ, "{{", "}}", f.funcMap)
if err != nil {
return nil, false
}
root := tree[""].Root
if len(root.Nodes) != 1 {
return nil, false
}
action, ok := root.Nodes[0].(*parse.ActionNode)
if !ok {
return nil, false
}
if len(action.Pipe.Cmds) != 1 {
return nil, false
}
args := action.Pipe.Cmds[0].Args
if len(args) == 0 {
return nil, false
}
funcName, ok := args[0].(*parse.IdentifierNode)
if !ok {
return nil, false
}
fn, ok := f.funcMap[funcName.Ident]
if !ok {
return nil, false
}
fnValue := reflect.ValueOf(fn)
fnType := fnValue.Type()
if fnType.NumOut() != 1 {
return nil, false
}
args = args[1:]
if len(args) != fnType.NumIn() {
return nil, false
}
argValues := make([]reflect.Value, len(args))
for i, node := range args {
switch node := node.(type) {
case *parse.StringNode:
argValues[i] = reflect.ValueOf(node.Text)
case *parse.NumberNode:
switch {
case node.IsInt:
argValues[i] = reflect.ValueOf(node.Int64)
case node.IsUint:
argValues[i] = reflect.ValueOf(node.Uint64)
case node.IsFloat:
argValues[i] = reflect.ValueOf(node.Float64)
case node.IsComplex:
argValues[i] = reflect.ValueOf(node.Complex128)
default:
argValues[i] = reflect.ValueOf(node.Text)
}
case *parse.BoolNode:
argValues[i] = reflect.ValueOf(node.True)
default:
return nil, false
}
}
out := fnValue.Call(argValues)
return out[0].Interface(), true
}
type fixtureData struct {
Model string `yaml:"model"`
Rows []row `yaml:"rows"`
}
type row map[string]yaml.Node
func asString(rv reflect.Value) string {
switch rv.Kind() {
case reflect.Bool:
return strconv.FormatBool(rv.Bool())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(rv.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return strconv.FormatUint(rv.Uint(), 10)
case reflect.Float32:
return strconv.FormatFloat(rv.Float(), 'g', -1, 32)
case reflect.Float64:
return strconv.FormatFloat(rv.Float(), 'g', -1, 64)
}
return fmt.Sprintf("%v", rv.Interface())
}
func defaultFuncs() template.FuncMap {
return template.FuncMap{
"now": func() interface{} {
return time.Now()
},
}
}
func scanFieldValue(strct reflect.Value, field *schema.Field, value interface{}) error {
if v := reflect.ValueOf(value); v.CanConvert(field.StructField.Type) {
field.Value(strct).Set(v.Convert(field.StructField.Type))
return nil
}
return field.ScanValue(strct, value)
}