diff --git a/gotry_generate/main.go b/gotry_generate/main.go index e4d552a..6584b40 100644 --- a/gotry_generate/main.go +++ b/gotry_generate/main.go @@ -4,11 +4,9 @@ import ( "bufio" "flag" "fmt" - "io" "os" "os/exec" "path" - "path/filepath" "regexp" "strings" "text/template" @@ -26,10 +24,11 @@ type line struct { UseExt bool } -func parseDefinitions(defFileName, outFileName string, useExt bool) (err try.Err) { +func parseDefinitions(defFileName, outFileName, pkgName string) (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) @@ -66,10 +65,12 @@ func parseDefinitions(defFileName, outFileName string, useExt bool) (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, })) @@ -118,64 +119,12 @@ 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.Catch(func(err try.Err) { - fmt.Println("Error: " + err.Error()) - }) + defer try.CatchTrace(1) // Read flags var definitionFile string @@ -186,24 +135,16 @@ 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) { - fmt.Println("Definition file does not exist") + try.Check(try.NewErr("Definition file does not exist")) } else { - try.Check(parseDefinitions(definitionFile, - path.Join(targetDir, typeFile+".go"), *flagUseExt)) + try.Check( + parseDefinitions(definitionFile, path.Join(targetDir, typeFile+".go"), + path.Base(targetDir))) } } diff --git a/try/try.go b/try/try.go index 650c878..f24576b 100644 --- a/try/try.go +++ b/try/try.go @@ -12,7 +12,6 @@ import ( "os" "regexp" "runtime" - "runtime/debug" "strings" ) @@ -42,12 +41,20 @@ type tryErrData struct { } type call struct { + File string `json:"file"` Function string `json:"fn"` Line int `json:"l"` } -// newErr wraps a standard error into a try-compatible one with extended stack info. -func newErr(err error) *tryErr { +// 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 + } + terr := &tryErr{err: err, callStack: []call{}} pc, _, _, ok := runtime.Caller(0) @@ -75,15 +82,15 @@ func newErr(err error) *tryErr { continue } - terr.callStack = append(terr.callStack, call{Line: l, Function: fnc}) + terr.callStack = append(terr.callStack, call{File: f, Function: fnc, Line: l}) } return terr } // NewErr creates a new try-compatible error with extended stack info. -func NewErr(msg string) *tryErr { - return newErr(errors.New(msg)) +func NewErr(msg string) Err { + return FromErr(errors.New(msg)) } // Error returns the standard go error message (initial message + annotations). @@ -106,9 +113,14 @@ 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.Error()) + 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):") for _, c := range e.callStack { - res += fmt.Sprintf("%s:%d\n", c.Function, c.Line) + res += fmt.Sprintf("%s:%d\n", c.File, c.Line) + res += fmt.Sprintf(" %s\n", c.Function) } return res } @@ -150,7 +162,7 @@ func Any(args ...interface{}) []interface{} { // {return err} on happy path. func Check(err error) { if err != nil { - panic(newErr(err)) + panic(FromErr(err)) } } @@ -165,7 +177,7 @@ func check(args []interface{}) { if !ok { panic("wrong signature") } - panic(newErr(err)) + panic(FromErr(err)) } } @@ -174,9 +186,6 @@ 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. @@ -203,9 +212,6 @@ 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 { @@ -218,9 +224,6 @@ 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 { @@ -231,20 +234,18 @@ func CatchAll(errorHandler func(err Err), panicHandler func(v interface{})) { } } -// 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. - +// 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) { if r := recover(); r != nil { e, ok := r.(Err) - if ok { - errorHandler(e) - } else { - println(r) - debug.PrintStack() + if !ok { + panic(r) + } + e.PrintCallStack() + if exit > 0 { + os.Exit(exit) } } } @@ -253,9 +254,6 @@ func CatchTrace(errorHandler func(err Err)) { // 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 { @@ -265,13 +263,19 @@ func Return(err *Err) { } } -// 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. +// 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. +func Annotate(err *Err, msg string) { if r := recover(); r != nil { e, ok := r.(Err) if !ok { diff --git a/try/try_test.go b/try/try_test.go index 370660b..fac96ee 100644 --- a/try/try_test.go +++ b/try/try_test.go @@ -114,21 +114,6 @@ 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) @@ -179,8 +164,9 @@ func TestNewTryErr(t *testing.T) { t.Fail() } - if strings.Count(callStack, "\n") != 4 { - fmt.Println("Call stack is not 4 lines long.\n" + callStack) + if strings.Count(callStack, "\n") != 9 { + fmt.Println("Call stack is not 9 lines long.\n" + + fmt.Sprint(strings.Count(callStack, "\n"))) t.Fail() } } @@ -213,13 +199,43 @@ func TestGetData(t *testing.T) { func TestErrCompare(t *testing.T) { err := errors.New("TestError") - tryErr := newErr(err) + tryErr := FromErr(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 3ccaa9e..4d1a62d 100644 --- a/try/types.tmpl +++ b/try/types.tmpl @@ -1,7 +1,6 @@ -package try +package {{.PkgName}} // gotry auto-generated type definitions. DO NOT EDIT. -{{- if .Imports }} import ( {{- if .UseExt }} @@ -11,9 +10,6 @@ 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.