Skip to content

Commit

Permalink
feat: Add HandleErrorFunc for ORM
Browse files Browse the repository at this point in the history
  • Loading branch information
ginokent committed Aug 12, 2024
1 parent 8ea5a7a commit 691e056
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 118 deletions.
298 changes: 217 additions & 81 deletions internal/arcgen/lang/go/generate_orm_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import (
"github.com/kunitsucom/arcgen/internal/config"
)

const (
importName = "orm"
receiverName = "_orm"
queryerContextVarName = "dbtx"
queryerContextTypeName = "DBTX"
)

func fprintORMCommon(osFile osFile, buf buffer, arcSrcSetSlice ARCSourceSetSlice, crudFiles []string) error {
content, err := generateORMCommonFileContent(buf, arcSrcSetSlice, crudFiles)
if err != nil {
Expand All @@ -30,12 +37,6 @@ func fprintORMCommon(osFile osFile, buf buffer, arcSrcSetSlice ARCSourceSetSlice
return nil
}

const (
importName = "orm"
queryerContextVarName = "queryerContext"
queryerContextTypeName = "QueryerContext"
)

//nolint:cyclop,funlen,gocognit,maintidx
func generateORMCommonFileContent(buf buffer, arcSrcSetSlice ARCSourceSetSlice, crudFiles []string) (string, error) {
astFile := &ast.File{
Expand Down Expand Up @@ -83,6 +84,216 @@ func generateORMCommonFileContent(buf buffer, arcSrcSetSlice ARCSourceSetSlice,
},
)

// type ORM interface {
// Create{StructName}(ctx context.Context, queryerContext QueryerContext, s *{Struct}) error
// ...
// }
methods := make([]*ast.Field, 0)
fset := token.NewFileSet()
for _, crudFile := range crudFiles {
rootNode, err := parser.ParseFile(fset, crudFile, nil, parser.ParseComments)
if err != nil {
// MEMO: parser.ParseFile err contains file path, so no need to log it
return "", errorz.Errorf("parser.ParseFile: %w", err)
}

// MEMO: Inspect is used to get the method declaration from the file
ast.Inspect(rootNode, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.FuncDecl:
//nolint:nestif
if n.Recv != nil && len(n.Recv.List) > 0 {
if t, ok := n.Recv.List[0].Type.(*ast.StarExpr); ok {
if ident, ok := t.X.(*ast.Ident); ok {
if ident.Name == config.GoORMStructName() {
methods = append(methods, &ast.Field{
Names: []*ast.Ident{{Name: n.Name.Name}},
Type: n.Type,
})
}
}
}
}
default:
// noop
}
return true
})
}
astFile.Decls = append(astFile.Decls,
&ast.GenDecl{
Tok: token.TYPE,
Specs: []ast.Spec{
&ast.TypeSpec{
Name: &ast.Ident{Name: config.GoORMTypeName()},
Type: &ast.InterfaceType{
Methods: &ast.FieldList{List: methods},
},
},
},
},
)

// func NewORM(opts ...ORMOption) ORM {
// o := new(_ORM)
// for _, opt := range opts {
// opt.Apply(o)
// }
// return o
// }
astFile.Decls = append(astFile.Decls,
&ast.FuncDecl{
Name: &ast.Ident{Name: "New" + config.GoORMTypeName()},
Type: &ast.FuncType{
Params: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "opts"}}, Type: &ast.Ellipsis{Elt: &ast.Ident{Name: config.GoORMTypeName() + "Option"}}}}},
Results: &ast.FieldList{List: []*ast.Field{{Type: &ast.Ident{Name: config.GoORMTypeName()}}}},
},
Body: &ast.BlockStmt{
List: []ast.Stmt{
&ast.AssignStmt{
Lhs: []ast.Expr{&ast.Ident{Name: "o"}},
Tok: token.DEFINE,
Rhs: []ast.Expr{&ast.CallExpr{Fun: &ast.Ident{Name: "new"}, Args: []ast.Expr{&ast.Ident{Name: config.GoORMStructName()}}}},
},
&ast.RangeStmt{
Key: &ast.Ident{Name: "_"},
Value: &ast.Ident{Name: "opt"},
Tok: token.DEFINE,
X: &ast.Ident{Name: "opts"},
Body: &ast.BlockStmt{
List: []ast.Stmt{
&ast.ExprStmt{X: &ast.CallExpr{Fun: &ast.SelectorExpr{X: &ast.Ident{Name: "opt"}, Sel: &ast.Ident{Name: "Apply"}}, Args: []ast.Expr{&ast.Ident{Name: "o"}}}},
},
},
},
&ast.ReturnStmt{Results: []ast.Expr{&ast.Ident{Name: "o"}}},
},
},
},
)

// type ORMOption interface {
// Apply(o *_ORM)
// }
astFile.Decls = append(astFile.Decls, &ast.GenDecl{
Tok: token.TYPE,
Specs: []ast.Spec{&ast.TypeSpec{
Name: &ast.Ident{Name: config.GoORMTypeName() + "Option"},
Type: &ast.InterfaceType{Methods: &ast.FieldList{List: []*ast.Field{{
Names: []*ast.Ident{{Name: "Apply"}},
Type: &ast.FuncType{Params: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "o"}}, Type: &ast.StarExpr{X: &ast.Ident{Name: config.GoORMStructName()}}}}}},
}}}},
}},
})

//nolint:dupword
// func NewORMOptionHandleErrorFunc(handleErrorFunc func(ctx context.Context, err error) error) ORMOption {
// return &ormOptionHandleErrorFunc{handleErrorFunc: handleErrorFunc}
// }
// type ormOptionHandleErrorFunc struct {
// handleErrorFunc func(ctx context.Context, err error) error
// }
// func (o *ormOptionHandleErrorFunc) Apply(s *_ORM) {
// s.HandleErrorFunc = o.handleErrorFunc
// }
astFile.Decls = append(astFile.Decls,
&ast.FuncDecl{
Name: &ast.Ident{Name: "New" + config.GoORMTypeName() + "OptionHandleErrorFunc"},
Type: &ast.FuncType{
Params: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "handleErrorFunc"}}, Type: &ast.FuncType{Params: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "ctx"}}, Type: &ast.Ident{Name: "context.Context"}}, {Names: []*ast.Ident{{Name: "err"}}, Type: &ast.Ident{Name: "error"}}}}, Results: &ast.FieldList{List: []*ast.Field{{Type: &ast.Ident{Name: "error"}}}}}}}},
Results: &ast.FieldList{List: []*ast.Field{{Type: &ast.Ident{Name: config.GoORMTypeName() + "Option"}}}},
},
Body: &ast.BlockStmt{List: []ast.Stmt{
&ast.ReturnStmt{Results: []ast.Expr{
&ast.UnaryExpr{Op: token.AND, X: &ast.CompositeLit{
Type: &ast.Ident{Name: "ormOptionHandleErrorFunc"},
Elts: []ast.Expr{&ast.KeyValueExpr{Key: &ast.Ident{Name: "handleErrorFunc"}, Value: &ast.Ident{Name: "handleErrorFunc"}}},
}},
}},
}},
},
&ast.GenDecl{
Tok: token.TYPE,
Specs: []ast.Spec{&ast.TypeSpec{
Name: &ast.Ident{Name: "ormOptionHandleErrorFunc"},
Type: &ast.StructType{Fields: &ast.FieldList{List: []*ast.Field{{
Names: []*ast.Ident{{Name: "handleErrorFunc"}},
Type: &ast.FuncType{Params: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "ctx"}}, Type: &ast.Ident{Name: "context.Context"}}, {Names: []*ast.Ident{{Name: "err"}}, Type: &ast.Ident{Name: "error"}}}}, Results: &ast.FieldList{List: []*ast.Field{{Type: &ast.Ident{Name: "error"}}}}},
}}}},
}},
},
&ast.FuncDecl{
Recv: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "o"}}, Type: &ast.StarExpr{X: &ast.Ident{Name: "ormOptionHandleErrorFunc"}}}}},
Name: &ast.Ident{Name: "Apply"},
Type: &ast.FuncType{Params: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "s"}}, Type: &ast.StarExpr{X: &ast.Ident{Name: config.GoORMStructName()}}}}}},
Body: &ast.BlockStmt{
List: []ast.Stmt{&ast.AssignStmt{
Lhs: []ast.Expr{&ast.SelectorExpr{X: &ast.Ident{Name: "s"}, Sel: &ast.Ident{Name: "HandleErrorFunc"}}},
Tok: token.ASSIGN,
Rhs: []ast.Expr{&ast.SelectorExpr{X: &ast.Ident{Name: "o"}, Sel: &ast.Ident{Name: "handleErrorFunc"}}},
}},
},
},
)

// type _ORM struct {
// HandleErrorFunc func(ctx context.Context, err error) error
// }
astFile.Decls = append(astFile.Decls,
&ast.GenDecl{
Tok: token.TYPE,
Specs: []ast.Spec{
&ast.TypeSpec{
Name: &ast.Ident{Name: config.GoORMStructName()},
Type: &ast.StructType{Fields: &ast.FieldList{List: []*ast.Field{{
Names: []*ast.Ident{{Name: "HandleErrorFunc"}},
Type: &ast.FuncType{
Params: &ast.FieldList{List: []*ast.Field{
{Names: []*ast.Ident{{Name: "ctx"}}, Type: &ast.Ident{Name: "context.Context"}},
{Names: []*ast.Ident{{Name: "err"}}, Type: &ast.Ident{Name: "error"}},
}},
Results: &ast.FieldList{List: []*ast.Field{{Type: &ast.Ident{Name: "error"}}}},
},
}}}},
},
},
},
)

// func (o *_ORM) HandleError(ctx context.Context, err error) error {
// if o.HandleErrorFunc != nil {
// return o.HandleErrorFunc(ctx, err)
// }
// return err
// }
astFile.Decls = append(astFile.Decls,
&ast.FuncDecl{
Recv: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "o"}}, Type: &ast.StarExpr{X: &ast.Ident{Name: config.GoORMStructName()}}}}},
Name: &ast.Ident{Name: "HandleError"},
Type: &ast.FuncType{
Params: &ast.FieldList{List: []*ast.Field{
{Names: []*ast.Ident{{Name: "ctx"}}, Type: &ast.Ident{Name: "context.Context"}},
{Names: []*ast.Ident{{Name: "err"}}, Type: &ast.Ident{Name: "error"}},
}},
Results: &ast.FieldList{List: []*ast.Field{{Type: &ast.Ident{Name: "error"}}}},
},
Body: &ast.BlockStmt{
List: []ast.Stmt{
&ast.IfStmt{
Cond: &ast.BinaryExpr{X: &ast.SelectorExpr{X: &ast.Ident{Name: "o"}, Sel: &ast.Ident{Name: "HandleErrorFunc"}}, Op: token.NEQ, Y: &ast.Ident{Name: "nil"}},
Body: &ast.BlockStmt{List: []ast.Stmt{
&ast.ReturnStmt{Results: []ast.Expr{&ast.CallExpr{
Fun: &ast.SelectorExpr{X: &ast.Ident{Name: "o"}, Sel: &ast.Ident{Name: "HandleErrorFunc"}},
Args: []ast.Expr{&ast.Ident{Name: "ctx"}, &ast.Ident{Name: "err"}},
}}},
}},
},
&ast.ReturnStmt{Results: []ast.Expr{&ast.Ident{Name: "err"}}},
},
},
},
)

// type QueryerContext interface {
// ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
// PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
Expand Down Expand Up @@ -159,20 +370,6 @@ func generateORMCommonFileContent(buf buffer, arcSrcSetSlice ARCSourceSetSlice,
},
)

// type _ORM struct {
// }
astFile.Decls = append(astFile.Decls,
&ast.GenDecl{
Tok: token.TYPE,
Specs: []ast.Spec{
&ast.TypeSpec{
Name: &ast.Ident{Name: config.GoORMStructName()},
Type: &ast.StructType{Fields: &ast.FieldList{}},
},
},
},
)

// func LoggerFromContext(ctx context.Context) *slog.Logger {
// if ctx == nil {
// return slog.Default()
Expand Down Expand Up @@ -233,67 +430,6 @@ func generateORMCommonFileContent(buf buffer, arcSrcSetSlice ARCSourceSetSlice,
},
)

// type ORM interface {
// Create{StructName}(ctx context.Context, queryerContext QueryerContext, s *{Struct}) error
// ...
// }
methods := make([]*ast.Field, 0)
fset := token.NewFileSet()
for _, crudFile := range crudFiles {
rootNode, err := parser.ParseFile(fset, crudFile, nil, parser.ParseComments)
if err != nil {
// MEMO: parser.ParseFile err contains file path, so no need to log it
return "", errorz.Errorf("parser.ParseFile: %w", err)
}

// MEMO: Inspect is used to get the method declaration from the file
ast.Inspect(rootNode, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.FuncDecl:
//nolint:nestif
if n.Recv != nil && len(n.Recv.List) > 0 {
if t, ok := n.Recv.List[0].Type.(*ast.StarExpr); ok {
if ident, ok := t.X.(*ast.Ident); ok {
if ident.Name == config.GoORMStructName() {
methods = append(methods, &ast.Field{
Names: []*ast.Ident{{Name: n.Name.Name}},
Type: n.Type,
})
}
}
}
}
default:
// noop
}
return true
})
}
astFile.Decls = append(astFile.Decls,
&ast.GenDecl{
Tok: token.TYPE,
Specs: []ast.Spec{
&ast.TypeSpec{
Name: &ast.Ident{Name: config.GoORMTypeName()},
Type: &ast.InterfaceType{
Methods: &ast.FieldList{List: methods},
},
},
},
},
)

// func NewORM() ORM {
// return &_ORM{}
// }
astFile.Decls = append(astFile.Decls,
&ast.FuncDecl{
Name: &ast.Ident{Name: "New" + config.GoORMTypeName()},
Type: &ast.FuncType{Results: &ast.FieldList{List: []*ast.Field{{Type: &ast.Ident{Name: config.GoORMTypeName()}}}}},
Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ReturnStmt{Results: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: &ast.Ident{Name: config.GoORMStructName() + "{}"}}}}}},
},
)

if err := printer.Fprint(buf, token.NewFileSet(), astFile); err != nil {
return "", errorz.Errorf("printer.Fprint: %w", err)
}
Expand Down
13 changes: 8 additions & 5 deletions internal/arcgen/lang/go/generate_orm_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ func generateCREATEContent(astFile *ast.File, arcSrcSet *ARCSourceSet) {

// const Create{StructName}Query = `INSERT INTO {table_name} ({column_name1}, {column_name2}) VALUES ($1, $2)`
//
// func (q *query) Create{StructName}(ctx context.Context, queryer QueryerContext, s *{Struct}) error {
// func (orm *_ORM) Create{StructName}(ctx context.Context, queryer QueryerContext, s *{Struct}) error {
// LoggerFromContext(ctx).Debug(Create{StructName}Query)
// if _, err := queryerContext.ExecContext(ctx, Create{StructName}Query, s.{ColumnName1}, s.{ColumnName2}); err != nil {
// return fmt.Errorf("queryerContext.ExecContext: %w", err)
// return fmt.Errorf("queryerContext.ExecContext: %w", orm.HandleError(err))
// }
// return nil
// }
Expand All @@ -42,7 +42,7 @@ func generateCREATEContent(astFile *ast.File, arcSrcSet *ARCSourceSet) {
},
},
&ast.FuncDecl{
Recv: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "q"}}, Type: &ast.StarExpr{X: &ast.Ident{Name: config.GoORMStructName()}}}}},
Recv: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: receiverName}}, Type: &ast.StarExpr{X: &ast.Ident{Name: config.GoORMStructName()}}}}},
Name: &ast.Ident{Name: funcName},
Type: &ast.FuncType{
Params: &ast.FieldList{List: []*ast.Field{
Expand Down Expand Up @@ -95,8 +95,11 @@ func generateCREATEContent(astFile *ast.File, arcSrcSet *ARCSourceSet) {
Body: &ast.BlockStmt{List: []ast.Stmt{
// return fmt.Errorf("queryerContext.ExecContext: %w", err)
&ast.ReturnStmt{Results: []ast.Expr{&ast.CallExpr{
Fun: &ast.SelectorExpr{X: &ast.Ident{Name: "fmt"}, Sel: &ast.Ident{Name: "Errorf"}},
Args: []ast.Expr{&ast.Ident{Name: strconv.Quote(queryerContextVarName + ".ExecContext: %w")}, &ast.Ident{Name: "err"}},
Fun: &ast.SelectorExpr{X: &ast.Ident{Name: "fmt"}, Sel: &ast.Ident{Name: "Errorf"}},
Args: []ast.Expr{&ast.Ident{Name: strconv.Quote(queryerContextVarName + ".ExecContext: %w")}, &ast.CallExpr{
Fun: &ast.SelectorExpr{X: &ast.Ident{Name: receiverName}, Sel: &ast.Ident{Name: "HandleError"}},
Args: []ast.Expr{&ast.Ident{Name: "ctx"}, &ast.Ident{Name: "err"}},
}},
}}},
}},
},
Expand Down
9 changes: 6 additions & 3 deletions internal/arcgen/lang/go/generate_orm_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func generateDELETEContent(astFile *ast.File, arcSrcSet *ARCSourceSet) {
},
},
&ast.FuncDecl{
Recv: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "q"}}, Type: &ast.StarExpr{X: &ast.Ident{Name: config.GoORMStructName()}}}}},
Recv: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: receiverName}}, Type: &ast.StarExpr{X: &ast.Ident{Name: config.GoORMStructName()}}}}},
Name: &ast.Ident{Name: funcName},
Type: &ast.FuncType{
Params: &ast.FieldList{List: append(
Expand Down Expand Up @@ -109,8 +109,11 @@ func generateDELETEContent(astFile *ast.File, arcSrcSet *ARCSourceSet) {
Body: &ast.BlockStmt{List: []ast.Stmt{
// return fmt.Errorf("queryerContext.ExecContext: %w", err)
&ast.ReturnStmt{Results: []ast.Expr{&ast.CallExpr{
Fun: &ast.SelectorExpr{X: &ast.Ident{Name: "fmt"}, Sel: &ast.Ident{Name: "Errorf"}},
Args: []ast.Expr{&ast.Ident{Name: strconv.Quote(queryerContextVarName + ".ExecContext: %w")}, &ast.Ident{Name: "err"}},
Fun: &ast.SelectorExpr{X: &ast.Ident{Name: "fmt"}, Sel: &ast.Ident{Name: "Errorf"}},
Args: []ast.Expr{&ast.Ident{Name: strconv.Quote(queryerContextVarName + ".ExecContext: %w")}, &ast.CallExpr{
Fun: &ast.SelectorExpr{X: &ast.Ident{Name: receiverName}, Sel: &ast.Ident{Name: "HandleError"}},
Args: []ast.Expr{&ast.Ident{Name: "ctx"}, &ast.Ident{Name: "err"}},
}},
}}},
}},
},
Expand Down
Loading

0 comments on commit 691e056

Please sign in to comment.