diff --git a/gotry_generate/main.go b/gotry_generate/main.go index 6584b40..e4d552a 100644 --- a/gotry_generate/main.go +++ b/gotry_generate/main.go @@ -4,9 +4,11 @@ import ( "bufio" "flag" "fmt" + "io" "os" "os/exec" "path" + "path/filepath" "regexp" "strings" "text/template" @@ -24,11 +26,10 @@ type line struct { UseExt bool } -func parseDefinitions(defFileName, outFileName, pkgName string) (err try.Err) { +func parseDefinitions(defFileName, outFileName string, useExt bool) (err try.Err) { defer try.Annotate(&err, "error parsing definitions") defFile := try.File(os.Open(defFileName)) - useExt := pkgName != "try" fileScanner := bufio.NewScanner(defFile) fileScanner.Split(bufio.ScanLines) @@ -65,12 +66,10 @@ func parseDefinitions(defFileName, outFileName, pkgName string) (err try.Err) { try.Check(tmpl.Execute(outFile, struct { Definitions []line Imports map[string]bool - PkgName string UseExt bool }{ Definitions: lines, Imports: imports, - PkgName: pkgName, UseExt: useExt, })) @@ -119,12 +118,64 @@ func parseLine(lineStr string, imports map[string]bool) (*line, try.Err) { return &lineData, nil } +func copyLibrary(targetDir string, noTests, noStds bool) (caught try.Err) { + defer try.Return(&caught) + + filesToCopy := []string{"try.go"} + + if !noTests { + filesToCopy = append(filesToCopy, "try_test.go") + } + if !noStds { + filesToCopy = append(filesToCopy, "std_types.go") + if !noTests { + filesToCopy = append(filesToCopy, "std_types_test.go") + } + } + + for _, fname := range filesToCopy { + f, e := gotry.TryFiles.Open("try/" + fname) + try.Check(e) + + targetPath := filepath.Join(targetDir, fname) + dest := try.File(os.Create(targetPath)) + + try.Empty(io.Copy(dest, f)) + fmt.Printf("Copied %s\n", fname) + + try.Check(f.Close()) + try.Check(dest.Close()) + } + + // Add go-generate command + gencmd := "\n//go:generate go run code.thetadev.de/ThetaDev/gotry/gotry_generate " + + "-def types.csv -o .\n" + f := try.File( + os.OpenFile(filepath.Join(targetDir, "try.go"), os.O_APPEND|os.O_WRONLY, 0o644)) + try.Empty(f.WriteString(gencmd)) + try.Check(f.Close()) + + // If not present, add definition file template + definitionFile := filepath.Join(targetDir, "types.csv") + + if _, e := os.Stat(definitionFile); os.IsNotExist(e) { + f := try.File(os.Create(definitionFile)) + try.Int(f.WriteString( + "# Add your type definitions here\n# Example:\n# DB;*gorm.DB;gorm.io/gorm\n")) + try.Check(f.Close()) + } + + return +} + func goFmt(goFile string) error { return exec.Command("gofmt", "-s", "-w", goFile).Run() } func main() { - defer try.CatchTrace(1) + defer try.Catch(func(err try.Err) { + fmt.Println("Error: " + err.Error()) + }) // Read flags var definitionFile string @@ -135,16 +186,24 @@ func main() { "Definition file") flag.StringVar(&targetDir, "o", "try", "Target directory") flag.StringVar(&typeFile, "of", "types", "Name of generated type definition file") + flagInit := flag.Bool("init", false, "Copy gotry package") + flagNoTests := flag.Bool("no-tests", false, "Dont copy tests") + flagNoStds := flag.Bool("no-std", false, "Dont copy std types") + flagUseExt := flag.Bool("ext", false, "Use gotry as an external dependency") flag.Parse() try.Check(os.MkdirAll(targetDir, 0o777)) + // Copy library + if *flagInit { + try.Check(copyLibrary(targetDir, *flagNoTests, *flagNoStds)) + } + // Generate types if _, e := os.Stat(definitionFile); os.IsNotExist(e) { - try.Check(try.NewErr("Definition file does not exist")) + fmt.Println("Definition file does not exist") } else { - try.Check( - parseDefinitions(definitionFile, path.Join(targetDir, typeFile+".go"), - path.Base(targetDir))) + try.Check(parseDefinitions(definitionFile, + path.Join(targetDir, typeFile+".go"), *flagUseExt)) } } diff --git a/try/try.go b/try/try.go index f24576b..650c878 100644 --- a/try/try.go +++ b/try/try.go @@ -12,6 +12,7 @@ import ( "os" "regexp" "runtime" + "runtime/debug" "strings" ) @@ -41,20 +42,12 @@ type tryErrData struct { } type call struct { - File string `json:"file"` Function string `json:"fn"` Line int `json:"l"` } -// FromErr wraps a standard error into a try-compatible one with extended stack info. -func FromErr(err error) Err { - // If the error is already try-compatible, return - //nolint:errorlint - cterr, ok := err.(Err) - if ok { - return cterr - } - +// newErr wraps a standard error into a try-compatible one with extended stack info. +func newErr(err error) *tryErr { terr := &tryErr{err: err, callStack: []call{}} pc, _, _, ok := runtime.Caller(0) @@ -82,15 +75,15 @@ func FromErr(err error) Err { continue } - terr.callStack = append(terr.callStack, call{File: f, Function: fnc, Line: l}) + terr.callStack = append(terr.callStack, call{Line: l, Function: fnc}) } return terr } // NewErr creates a new try-compatible error with extended stack info. -func NewErr(msg string) Err { - return FromErr(errors.New(msg)) +func NewErr(msg string) *tryErr { + return newErr(errors.New(msg)) } // Error returns the standard go error message (initial message + annotations). @@ -113,14 +106,9 @@ func (e *tryErr) CallStack() []call { // CallStackString outputs the call stack as a printable multiline string. func (e *tryErr) CallStackString() string { - res := fmt.Sprintf("ERROR: %s\n", e.err.Error()) - for _, msg := range e.annotations { - res += fmt.Sprintln(msg) - } - res += fmt.Sprintln("\nTraceback (most recent call first):") + res := fmt.Sprintf("ERROR: %s\n", e.Error()) for _, c := range e.callStack { - res += fmt.Sprintf("%s:%d\n", c.File, c.Line) - res += fmt.Sprintf(" %s\n", c.Function) + res += fmt.Sprintf("%s:%d\n", c.Function, c.Line) } return res } @@ -162,7 +150,7 @@ func Any(args ...interface{}) []interface{} { // {return err} on happy path. func Check(err error) { if err != nil { - panic(FromErr(err)) + panic(newErr(err)) } } @@ -177,7 +165,7 @@ func check(args []interface{}) { if !ok { panic("wrong signature") } - panic(FromErr(err)) + panic(newErr(err)) } } @@ -186,6 +174,9 @@ func check(args []interface{}) { // return errors there is a Catch function. Note! The handler function f is // called only when err != nil. func Handle(err *Err, f func()) { + // This and Catch are similar but we need to call recover() here because + // how it works with defer. We cannot refactor these to use same function. + // We put real panic objects back and keep only those which are // carrying our errors. We must also call all of the handlers in defer // stack. @@ -212,6 +203,9 @@ func Handle(err *Err, f func()) { // Catch function per non error returning function. See Handle for more // information. func Catch(f func(err Err)) { + // This and Handle are similar but we need to call recover here because how + // it works with defer. We cannot refactor these 2 to use same function. + if r := recover(); r != nil { e, ok := r.(Err) if !ok { @@ -224,6 +218,9 @@ func Catch(f func(err Err)) { // CatchAll is a helper function to catch and write handlers for all errors and // all panics thrown in the current go routine. func CatchAll(errorHandler func(err Err), panicHandler func(v interface{})) { + // This and Handle are similar but we need to call recover here because how + // it works with defer. We cannot refactor these 2 to use same function. + if r := recover(); r != nil { e, ok := r.(Err) if ok { @@ -234,18 +231,20 @@ func CatchAll(errorHandler func(err Err), panicHandler func(v interface{})) { } } -// CatchTrace catches all errors and prints their call stack. -// Setting the exit parameter to a value above 0 will make the program -// exit in case of an error. -func CatchTrace(exit int) { +// CatchTrace is a helper function to catch and handle all errors. It recovers a +// panic as well and prints its call stack. This is preferred helper for go +// workers on long running servers. +func CatchTrace(errorHandler func(err Err)) { + // This and Handle are similar but we need to call recover here because how + // it works with defer. We cannot refactor these 2 to use same function. + if r := recover(); r != nil { e, ok := r.(Err) - if !ok { - panic(r) - } - e.PrintCallStack() - if exit > 0 { - os.Exit(exit) + if ok { + errorHandler(e) + } else { + println(r) + debug.PrintStack() } } } @@ -254,6 +253,9 @@ func CatchTrace(exit int) { // their errors. If you want to annotate errors see Annotate for more // information. func Return(err *Err) { + // This and Handle are similar but we need to call recover here because how + // it works with defer. We cannot refactor these two to use same function. + if r := recover(); r != nil { e, ok := r.(Err) if !ok { @@ -263,19 +265,13 @@ func Return(err *Err) { } } -// ReturnStd is like Return, but it returns the Go standard error type. -func ReturnStd(err *error) { - if r := recover(); r != nil { - e, ok := r.(Err) - if !ok { - panic(r) // Not ours, carry on panicking - } - *err = e.Unwrap() - } -} - -// Annotate adds additional messages to the error. +// Annotate is for annotating an error. It's similar to Returnf but it takes only +// two arguments: a prefix string and a pointer to error. It adds ": " between +// the prefix and the error text automatically. func Annotate(err *Err, msg string) { + // This and Handle are similar but we need to call recover here because how + // it works with defer. We cannot refactor these two to use same function. + if r := recover(); r != nil { e, ok := r.(Err) if !ok { diff --git a/try/try_test.go b/try/try_test.go index fac96ee..370660b 100644 --- a/try/try_test.go +++ b/try/try_test.go @@ -114,6 +114,21 @@ func TestPanickingCatchAll(t *testing.T) { panickingCatchAll() } +func panickingCatchTrace() { + defer CatchTrace(func(err Err) {}) + + Any(wrongSignature()) +} + +func TestPanickingCatchTrace(t *testing.T) { + defer func() { + if recover() != nil { + t.Error("panics should NOT carry on when tracing") + } + }() + panickingCatchTrace() +} + func panickingReturn() { var err Err defer Return(&err) @@ -164,9 +179,8 @@ func TestNewTryErr(t *testing.T) { t.Fail() } - if strings.Count(callStack, "\n") != 9 { - fmt.Println("Call stack is not 9 lines long.\n" + - fmt.Sprint(strings.Count(callStack, "\n"))) + if strings.Count(callStack, "\n") != 4 { + fmt.Println("Call stack is not 4 lines long.\n" + callStack) t.Fail() } } @@ -199,43 +213,13 @@ func TestGetData(t *testing.T) { func TestErrCompare(t *testing.T) { err := errors.New("TestError") - tryErr := FromErr(err) + tryErr := newErr(err) if !errors.Is(tryErr, err) { t.Fail() } } -func TestReturnStd(t *testing.T) { - tf := func() (err error) { - defer ReturnStd(&err) - String(throw()) - return - } - - err := tf() - - if err.Error() != "this is an ERROR" { - t.Fail() - } -} - -func TestCheckTryErr(t *testing.T) { - testErr := NewErr("TestErr") - - tf := func() (err Err) { - defer Return(&err) - Check(testErr) - return - } - - err := tf() - - if err != testErr { - t.Fail() - } -} - func ExampleReturn() { var err Err defer Return(&err) diff --git a/try/types.tmpl b/try/types.tmpl index 4d1a62d..3ccaa9e 100644 --- a/try/types.tmpl +++ b/try/types.tmpl @@ -1,6 +1,7 @@ -package {{.PkgName}} +package try // gotry auto-generated type definitions. DO NOT EDIT. +{{- if .Imports }} import ( {{- if .UseExt }} @@ -10,6 +11,9 @@ import ( "{{$pkg}}" {{- end}} ) +{{- else if .UseExt }} +import "code.thetadev.de/ThetaDev/gotry/try" +{{- end}} {{range $d := .Definitions }} // {{$d.Name}} is a helper method to handle errors of // func() ({{range $i, $t := $d.Types}}{{$t}}, {{end}}error) functions.