//nolint:lll // This file has lots of long lines lol package modelgenerate import ( "fmt" "go/ast" "go/token" "strings" "github.com/jinzhu/inflection" "git.offline-twitter.com/offline-labs/gas-stack/pkg/schema" "git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils" ) func GenerateIDType(table schema.Table) *ast.GenDecl { // e.g., `type FoodID uint64` return &ast.GenDecl{ Tok: token.TYPE, Specs: []ast.Spec{&ast.TypeSpec{Name: ast.NewIdent(table.TypeIDName), Type: ast.NewIdent("uint64")}}, } } // GenerateModelAST produces an AST for a struct type corresponding to the model. // TODO: generate the right field types here based on column types. func GenerateModelAST(table schema.Table) *ast.GenDecl { // Fields for the struct fields := []*ast.Field{} // Other fields (just strings for now) for _, col := range table.Columns { switch col.Name { case "rowid": fields = append(fields, &ast.Field{ Names: []*ast.Ident{ast.NewIdent("ID")}, Type: ast.NewIdent(table.TypeIDName), Tag: &ast.BasicLit{Kind: token.STRING, Value: "`db:\"rowid\" json:\"id\"`"}, }) default: if col.IsForeignKey && strings.HasSuffix(col.Name, "_id") { fields = append(fields, &ast.Field{ Names: []*ast.Ident{ast.NewIdent(textutils.SnakeToCamel(strings.TrimSuffix(col.Name, "_id")) + "ID")}, Type: ast.NewIdent(textutils.SnakeToCamel(inflection.Singular(col.ForeignKeyTargetTable) + "ID")), Tag: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`db:\"%s\" json:\"%s\"`", col.Name, col.Name)}, }) } else { typeName := "string" switch col.Type { case "integer", "int": if strings.HasPrefix(col.Name, "is_") || strings.HasPrefix(col.Name, "has_") { typeName = "bool" } else if strings.HasSuffix(col.Name, "_at") { typeName = "Timestamp" } else { typeName = "int64" } case "text": typeName = "string" case "real": typeName = "float32" case "blob": typeName = "[]byte" default: panic("Unrecognized sqlite column type: " + col.Type) } fields = append(fields, &ast.Field{ Names: []*ast.Ident{ast.NewIdent(textutils.SnakeToCamel(col.Name))}, Type: ast.NewIdent(typeName), Tag: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`db:\"%s\" json:\"%s\"`", col.Name, col.Name)}, }) } } } return &ast.GenDecl{ Tok: token.TYPE, Specs: []ast.Spec{&ast.TypeSpec{ Name: ast.NewIdent(table.TypeName), Type: &ast.StructType{Fields: &ast.FieldList{List: fields}}, }}, } } // GenerateSaveItemFunc produces an AST for the SaveXyz() function of the model. // E.g., a table with `table.TypeName = "foods"` will produce a "SaveFood()" function. func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl { insertCols := make([]string, 0, len(tbl.Columns)) insertVals := make([]string, 0, len(tbl.Columns)) updatePairs := make([]string, 0, len(tbl.Columns)) for _, col := range tbl.Columns { if col.Name == "rowid" { continue } insertCols = append(insertCols, col.Name) val := ":" + col.Name if col.IsNullableForeignKey() { val = fmt.Sprintf("nullif(%s, 0)", val) } insertVals = append(insertVals, val) updatePairs = append(updatePairs, col.Name+"="+val) } insertStmt := fmt.Sprintf("\n\t\t insert into %s (%s)\n\t\t values (%s)\n\t\t", tbl.TableName, strings.Join(insertCols, ", "), strings.Join(insertVals, ", ")) updateStmt := fmt.Sprintf("\n\t\t update %s\n\t\t set %s\n\t\t where rowid = :rowid\n\t\t", tbl.TableName, strings.Join(updatePairs, ",\n\t\t ")) funcBody := &ast.BlockStmt{ List: []ast.Stmt{ &ast.IfStmt{ Cond: &ast.BinaryExpr{ X: &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")}, Op: token.EQL, Y: &ast.BasicLit{Kind: token.INT, Value: "0"}, }, Body: &ast.BlockStmt{ List: []ast.Stmt{ &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("result"), ast.NewIdent("err")}, Tok: token.DEFINE, Rhs: []ast.Expr{&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("db.DB"), Sel: ast.NewIdent("NamedExec")}, Args: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: "`" + insertStmt + "`"}, ast.NewIdent(tbl.VarName)}}}, }, &ast.IfStmt{Cond: &ast.BinaryExpr{X: ast.NewIdent("err"), Op: token.NEQ, Y: ast.NewIdent("nil")}, Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ExprStmt{X: &ast.CallExpr{Fun: ast.NewIdent("panic"), Args: []ast.Expr{ast.NewIdent("err")}}}}}}, &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("id"), ast.NewIdent("err")}, Tok: token.DEFINE, Rhs: []ast.Expr{&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("LastInsertId")}, Args: []ast.Expr{}}}, }, &ast.IfStmt{Cond: &ast.BinaryExpr{X: ast.NewIdent("err"), Op: token.NEQ, Y: ast.NewIdent("nil")}, Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ExprStmt{X: &ast.CallExpr{Fun: ast.NewIdent("panic"), Args: []ast.Expr{ast.NewIdent("err")}}}}}}, &ast.AssignStmt{Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")}}, Tok: token.ASSIGN, Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent(tbl.TypeIDName), Args: []ast.Expr{ast.NewIdent("id")}}}}, }, }, Else: &ast.BlockStmt{ List: []ast.Stmt{ &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("result"), ast.NewIdent("err")}, Tok: token.DEFINE, Rhs: []ast.Expr{&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("db.DB"), Sel: ast.NewIdent("NamedExec")}, Args: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: "`" + updateStmt + "`"}, ast.NewIdent(tbl.VarName)}}}, }, &ast.IfStmt{Cond: &ast.BinaryExpr{X: ast.NewIdent("err"), Op: token.NEQ, Y: ast.NewIdent("nil")}, Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ExprStmt{X: &ast.CallExpr{Fun: ast.NewIdent("panic"), Args: []ast.Expr{ast.NewIdent("err")}}}}}}, &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("count"), ast.NewIdent("err")}, Tok: token.DEFINE, Rhs: []ast.Expr{&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("RowsAffected")}, Args: []ast.Expr{}}}, }, &ast.IfStmt{Cond: &ast.BinaryExpr{X: ast.NewIdent("err"), Op: token.NEQ, Y: ast.NewIdent("nil")}, Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ExprStmt{X: &ast.CallExpr{Fun: ast.NewIdent("panic"), Args: []ast.Expr{ast.NewIdent("err")}}}}}}, &ast.IfStmt{ Cond: &ast.BinaryExpr{X: ast.NewIdent("count"), Op: token.NEQ, Y: &ast.BasicLit{Kind: token.INT, Value: "1"}}, Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ExprStmt{X: &ast.CallExpr{Fun: ast.NewIdent("panic"), Args: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("fmt.Errorf"), Args: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"got %s with ID (%%d), so attempted update, but it doesn't exist\"", strings.ToLower(tbl.TypeName))}, &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")}}}}}}}}, }, }, }, }, }, } funcDecl := &ast.FuncDecl{ Recv: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")}}}, Name: ast.NewIdent("Save" + tbl.TypeName), Type: &ast.FuncType{ Params: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent(tbl.VarName)}, Type: &ast.StarExpr{X: ast.NewIdent(tbl.TypeName)}}}}, Results: nil, }, Body: funcBody, } return funcDecl } // GenerateGetItemByIDFunc produces an AST for the `GetXyzByID()` function. // E.g., a table with `table.TypeName = "foods"` will produce a "GetFoodByID()" function. func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl { funcName := "Get" + tbl.TypeName + "ByID" recv := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")}}} arg := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("id")}, Type: ast.NewIdent(tbl.TypeIDName)}}} result := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("ret")}, Type: ast.NewIdent(tbl.TypeName)}, {Names: []*ast.Ident{ast.NewIdent("err")}, Type: ast.NewIdent("error")}}} selectCols := make([]string, 0, len(tbl.Columns)) for _, col := range tbl.Columns { selectCols = append(selectCols, col.Name) } selectStmt := fmt.Sprintf("\n\t select %s\n\t from %s\n\t where rowid = ?\n\t", strings.Join(selectCols, ", "), tbl.TableName) funcBody := &ast.BlockStmt{ List: []ast.Stmt{ &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("err")}, Tok: token.ASSIGN, Rhs: []ast.Expr{&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("db.DB"), Sel: ast.NewIdent("Get")}, Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: ast.NewIdent("ret")}, &ast.BasicLit{Kind: token.STRING, Value: "`" + selectStmt + "`"}, ast.NewIdent("id")}}}, }, &ast.IfStmt{ Cond: &ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("errors"), Sel: ast.NewIdent("Is")}, Args: []ast.Expr{ast.NewIdent("err"), &ast.SelectorExpr{X: ast.NewIdent("sql"), Sel: ast.NewIdent("ErrNoRows")}}}, Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ReturnStmt{Results: []ast.Expr{&ast.CompositeLit{Type: ast.NewIdent(tbl.TypeName)}, ast.NewIdent("ErrNotInDB")}}}}, }, &ast.ReturnStmt{}, }, } funcDecl := &ast.FuncDecl{ Recv: recv, Name: ast.NewIdent(funcName), Type: &ast.FuncType{Params: arg, Results: result}, Body: funcBody, } return funcDecl } // GenerateDeleteItemFunc produces an AST for the `DeleteXyz()` function. // E.g., a table with `table.TypeName = "foods"` will produce a "DeleteFood()" function. func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl { funcName := "Delete" + tbl.TypeName recv := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")}}} arg := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent(tbl.VarName)}, Type: ast.NewIdent(tbl.TypeName)}}} funcBody := &ast.BlockStmt{ List: []ast.Stmt{ &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("result"), ast.NewIdent("err")}, Tok: token.DEFINE, Rhs: []ast.Expr{&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("db.DB"), Sel: ast.NewIdent("Exec")}, Args: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: "`delete from " + tbl.TableName + " where rowid = ?`"}, &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")}}}}, }, &ast.IfStmt{Cond: &ast.BinaryExpr{X: ast.NewIdent("err"), Op: token.NEQ, Y: ast.NewIdent("nil")}, Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ExprStmt{X: &ast.CallExpr{Fun: ast.NewIdent("panic"), Args: []ast.Expr{ast.NewIdent("err")}}}}}}, &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("count"), ast.NewIdent("err")}, Tok: token.DEFINE, Rhs: []ast.Expr{&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("RowsAffected")}, Args: []ast.Expr{}}}, }, &ast.IfStmt{Cond: &ast.BinaryExpr{X: ast.NewIdent("err"), Op: token.NEQ, Y: ast.NewIdent("nil")}, Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ExprStmt{X: &ast.CallExpr{Fun: ast.NewIdent("panic"), Args: []ast.Expr{ast.NewIdent("err")}}}}}}, &ast.IfStmt{ Cond: &ast.BinaryExpr{X: ast.NewIdent("count"), Op: token.NEQ, Y: &ast.BasicLit{Kind: token.INT, Value: "1"}}, Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ExprStmt{X: &ast.CallExpr{Fun: ast.NewIdent("panic"), Args: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("fmt.Errorf"), Args: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"tried to delete %s with ID (%%d) but it doesn't exist\"", strings.ToLower(tbl.TypeName))}, &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")}}}}}}}}, }, }, } funcDecl := &ast.FuncDecl{ Recv: recv, Name: ast.NewIdent(funcName), Type: &ast.FuncType{Params: arg, Results: nil}, Body: funcBody, } return funcDecl }