src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/diag/error.go (about)

     1  package diag
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"src.elv.sh/pkg/strutil"
     8  )
     9  
    10  // Error represents an error with context that can be showed.
    11  type Error[T ErrorTag] struct {
    12  	Message string
    13  	Context Context
    14  }
    15  
    16  // ErrorTag is used to parameterize [Error] into different concrete types. The
    17  // ErrorTag method is called with a zero receiver, and its return value is used
    18  // in [Error.Error] and [Error.Show].
    19  type ErrorTag interface {
    20  	ErrorTag() string
    21  }
    22  
    23  // RangeError combines error with [Ranger].
    24  type RangeError interface {
    25  	error
    26  	Ranger
    27  }
    28  
    29  // Error returns a plain text representation of the error.
    30  func (e *Error[T]) Error() string {
    31  	return errorTag[T]() + ": " + e.errorNoType()
    32  }
    33  
    34  func (e *Error[T]) errorNoType() string {
    35  	return e.Context.describeRange() + ": " + e.Message
    36  }
    37  
    38  // Range returns the range of the error.
    39  func (e *Error[T]) Range() Ranging {
    40  	return e.Context.Range()
    41  }
    42  
    43  var (
    44  	messageStart = "\033[31;1m"
    45  	messageEnd   = "\033[m"
    46  )
    47  
    48  // Show shows the error.
    49  func (e *Error[T]) Show(indent string) string {
    50  	return errorTagTitle[T]() + ": " + e.showNoType(indent)
    51  }
    52  
    53  func (e *Error[T]) showNoType(indent string) string {
    54  	indent += "  "
    55  	return messageStart + e.Message + messageEnd +
    56  		"\n" + indent + e.Context.Show(indent)
    57  }
    58  
    59  // PackErrors packs multiple instances of [Error] with the same tag into one
    60  // error:
    61  //
    62  //   - If called with no errors, it returns nil.
    63  //
    64  //   - If called with one error, it returns that error itself.
    65  //
    66  //   - If called with more than one [Error], it returns an error that combines
    67  //     all of them. The returned error also implements [Shower], and its Error
    68  //     and Show methods only print the tag once.
    69  func PackErrors[T ErrorTag](errs []*Error[T]) error {
    70  	switch len(errs) {
    71  	case 0:
    72  		return nil
    73  	case 1:
    74  		return errs[0]
    75  	default:
    76  		return append(multiError[T](nil), errs...)
    77  	}
    78  }
    79  
    80  // UnpackErrors returns the constituent [Error] instances in an error if it is
    81  // built from [PackErrors]. Otherwise it returns nil.
    82  func UnpackErrors[T ErrorTag](err error) []*Error[T] {
    83  	switch err := err.(type) {
    84  	case *Error[T]:
    85  		return []*Error[T]{err}
    86  	case multiError[T]:
    87  		return append([]*Error[T](nil), err...)
    88  	default:
    89  		return nil
    90  	}
    91  }
    92  
    93  type multiError[T ErrorTag] []*Error[T]
    94  
    95  func (err multiError[T]) Error() string {
    96  	var sb strings.Builder
    97  	fmt.Fprintf(&sb, "multiple %s: ", errorTagPlural[T]())
    98  	for i, e := range err {
    99  		if i > 0 {
   100  			sb.WriteString("; ")
   101  		}
   102  		sb.WriteString(e.errorNoType())
   103  	}
   104  	return sb.String()
   105  }
   106  
   107  func (err multiError[T]) Show(indent string) string {
   108  	var sb strings.Builder
   109  	fmt.Fprintf(&sb, "Multiple %s:", errorTagPlural[T]())
   110  	indent += "  "
   111  	for _, e := range err {
   112  		sb.WriteString("\n" + indent)
   113  		sb.WriteString(e.showNoType(indent))
   114  	}
   115  	return sb.String()
   116  }
   117  
   118  func errorTag[T ErrorTag]() string {
   119  	var t T
   120  	return t.ErrorTag()
   121  }
   122  
   123  // We don't have any error tags with an irregular plural yet. When we do, we can
   124  // let ErrorTag optionally implement interface{ ErrorTagPlural() } and use that
   125  // when available.
   126  func errorTagPlural[T ErrorTag]() string { return errorTag[T]() + "s" }
   127  
   128  func errorTagTitle[T ErrorTag]() string { return strutil.Title(errorTag[T]()) }