1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-11-23 22:14:53 +02:00
Files
fp-go/v2/idiomatic/result/validation_test.go
Dr. Carsten Leue 8a2e9539b1 fix: add result
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-17 20:36:06 +01:00

374 lines
9.4 KiB
Go

// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package result
import (
"errors"
"fmt"
"strings"
"testing"
S "github.com/IBM/fp-go/v2/semigroup"
"github.com/stretchr/testify/assert"
)
// Helper function to create a semigroup that concatenates error messages
func makeErrorConcatSemigroup() S.Semigroup[error] {
return S.MakeSemigroup(func(e1, e2 error) error {
return fmt.Errorf("%v; %v", e1, e2)
})
}
// Helper function to create a semigroup that collects error messages in a slice
func makeErrorListSemigroup() S.Semigroup[error] {
return S.MakeSemigroup(func(e1, e2 error) error {
msg1 := e1.Error()
msg2 := e2.Error()
// Parse existing lists
var msgs []string
if strings.HasPrefix(msg1, "[") && strings.HasSuffix(msg1, "]") {
trimmed := strings.Trim(msg1, "[]")
if trimmed != "" {
msgs = strings.Split(trimmed, ", ")
}
} else {
msgs = []string{msg1}
}
if strings.HasPrefix(msg2, "[") && strings.HasSuffix(msg2, "]") {
trimmed := strings.Trim(msg2, "[]")
if trimmed != "" {
msgs = append(msgs, strings.Split(trimmed, ", ")...)
}
} else {
msgs = append(msgs, msg2)
}
return fmt.Errorf("[%s]", strings.Join(msgs, ", "))
})
}
// TestApV_BothRight tests ApV when both the value and function are Right
func TestApV_BothRight(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
double := func(x int) int { return x * 2 }
value, verr := Right(5)
fn, ferr := Right(double)
result, err := apv(value, verr)(fn, ferr)
assert.NoError(t, err)
assert.Equal(t, 10, result)
}
// TestApV_ValueLeft_FunctionRight tests ApV when value is Left and function is Right
func TestApV_ValueLeft_FunctionRight(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
double := func(x int) int { return x * 2 }
valueError := errors.New("invalid value")
value, verr := Left[int](valueError)
fn, ferr := Right(double)
result, err := apv(value, verr)(fn, ferr)
assert.Error(t, err)
assert.Equal(t, valueError, err)
assert.Equal(t, 0, result) // zero value for int
}
// TestApV_ValueRight_FunctionLeft tests ApV when value is Right and function is Left
func TestApV_ValueRight_FunctionLeft(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
fnError := errors.New("invalid function")
value, verr := Right(5)
fn, ferr := Left[func(int) int](fnError)
result, err := apv(value, verr)(fn, ferr)
assert.Error(t, err)
assert.Equal(t, fnError, err)
assert.Equal(t, 0, result) // zero value for int
}
// TestApV_BothLeft tests ApV when both value and function are Left
func TestApV_BothLeft(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
valueError := errors.New("invalid value")
fnError := errors.New("invalid function")
value, verr := Left[int](valueError)
fn, ferr := Left[func(int) int](fnError)
result, err := apv(value, verr)(fn, ferr)
assert.Error(t, err)
assert.Equal(t, 0, result) // zero value for int
// Verify the error message contains both errors
expectedMsg := "invalid function; invalid value"
assert.Equal(t, expectedMsg, err.Error())
}
// TestApV_BothLeft_WithListSemigroup tests error accumulation with a list semigroup
func TestApV_BothLeft_WithListSemigroup(t *testing.T) {
sg := makeErrorListSemigroup()
apv := ApV[string, string](sg)
valueError := errors.New("error1")
fnError := errors.New("error2")
value, verr := Left[string](valueError)
fn, ferr := Left[func(string) string](fnError)
result, err := apv(value, verr)(fn, ferr)
assert.Error(t, err)
assert.Equal(t, "", result) // zero value for string
// Verify both errors are in the list
expectedMsg := "[error2, error1]"
assert.Equal(t, expectedMsg, err.Error())
}
// TestApV_StringTransformation tests ApV with string transformation
func TestApV_StringTransformation(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[string, string](sg)
toUpper := func(s string) string { return strings.ToUpper(s) }
value, verr := Right("hello")
fn, ferr := Right(toUpper)
result, err := apv(value, verr)(fn, ferr)
assert.NoError(t, err)
assert.Equal(t, "HELLO", result)
}
// TestApV_DifferentTypes tests ApV with different input and output types
func TestApV_DifferentTypes(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[string, int](sg)
intToString := func(x int) string { return fmt.Sprintf("Number: %d", x) }
value, verr := Right(42)
fn, ferr := Right(intToString)
result, err := apv(value, verr)(fn, ferr)
assert.NoError(t, err)
assert.Equal(t, "Number: 42", result)
}
// TestApV_ComplexType tests ApV with complex types (structs)
func TestApV_ComplexType(t *testing.T) {
type Person struct {
Name string
Age int
}
sg := makeErrorConcatSemigroup()
apv := ApV[string, Person](sg)
getName := func(p Person) string { return p.Name }
person := Person{Name: "Alice", Age: 30}
value, verr := Right(person)
fn, ferr := Right(getName)
result, err := apv(value, verr)(fn, ferr)
assert.NoError(t, err)
assert.Equal(t, "Alice", result)
}
// TestApV_MultipleValidations demonstrates chaining multiple validations
func TestApV_MultipleValidations(t *testing.T) {
_ = makeErrorListSemigroup() // Semigroup available for future use
// Validation functions
validatePositive := func(x int) (int, error) {
if x > 0 {
return Right(x)
}
return Left[int](errors.New("must be positive"))
}
validateEven := func(x int) (int, error) {
if x%2 == 0 {
return Right(x)
}
return Left[int](errors.New("must be even"))
}
// Test valid value (positive and even)
t.Run("valid value", func(t *testing.T) {
value, err := Right(4)
validatedPositive, err1 := validatePositive(value)
validatedEven, err2 := validateEven(validatedPositive)
assert.NoError(t, err)
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.Equal(t, 4, validatedEven)
})
// Test invalid value (negative)
t.Run("negative value", func(t *testing.T) {
value, _ := Right(-3)
_, err := validatePositive(value)
assert.Error(t, err)
assert.Equal(t, "must be positive", err.Error())
})
// Test invalid value (odd)
t.Run("odd value", func(t *testing.T) {
value, _ := Right(3)
validatedPositive, err1 := validatePositive(value)
_, err2 := validateEven(validatedPositive)
assert.NoError(t, err1)
assert.Error(t, err2)
assert.Equal(t, "must be even", err2.Error())
})
}
// TestApV_ZeroValues tests ApV with zero values
func TestApV_ZeroValues(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
identity := func(x int) int { return x }
value, verr := Right(0)
fn, ferr := Right(identity)
result, err := apv(value, verr)(fn, ferr)
assert.NoError(t, err)
assert.Equal(t, 0, result)
}
// TestApV_NilError tests that nil errors are handled correctly
func TestApV_NilError(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[string, string](sg)
identity := func(s string) string { return s }
// Right is equivalent to (value, nil)
value, verr := Right("test")
fn, ferr := Right(identity)
assert.Nil(t, verr)
assert.Nil(t, ferr)
result, err := apv(value, verr)(fn, ferr)
assert.NoError(t, err)
assert.Equal(t, "test", result)
}
// TestApV_SemigroupAssociativity tests that error combination is associative
func TestApV_SemigroupAssociativity(t *testing.T) {
sg := makeErrorConcatSemigroup()
e1 := errors.New("error1")
e2 := errors.New("error2")
e3 := errors.New("error3")
// (e1 + e2) + e3
left := sg.Concat(sg.Concat(e1, e2), e3)
// e1 + (e2 + e3)
right := sg.Concat(e1, sg.Concat(e2, e3))
assert.Equal(t, left.Error(), right.Error())
}
// TestApV_CustomSemigroup tests ApV with a custom semigroup
func TestApV_CustomSemigroup(t *testing.T) {
// Custom semigroup that counts errors
type ErrorCount struct {
count int
msg string
}
countSemigroup := S.MakeSemigroup(func(e1, e2 error) error {
// Simple counter in error message
return fmt.Errorf("combined: %v | %v", e1, e2)
})
apv := ApV[int, int](countSemigroup)
e1 := errors.New("first")
e2 := errors.New("second")
value, verr := Left[int](e1)
fn, ferr := Left[func(int) int](e2)
_, err := apv(value, verr)(fn, ferr)
assert.Error(t, err)
assert.Contains(t, err.Error(), "first")
assert.Contains(t, err.Error(), "second")
}
// BenchmarkApV_BothRight benchmarks the happy path
func BenchmarkApV_BothRight(b *testing.B) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
double := func(x int) int { return x * 2 }
value, verr := Right(5)
fn, ferr := Right(double)
b.ResetTimer()
for i := 0; i < b.N; i++ {
apv(value, verr)(fn, ferr)
}
}
// BenchmarkApV_BothLeft benchmarks the error accumulation path
func BenchmarkApV_BothLeft(b *testing.B) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
valueError := errors.New("value error")
fnError := errors.New("function error")
value, verr := Left[int](valueError)
fn, ferr := Left[func(int) int](fnError)
b.ResetTimer()
for i := 0; i < b.N; i++ {
apv(value, verr)(fn, ferr)
}
}