diff --git a/pkg/res/failure.go b/pkg/res/failure.go new file mode 100644 index 0000000..b43cbac --- /dev/null +++ b/pkg/res/failure.go @@ -0,0 +1,47 @@ +package res + +import ( + "fmt" + "github.com/jpfourny/papaya/pkg/opt" +) + +// Failure represents a failed result. +// A failed result has an error and no value. +type Failure[T any] struct { + Err error +} + +// Assert that Failure[T] implements Result[T] +var _ Result[any] = Failure[any]{} + +func (r Failure[T]) Succeeded() bool { + return false +} + +func (r Failure[T]) PartiallySucceeded() bool { + return false +} + +func (r Failure[T]) Failed() bool { + return true +} + +func (r Failure[T]) HasError() bool { + return true +} + +func (r Failure[T]) HasValue() bool { + return false +} + +func (r Failure[T]) Value() opt.Optional[T] { + return opt.Empty[T]() +} + +func (r Failure[T]) Error() opt.Optional[error] { + return opt.Of(r.Err) +} + +func (r Failure[T]) String() string { + return fmt.Sprintf("Failure(%v)", r.Err) +} diff --git a/pkg/res/partial_success.go b/pkg/res/partial_success.go new file mode 100644 index 0000000..2500858 --- /dev/null +++ b/pkg/res/partial_success.go @@ -0,0 +1,49 @@ +package res + +import ( + "fmt" + "github.com/jpfourny/papaya/pkg/opt" +) + +// PartialSuccess represents a partially-successful result. +// A partially successful result has a value and an error. +// This is useful when a result is expected to have multiple values, and some of them are missing. +type PartialSuccess[T any] struct { + Val T + Err error +} + +// Assert that PartialSuccess[T] implements Result[T] +var _ Result[any] = PartialSuccess[any]{} + +func (r PartialSuccess[T]) Succeeded() bool { + return false +} + +func (r PartialSuccess[T]) PartiallySucceeded() bool { + return true +} + +func (r PartialSuccess[T]) Failed() bool { + return false +} + +func (r PartialSuccess[T]) HasError() bool { + return true +} + +func (r PartialSuccess[T]) HasValue() bool { + return true +} + +func (r PartialSuccess[T]) Value() opt.Optional[T] { + return opt.Of(r.Val) +} + +func (r PartialSuccess[T]) Error() opt.Optional[error] { + return opt.Of(r.Err) +} + +func (r PartialSuccess[T]) String() string { + return fmt.Sprintf("PartialSuccess(%#v, %v)", r.Val, r.Err) +} diff --git a/pkg/res/result.go b/pkg/res/result.go new file mode 100644 index 0000000..fdb7376 --- /dev/null +++ b/pkg/res/result.go @@ -0,0 +1,69 @@ +package res + +import ( + "fmt" + "github.com/jpfourny/papaya/pkg/opt" +) + +// Result represents the result of an operation that may have a value and/or an error. +// The result can be successful, failed, or partially successful. +// A successful result has a value and no error. +// A failed result has an error and no value. +// A partially successful result has both a value and an error. +// The result can be queried for its value and/or error, and for its success/failure status. +type Result[T any] interface { + fmt.Stringer + + // Succeeded returns true for a successful result; false otherwise. + // A successful result is one that has a value and no error. + Succeeded() bool + + // PartiallySucceeded returns true for a partially successful result that includes both a value and an error. + // This is useful when a result is expected to have multiple values, and some of them are missing. + PartiallySucceeded() bool + + // Failed returns true for a failed result; false otherwise. + // A failed result is one that has an error and no value. + Failed() bool + + // HasError returns true if the result has an error; false otherwise. + // This is true for both failed and partially successful results. + HasError() bool + + // HasValue returns true if the result has a value; false otherwise. + // This is true for both successful and partially successful results. + HasValue() bool + + // Value returns the value of the result as an optional. + // If the result has a value, the optional is non-empty; otherwise, it is empty. + Value() opt.Optional[T] + + // Error returns the error of the result as an optional. + // If the result has an error, the optional is non-empty; otherwise, it is empty. + Error() opt.Optional[error] +} + +// OfSuccess returns a successful result with the provided value. +func OfSuccess[T any](val T) Result[T] { + return Success[T]{Val: val} +} + +// OfPartialSuccess returns a partially successful result with the provided value and error. +func OfPartialSuccess[T any](val T, err error) Result[T] { + return PartialSuccess[T]{Val: val, Err: err} +} + +// OfFailure returns a failed result with the provided error. +func OfFailure[T any](err error) Result[T] { + return Failure[T]{Err: err} +} + +// Of returns a result with the provided value and error. +// If the error is nil, the result is successful; otherwise, it is failed. +// This is a convenience function that creates a successful or failed result based on the provided error. +func Of[T any](val T, err error) Result[T] { + if err != nil { + return OfFailure[T](err) + } + return OfSuccess[T](val) +} diff --git a/pkg/res/result_test.go b/pkg/res/result_test.go new file mode 100644 index 0000000..646135e --- /dev/null +++ b/pkg/res/result_test.go @@ -0,0 +1,148 @@ +package res + +import ( + "errors" + "testing" +) + +func TestOfSuccess(t *testing.T) { + r := OfSuccess(42) + if !r.Succeeded() { + t.Errorf("expected Succeeded() to be true") + } + if r.PartiallySucceeded() { + t.Errorf("expected PartiallySucceeded() to be false") + } + if r.Failed() { + t.Errorf("expected Failed() to be false") + } + if !r.HasValue() { + t.Errorf("expected HasValue() to be true") + } + if r.HasError() { + t.Errorf("expected HasError() to be false") + } + if r.Value().GetOrZero() != 42 { + t.Errorf("expected Value() to return 42") + } + if r.Error().Present() { + t.Errorf("expected Error() to be empty") + } + if r.String() != "Success(42)" { + t.Errorf("expected String() to return Success(42)") + } +} + +func TestOfFailure(t *testing.T) { + r := OfFailure[any](errors.New("error")) + if r.Succeeded() { + t.Errorf("expected Succeeded() to be false") + } + if r.PartiallySucceeded() { + t.Errorf("expected PartiallySucceeded() to be false") + } + if !r.Failed() { + t.Errorf("expected Failed() to be true") + } + if r.HasValue() { + t.Errorf("expected HasValue() to be false") + } + if !r.HasError() { + t.Errorf("expected HasError() to be true") + } + if r.Error().GetOrZero().Error() != "error" { + t.Errorf("expected Error() to return error") + } + if r.Value().Present() { + t.Errorf("expected Value() to be empty") + } + if r.String() != "Failure(error)" { + t.Errorf("expected String() to return Failure(error)") + } +} + +func TestOfPartialSuccess(t *testing.T) { + r := OfPartialSuccess(42, errors.New("error")) + if r.Succeeded() { + t.Errorf("expected Succeeded() to be false") + } + if !r.PartiallySucceeded() { + t.Errorf("expected PartiallySucceeded() to be true") + } + if r.Failed() { + t.Errorf("expected Failed() to be false") + } + if !r.HasValue() { + t.Errorf("expected HasValue() to be true") + } + if !r.HasError() { + t.Errorf("expected HasError() to be true") + } + if r.Value().GetOrZero() != 42 { + t.Errorf("expected Value() to return 42") + } + if !r.Error().Present() { + t.Errorf("expected Error() to be non-empty") + } + if r.String() != "PartialSuccess(42, error)" { + t.Errorf("expected String() to return PartialSuccess(42, error)") + } +} + +func TestOf(t *testing.T) { + t.Run("Success", func(t *testing.T) { + r := Of(42, nil) + if !r.Succeeded() { + t.Errorf("expected Succeeded() to be true") + } + if r.PartiallySucceeded() { + t.Errorf("expected PartiallySucceeded() to be false") + } + if r.Failed() { + t.Errorf("expected Failed() to be false") + } + if !r.HasValue() { + t.Errorf("expected HasValue() to be true") + } + if r.HasError() { + t.Errorf("expected HasError() to be false") + } + if r.Value().GetOrZero() != 42 { + t.Errorf("expected Value() to return 42") + } + if r.Error().Present() { + t.Errorf("expected Error() to be empty") + } + if r.String() != "Success(42)" { + t.Errorf("expected String() to return Success(42)") + } + }) + + t.Run("Failure", func(t *testing.T) { + r := Of(42, errors.New("error")) + if r.Succeeded() { + t.Errorf("expected Succeeded() to be false") + } + if r.PartiallySucceeded() { + t.Errorf("expected PartiallySucceeded() to be false") + } + if !r.Failed() { + t.Errorf("expected Failed() to be true") + } + if r.HasValue() { + t.Errorf("expected HasValue() to be false") + } + if !r.HasError() { + t.Errorf("expected HasError() to be true") + } + if r.Error().GetOrZero().Error() != "error" { + t.Errorf("expected Error() to return error") + } + if r.Value().Present() { + t.Errorf("expected Value() to be empty") + } + if r.String() != "Failure(error)" { + t.Errorf("expected String() to return Failure(error)") + } + }) +} diff --git a/pkg/res/success.go b/pkg/res/success.go new file mode 100644 index 0000000..67edfcb --- /dev/null +++ b/pkg/res/success.go @@ -0,0 +1,47 @@ +package res + +import ( + "fmt" + "github.com/jpfourny/papaya/pkg/opt" +) + +// Success represents a successful result. +// A successful result has a value and no error. +type Success[T any] struct { + Val T +} + +// Assert that Success[T] implements Result[T] +var _ Result[any] = Success[any]{} + +func (r Success[T]) Succeeded() bool { + return true +} + +func (r Success[T]) PartiallySucceeded() bool { + return false +} + +func (r Success[T]) Failed() bool { + return false +} + +func (r Success[T]) HasError() bool { + return false +} + +func (r Success[T]) HasValue() bool { + return true +} + +func (r Success[T]) Value() opt.Optional[T] { + return opt.Of(r.Val) +} + +func (r Success[T]) Error() opt.Optional[error] { + return opt.Empty[error]() +} + +func (r Success[T]) String() string { + return fmt.Sprintf("Success(%#v)", r.Val) +}