Compare commits

..

No commits in common. "main" and "v0.2.0" have entirely different histories.

4 changed files with 131 additions and 88 deletions

View file

@ -4,9 +4,11 @@ import (
"bufio" "bufio"
"flag" "flag"
"fmt" "fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"path" "path"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"text/template" "text/template"
@ -24,11 +26,10 @@ type line struct {
UseExt bool 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") defer try.Annotate(&err, "error parsing definitions")
defFile := try.File(os.Open(defFileName)) defFile := try.File(os.Open(defFileName))
useExt := pkgName != "try"
fileScanner := bufio.NewScanner(defFile) fileScanner := bufio.NewScanner(defFile)
fileScanner.Split(bufio.ScanLines) fileScanner.Split(bufio.ScanLines)
@ -65,12 +66,10 @@ func parseDefinitions(defFileName, outFileName, pkgName string) (err try.Err) {
try.Check(tmpl.Execute(outFile, struct { try.Check(tmpl.Execute(outFile, struct {
Definitions []line Definitions []line
Imports map[string]bool Imports map[string]bool
PkgName string
UseExt bool UseExt bool
}{ }{
Definitions: lines, Definitions: lines,
Imports: imports, Imports: imports,
PkgName: pkgName,
UseExt: useExt, UseExt: useExt,
})) }))
@ -119,12 +118,64 @@ func parseLine(lineStr string, imports map[string]bool) (*line, try.Err) {
return &lineData, nil 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 { func goFmt(goFile string) error {
return exec.Command("gofmt", "-s", "-w", goFile).Run() return exec.Command("gofmt", "-s", "-w", goFile).Run()
} }
func main() { func main() {
defer try.CatchTrace(1) defer try.Catch(func(err try.Err) {
fmt.Println("Error: " + err.Error())
})
// Read flags // Read flags
var definitionFile string var definitionFile string
@ -135,16 +186,24 @@ func main() {
"Definition file") "Definition file")
flag.StringVar(&targetDir, "o", "try", "Target directory") flag.StringVar(&targetDir, "o", "try", "Target directory")
flag.StringVar(&typeFile, "of", "types", "Name of generated type definition file") 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() flag.Parse()
try.Check(os.MkdirAll(targetDir, 0o777)) try.Check(os.MkdirAll(targetDir, 0o777))
// Copy library
if *flagInit {
try.Check(copyLibrary(targetDir, *flagNoTests, *flagNoStds))
}
// Generate types // Generate types
if _, e := os.Stat(definitionFile); os.IsNotExist(e) { 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 { } else {
try.Check( try.Check(parseDefinitions(definitionFile,
parseDefinitions(definitionFile, path.Join(targetDir, typeFile+".go"), path.Join(targetDir, typeFile+".go"), *flagUseExt))
path.Base(targetDir)))
} }
} }

View file

@ -12,6 +12,7 @@ import (
"os" "os"
"regexp" "regexp"
"runtime" "runtime"
"runtime/debug"
"strings" "strings"
) )
@ -41,20 +42,12 @@ type tryErrData struct {
} }
type call struct { type call struct {
File string `json:"file"`
Function string `json:"fn"` Function string `json:"fn"`
Line int `json:"l"` Line int `json:"l"`
} }
// FromErr wraps a standard error into a try-compatible one with extended stack info. // newErr wraps a standard error into a try-compatible one with extended stack info.
func FromErr(err error) Err { func newErr(err error) *tryErr {
// If the error is already try-compatible, return
//nolint:errorlint
cterr, ok := err.(Err)
if ok {
return cterr
}
terr := &tryErr{err: err, callStack: []call{}} terr := &tryErr{err: err, callStack: []call{}}
pc, _, _, ok := runtime.Caller(0) pc, _, _, ok := runtime.Caller(0)
@ -82,15 +75,15 @@ func FromErr(err error) Err {
continue 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 return terr
} }
// NewErr creates a new try-compatible error with extended stack info. // NewErr creates a new try-compatible error with extended stack info.
func NewErr(msg string) Err { func NewErr(msg string) *tryErr {
return FromErr(errors.New(msg)) return newErr(errors.New(msg))
} }
// Error returns the standard go error message (initial message + annotations). // 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. // CallStackString outputs the call stack as a printable multiline string.
func (e *tryErr) CallStackString() string { func (e *tryErr) CallStackString() string {
res := fmt.Sprintf("ERROR: %s\n", e.err.Error()) res := fmt.Sprintf("ERROR: %s\n", e.Error())
for _, msg := range e.annotations {
res += fmt.Sprintln(msg)
}
res += fmt.Sprintln("\nTraceback (most recent call first):")
for _, c := range e.callStack { for _, c := range e.callStack {
res += fmt.Sprintf("%s:%d\n", c.File, c.Line) res += fmt.Sprintf("%s:%d\n", c.Function, c.Line)
res += fmt.Sprintf(" %s\n", c.Function)
} }
return res return res
} }
@ -162,7 +150,7 @@ func Any(args ...interface{}) []interface{} {
// {return err} on happy path. // {return err} on happy path.
func Check(err error) { func Check(err error) {
if err != nil { if err != nil {
panic(FromErr(err)) panic(newErr(err))
} }
} }
@ -177,7 +165,7 @@ func check(args []interface{}) {
if !ok { if !ok {
panic("wrong signature") 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 // return errors there is a Catch function. Note! The handler function f is
// called only when err != nil. // called only when err != nil.
func Handle(err *Err, f func()) { 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 // 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 // carrying our errors. We must also call all of the handlers in defer
// stack. // stack.
@ -212,6 +203,9 @@ func Handle(err *Err, f func()) {
// Catch function per non error returning function. See Handle for more // Catch function per non error returning function. See Handle for more
// information. // information.
func Catch(f func(err Err)) { 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 { if r := recover(); r != nil {
e, ok := r.(Err) e, ok := r.(Err)
if !ok { 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 // CatchAll is a helper function to catch and write handlers for all errors and
// all panics thrown in the current go routine. // all panics thrown in the current go routine.
func CatchAll(errorHandler func(err Err), panicHandler func(v interface{})) { 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 { if r := recover(); r != nil {
e, ok := r.(Err) e, ok := r.(Err)
if ok { 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. // CatchTrace is a helper function to catch and handle all errors. It recovers a
// Setting the exit parameter to a value above 0 will make the program // panic as well and prints its call stack. This is preferred helper for go
// exit in case of an error. // workers on long running servers.
func CatchTrace(exit int) { 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 { if r := recover(); r != nil {
e, ok := r.(Err) e, ok := r.(Err)
if !ok { if ok {
panic(r) errorHandler(e)
} } else {
e.PrintCallStack() println(r)
if exit > 0 { debug.PrintStack()
os.Exit(exit)
} }
} }
} }
@ -254,6 +253,9 @@ func CatchTrace(exit int) {
// their errors. If you want to annotate errors see Annotate for more // their errors. If you want to annotate errors see Annotate for more
// information. // information.
func Return(err *Err) { 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 { if r := recover(); r != nil {
e, ok := r.(Err) e, ok := r.(Err)
if !ok { if !ok {
@ -263,19 +265,13 @@ func Return(err *Err) {
} }
} }
// ReturnStd is like Return, but it returns the Go standard error type. // Annotate is for annotating an error. It's similar to Returnf but it takes only
func ReturnStd(err *error) { // two arguments: a prefix string and a pointer to error. It adds ": " between
if r := recover(); r != nil { // the prefix and the error text automatically.
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) { 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 { if r := recover(); r != nil {
e, ok := r.(Err) e, ok := r.(Err)
if !ok { if !ok {

View file

@ -114,6 +114,21 @@ func TestPanickingCatchAll(t *testing.T) {
panickingCatchAll() 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() { func panickingReturn() {
var err Err var err Err
defer Return(&err) defer Return(&err)
@ -164,9 +179,8 @@ func TestNewTryErr(t *testing.T) {
t.Fail() t.Fail()
} }
if strings.Count(callStack, "\n") != 9 { if strings.Count(callStack, "\n") != 4 {
fmt.Println("Call stack is not 9 lines long.\n" + fmt.Println("Call stack is not 4 lines long.\n" + callStack)
fmt.Sprint(strings.Count(callStack, "\n")))
t.Fail() t.Fail()
} }
} }
@ -199,43 +213,13 @@ func TestGetData(t *testing.T) {
func TestErrCompare(t *testing.T) { func TestErrCompare(t *testing.T) {
err := errors.New("TestError") err := errors.New("TestError")
tryErr := FromErr(err) tryErr := newErr(err)
if !errors.Is(tryErr, err) { if !errors.Is(tryErr, err) {
t.Fail() 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() { func ExampleReturn() {
var err Err var err Err
defer Return(&err) defer Return(&err)

View file

@ -1,6 +1,7 @@
package {{.PkgName}} package try
// gotry auto-generated type definitions. DO NOT EDIT. // gotry auto-generated type definitions. DO NOT EDIT.
{{- if .Imports }}
import ( import (
{{- if .UseExt }} {{- if .UseExt }}
@ -10,6 +11,9 @@ import (
"{{$pkg}}" "{{$pkg}}"
{{- end}} {{- end}}
) )
{{- else if .UseExt }}
import "code.thetadev.de/ThetaDev/gotry/try"
{{- end}}
{{range $d := .Definitions }} {{range $d := .Definitions }}
// {{$d.Name}} is a helper method to handle errors of // {{$d.Name}} is a helper method to handle errors of
// func() ({{range $i, $t := $d.Types}}{{$t}}, {{end}}error) functions. // func() ({{range $i, $t := $d.Types}}{{$t}}, {{end}}error) functions.