1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-08-10 22:31:32 +02:00

fix: add runtime type validation

Signed-off-by: Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
Carsten Leue
2023-08-24 22:49:03 +02:00
parent 0f061a5099
commit d5d89b1853
12 changed files with 362 additions and 31 deletions

View File

@@ -304,3 +304,7 @@ func FoldMap[A, B any](m M.Monoid[B]) func(func(A) B) func([]A) B {
func Fold[A any](m M.Monoid[A]) func([]A) A {
return G.Fold[[]A](m)
}
func Push[A any](a A) func([]A) []A {
return G.Push[[]A](a)
}

View File

@@ -28,6 +28,14 @@ func Of[GA ~[]A, A any](value A) GA {
return GA{value}
}
func Reduce[GA ~[]A, A, B any](fa GA, f func(B, A) B, initial B) B {
return array.Reduce(fa, f, initial)
}
func ReduceWithIndex[GA ~[]A, A, B any](fa GA, f func(int, B, A) B, initial B) B {
return array.ReduceWithIndex(fa, f, initial)
}
// From constructs an array from a set of variadic arguments
func From[GA ~[]A, A any](data ...A) GA {
return data
@@ -230,3 +238,7 @@ func Fold[AS ~[]A, A any](m M.Monoid[A]) func(AS) A {
return array.Reduce(as, m.Concat, m.Empty())
}
}
func Push[GA ~[]A, A any](a A) func(GA) GA {
return F.Bind2nd(array.Push[GA, A], a)
}

View File

@@ -146,8 +146,8 @@ func FromOption[E, A any](onNone func() E) func(O.Option[A]) Either[E, A] {
return O.Fold(F.Nullary2(onNone, Left[A, E]), Right[E, A])
}
func ToOption[E, A any]() func(Either[E, A]) O.Option[A] {
return Fold(F.Ignore1of1[E](O.None[A]), O.Some[A])
func ToOption[E, A any](ma Either[E, A]) O.Option[A] {
return MonadFold(ma, F.Ignore1of1[E](O.None[A]), O.Some[A])
}
func FromError[A any](f func(a A) error) func(A) Either[error, A] {

View File

@@ -42,10 +42,27 @@ func Reduce[GA ~[]A, A, B any](fa GA, f func(B, A) B, initial B) B {
return current
}
func ReduceWithIndex[GA ~[]A, A, B any](fa GA, f func(int, B, A) B, initial B) B {
current := initial
count := len(fa)
for i := 0; i < count; i++ {
current = f(i, current, fa[i])
}
return current
}
func Append[GA ~[]A, A any](as GA, a A) GA {
return append(as, a)
}
func Push[GA ~[]A, A any](as GA, a A) GA {
l := len(as)
cpy := make(GA, l+1)
copy(cpy, as)
cpy[l] = a
return cpy
}
func Empty[GA ~[]A, A any]() GA {
return make(GA, 0)
}

View File

@@ -32,6 +32,6 @@ func ToTypeE[A any](src any) E.Either[error, A] {
func ToTypeO[A any](src any) O.Option[A] {
return F.Pipe1(
ToTypeE[A](src),
E.ToOption[error, A](),
E.ToOption[error, A],
)
}

38
lazy/example_lazy_test.go Normal file
View File

@@ -0,0 +1,38 @@
// Copyright (c) 2023 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 lazy
import (
"fmt"
"strconv"
F "github.com/IBM/fp-go/function"
)
func ExampleLazy_creation() {
// lazy function of a constant value
val := Of(42)
// create another function to transform this
valS := F.Pipe1(
val,
Map(strconv.Itoa),
)
fmt.Println(valS())
// Output:
// 42
}

91
types/array.go Normal file
View File

@@ -0,0 +1,91 @@
// Copyright (c) 2023 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 types
import (
"fmt"
"reflect"
AR "github.com/IBM/fp-go/array/generic"
E "github.com/IBM/fp-go/either"
F "github.com/IBM/fp-go/function"
T "github.com/IBM/fp-go/tuple"
)
func toUnknownArray[A any](item func(reflect.Value, Context) E.Either[Errors, A], c Context, val reflect.Value) []E.Either[Errors, A] {
l := val.Len()
res := make([]E.Either[Errors, A], l)
for i := l - 1; i >= 0; i-- {
v := val.Index(i)
res[i] = item(v, AR.Push[Context](&ContextEntry{Key: fmt.Sprintf("[%d]", i), Value: v})(c))
}
return res
}
func flattenUnknownArray[GA ~[]A, A any](as []E.Either[Errors, A]) E.Either[Errors, GA] {
return F.Pipe1(
AR.Reduce(as, func(t T.Tuple2[GA, Errors], item E.Either[Errors, A]) T.Tuple2[GA, Errors] {
return E.MonadFold(item, func(e Errors) T.Tuple2[GA, Errors] {
return T.MakeTuple2(t.F1, append(t.F2, e...))
}, func(a A) T.Tuple2[GA, Errors] {
return T.MakeTuple2(append(t.F1, a), t.F2)
})
}, T.MakeTuple2(make(GA, len(as)), make(Errors, 0))),
func(t T.Tuple2[GA, Errors]) E.Either[Errors, GA] {
if AR.IsEmpty(t.F2) {
return E.Of[Errors](t.F1)
}
return E.Left[GA](t.F2)
},
)
}
func toValidatedArray[GA ~[]A, A any](item func(reflect.Value, Context) E.Either[Errors, A], c Context, val reflect.Value) E.Either[Errors, GA] {
return F.Pipe1(
toUnknownArray(item, c, val),
flattenUnknownArray[GA, A],
)
}
func validateArray[GA ~[]A, A any](item Validate[reflect.Value, A]) func(i reflect.Value, c Context) E.Either[Errors, GA] {
var r func(i reflect.Value, c Context) E.Either[Errors, GA]
r = func(i reflect.Value, c Context) E.Either[Errors, GA] {
// check for unknow array
switch i.Kind() {
case reflect.Slice:
return toValidatedArray[GA](item, c, i)
case reflect.Array:
return toValidatedArray[GA](item, c, i)
case reflect.Pointer:
return r(i.Elem(), c)
default:
return Failure[GA](c, fmt.Sprintf("Type %T is neither an array nor a slice nor a pointer to these values", i))
}
}
return r
}
// ArrayG returns the type validator for an array
func ArrayG[GA ~[]A, A any](item Validate[reflect.Value, A]) *Type[GA, GA, reflect.Value] {
return FromValidate(validateArray[GA, A](item))
}
// Array returns the type validator for an array
func Array[A any](item Validate[reflect.Value, A]) *Type[[]A, []A, reflect.Value] {
return ArrayG[[]A, A](item)
}

41
types/array_test.go Normal file
View File

@@ -0,0 +1,41 @@
// Copyright (c) 2023 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 types
import (
"reflect"
"testing"
A "github.com/IBM/fp-go/array"
E "github.com/IBM/fp-go/either"
"github.com/stretchr/testify/assert"
)
func TestArray(t *testing.T) {
stringArray := Array(String.Validate)
validData := A.From(
reflect.ValueOf(A.From("a", "b", "c")),
reflect.ValueOf(A.Empty[string]()),
reflect.ValueOf([]string{"a", "b"}),
reflect.ValueOf(A.From(1, 2, 3)),
)
for i := 0; i < len(validData); i++ {
assert.True(t, E.IsRight(stringArray.Decode(validData[i])))
}
}

16
types/kleisli.go Normal file
View File

@@ -0,0 +1,16 @@
// Copyright (c) 2023 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 types

57
types/string.go Normal file
View File

@@ -0,0 +1,57 @@
// Copyright (c) 2023 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 types
import (
"fmt"
"reflect"
"strconv"
E "github.com/IBM/fp-go/either"
)
func validateStringFromReflect(i reflect.Value, c Context) E.Either[Errors, string] {
switch i.Kind() {
case reflect.String:
return E.Of[Errors](i.String())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return E.Of[Errors](strconv.FormatInt(i.Int(), 10))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return E.Of[Errors](strconv.FormatUint(i.Uint(), 10))
case reflect.Bool:
return E.Of[Errors](strconv.FormatBool(i.Bool()))
case reflect.Pointer:
return validateStringFromReflect(i.Elem(), c)
case reflect.Invalid:
return Failure[string](c, "Invalid value")
}
if i.CanInterface() {
if strg, ok := i.Interface().(fmt.Stringer); ok {
return E.Of[Errors](strg.String())
}
}
return E.Of[Errors](i.String())
}
// String returns the type validator for a string
func makeString() *Type[string, string, reflect.Value] {
return FromValidate(validateStringFromReflect)
}
// converts from any type to string
var String = makeString()

47
types/string_test.go Normal file
View File

@@ -0,0 +1,47 @@
// Copyright (c) 2023 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 types
import (
"reflect"
"testing"
A "github.com/IBM/fp-go/array"
E "github.com/IBM/fp-go/either"
"github.com/stretchr/testify/assert"
)
func TestString(t *testing.T) {
s := "Carsten"
validData := A.From(
reflect.ValueOf("Carsten"),
reflect.ValueOf(s),
reflect.ValueOf(&s),
reflect.ValueOf(10),
reflect.ValueOf(true),
reflect.ValueOf(false),
)
for i := 0; i < len(validData); i++ {
assert.True(t, E.IsRight(String.Decode(validData[i])))
}
invalidDataData := A.From(
reflect.ValueOf(nil),
)
for i := 0; i < len(invalidDataData); i++ {
assert.True(t, E.IsLeft(String.Decode(invalidDataData[i])))
}
}

View File

@@ -16,8 +16,6 @@
package types
import (
"fmt"
AR "github.com/IBM/fp-go/array/generic"
E "github.com/IBM/fp-go/either"
F "github.com/IBM/fp-go/function"
@@ -31,7 +29,8 @@ type (
}
ContextEntry struct {
Key string
Key string
Value any
}
Errors []*ValidationError
@@ -47,10 +46,19 @@ type (
Decode(I) E.Either[Errors, A]
}
Codec[I, O, A any] interface {
Encoder[A, O]
Decoder[I, A]
}
Guard[I, A any] func(I) option.Option[A]
Validate[I, A any] func(I, Context) E.Either[Errors, A]
Type[A, O, I any] struct {
validate func(I, Context) E.Either[Errors, A]
encode func(A) O
is func(any) option.Option[A]
is Guard[I, A]
}
)
@@ -58,15 +66,19 @@ func (t *Type[A, O, I]) Validate(i I, c Context) E.Either[Errors, A] {
return t.validate(i, c)
}
func defaultContext(value any) Context {
return AR.Of[Context](&ContextEntry{Value: value})
}
func (t *Type[A, O, I]) Decode(i I) E.Either[Errors, A] {
return t.validate(i, AR.Of[Context](&ContextEntry{}))
return t.validate(i, defaultContext(i))
}
func (t *Type[A, O, I]) Encode(a A) O {
return t.encode(a)
}
func (t *Type[A, O, I]) Is(a any) option.Option[A] {
func (t *Type[A, O, I]) Is(a I) option.Option[A] {
return t.is(a)
}
@@ -85,7 +97,10 @@ func (val *ValidationError) Error() string {
func Pipe[O, I, A, B any](ab Type[B, A, A]) func(a Type[A, O, I]) Type[B, O, I] {
return func(a Type[A, O, I]) Type[B, O, I] {
return Type[B, O, I]{
is: ab.Is,
is: F.Flow2(
a.is,
option.Chain(ab.Is),
),
validate: func(i I, c Context) E.Either[Errors, B] {
return F.Pipe1(
a.Validate(i, c),
@@ -112,27 +127,20 @@ func Failure[A any](c Context, message string) E.Either[Errors, A] {
return Failures[A](AR.Of[Errors](&ValidationError{Context: c, Message: message}))
}
func makeCanonicalType[A any]() Type[A, A, any] {
is := option.ToType[A]
return Type[A, A, any]{
is: is,
validate: func(u any, c Context) E.Either[Errors, A] {
return F.Pipe2(
u,
is,
option.Fold(
func() E.Either[Errors, A] {
return Failure[A](c, fmt.Sprintf("source is of type %T", u))
},
Success[A],
),
)
},
encode: F.Identity[A],
func guardFromValidate[A, I any](validate func(I, Context) E.Either[Errors, A]) Guard[I, A] {
return func(i I) option.Option[A] {
return F.Pipe1(
validate(i, defaultContext(i)),
E.ToOption[Errors, A],
)
}
}
var String = makeCanonicalType[string]()
var Int = makeCanonicalType[int]()
var Bool = makeCanonicalType[bool]()
// FromValidate constructs a Type instance from just the validation function
func FromValidate[A, I any](validate Validate[I, A]) *Type[A, A, I] {
return &Type[A, A, I]{
validate,
F.Identity[A],
guardFromValidate[A, I](validate),
}
}