From 0780e8dd33be521bb27baac9c16ed4410366abea Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Fri, 3 May 2019 20:09:04 +0200 Subject: [PATCH] feat: update string type checker | test: string, int, float, any typecheckers feat: string type checker pre-process min,max/fixed length(s) before returning the CheckerFunc feat: add typecheckers for 'any', 'float' and 'int' (managing number overflows) --- typecheck/builtin/any_test.go | 78 ++++++++++++ typecheck/builtin/float64.go | 5 +- typecheck/builtin/int.go | 40 +++++- typecheck/builtin/int_test.go | 107 ++++++++++++++++ typecheck/builtin/string.go | 76 ++++++------ typecheck/builtin/string_test.go | 204 +++++++++++++++++++++++++++++++ 6 files changed, 466 insertions(+), 44 deletions(-) create mode 100644 typecheck/builtin/any_test.go create mode 100644 typecheck/builtin/int_test.go create mode 100644 typecheck/builtin/string_test.go diff --git a/typecheck/builtin/any_test.go b/typecheck/builtin/any_test.go new file mode 100644 index 0000000..ee178ff --- /dev/null +++ b/typecheck/builtin/any_test.go @@ -0,0 +1,78 @@ +package builtin_test + +import ( + "testing" + + "git.xdrm.io/go/aicra/typecheck/builtin" +) + +func TestAny_New(t *testing.T) { + inst := interface{}(builtin.NewAny()) + + switch cast := inst.(type) { + case *builtin.Any: + return + default: + t.Errorf("expect %T ; got %T", &builtin.Any{}, cast) + } +} + +func TestAny_AvailableTypes(t *testing.T) { + + inst := builtin.NewAny() + + tests := []struct { + Type string + Handled bool + }{ + {"any", true}, + {" any", false}, + {"any ", false}, + {" any ", false}, + {"Any", false}, + {"ANY", false}, + {"anything-else", false}, + } + + for _, test := range tests { + checker := inst.Checker(test.Type) + + if checker == nil { + if test.Handled { + t.Errorf("expect %q to be handled", test.Type) + } + continue + } + + if !test.Handled { + t.Errorf("expect %q NOT to be handled", test.Type) + } + } + +} + +func TestAny_AlwaysTrue(t *testing.T) { + const typeName = "any" + + checker := builtin.NewAny().Checker(typeName) + if checker == nil { + t.Errorf("expect %q to be handled", typeName) + t.Fail() + } + + values := []interface{}{ + 1, + 0.1, + nil, + "string", + []byte("bytes"), + } + + for i, value := range values { + if !checker(value) { + t.Errorf("%d: expect value to be valid", i) + t.Fail() + } + } + +} diff --git a/typecheck/builtin/float64.go b/typecheck/builtin/float64.go index 912b97b..7dc849c 100644 --- a/typecheck/builtin/float64.go +++ b/typecheck/builtin/float64.go @@ -1,7 +1,7 @@ package builtin import ( - "strconv" + "encoding/json" "git.xdrm.io/go/aicra/typecheck" ) @@ -41,7 +41,8 @@ func readFloat(value interface{}) (float64, bool) { // serialized string -> try to convert to float case string: - floatVal, err := strconv.ParseFloat(cast, 64) + num := json.Number(cast) + floatVal, err := num.Float64() return floatVal, err == nil // unknown type diff --git a/typecheck/builtin/int.go b/typecheck/builtin/int.go index 2e5ced9..806838f 100644 --- a/typecheck/builtin/int.go +++ b/typecheck/builtin/int.go @@ -1,6 +1,9 @@ package builtin import ( + "encoding/json" + "math" + "git.xdrm.io/go/aicra/typecheck" ) @@ -19,12 +22,37 @@ func (Int) Checker(typeName string) typecheck.CheckerFunc { return nil } return func(value interface{}) bool { - cast, isFloat := readFloat(value) + _, isInt := readInt(value) + + return isInt + } +} + +// readInt tries to read a serialized int and returns whether it succeeded. +func readInt(value interface{}) (int, bool) { + switch cast := value.(type) { + + case int: + return cast, true + + case uint: + overflows := cast > math.MaxInt64 + return int(cast), !overflows + + case float64: + intVal := int(cast) + overflows := cast < float64(math.MinInt64) || cast > float64(math.MaxInt64) + return intVal, cast == float64(intVal) && !overflows + + // serialized string -> try to convert to float + case string: + num := json.Number(cast) + intVal, err := num.Int64() + return int(intVal), err == nil + + // unknown type + default: + return 0, false - if !isFloat { - return false - } - - return cast == float64(int(cast)) } } diff --git a/typecheck/builtin/int_test.go b/typecheck/builtin/int_test.go new file mode 100644 index 0000000..a99b2f0 --- /dev/null +++ b/typecheck/builtin/int_test.go @@ -0,0 +1,107 @@ +package builtin_test + +import ( + "math" + "testing" + + "git.xdrm.io/go/aicra/typecheck/builtin" +) + +func TestInt_New(t *testing.T) { + inst := interface{}(builtin.NewInt()) + + switch cast := inst.(type) { + case *builtin.Int: + return + default: + t.Errorf("expect %T ; got %T", &builtin.Int{}, cast) + } +} + +func TestInt_AvailableTypes(t *testing.T) { + + inst := builtin.NewInt() + + tests := []struct { + Type string + Handled bool + }{ + {"int", true}, + {"Int", false}, + {"INT", false}, + {" int", false}, + {"int ", false}, + {" int ", false}, + } + + for _, test := range tests { + checker := inst.Checker(test.Type) + + if checker == nil { + if test.Handled { + t.Errorf("expect %q to be handled", test.Type) + } + continue + } + + if !test.Handled { + t.Errorf("expect %q NOT to be handled", test.Type) + } + } + +} + +func TestInt_Values(t *testing.T) { + const typeName = "int" + + checker := builtin.NewInt().Checker(typeName) + if checker == nil { + t.Errorf("expect %q to be handled", typeName) + t.Fail() + } + + tests := []struct { + Value interface{} + Valid bool + }{ + {-math.MaxInt64, true}, + {-1, true}, + {0, true}, + {1, true}, + {math.MaxInt64, true}, + + // overflows from type conversion + {uint(math.MaxInt64), true}, + {uint(math.MaxInt64 + 1), false}, + + {float64(math.MinInt64), true}, + // we cannot just substract 1 because of how precision works + {float64(math.MinInt64 - 1024 - 1), false}, + + // WARNING : this is due to how floats are compared + {float64(math.MaxInt64), false}, + // we cannot just add 1 because of how precision works + {float64(math.MaxInt64 + 1024 + 2), false}, + + {"string", false}, + {[]byte("bytes"), false}, + {-0.1, false}, + {0.1, false}, + {nil, false}, + } + + for i, test := range tests { + if checker(test.Value) { + if !test.Valid { + t.Errorf("%d: expect value to be invalid", i) + t.Fail() + } + continue + } + if test.Valid { + t.Errorf("%d: expect value to be valid", i) + t.Fail() + } + } + +} diff --git a/typecheck/builtin/string.go b/typecheck/builtin/string.go index ef2c6b2..1c9ae19 100644 --- a/typecheck/builtin/string.go +++ b/typecheck/builtin/string.go @@ -29,45 +29,49 @@ func (s String) Checker(typeName string) typecheck.CheckerFunc { return nil } + var ( + mustFail bool + min, max int + ) + + // extract fixed length + if fixedLengthMatches != nil { + exLen, ok := s.getFixedLength(fixedLengthMatches) + if !ok { + mustFail = true + } + min = exLen + max = exLen + + // extract variable length + } else if variableLengthMatches != nil { + exMin, exMax, ok := s.getVariableLength(variableLengthMatches) + if !ok { + mustFail = true + } + min = exMin + max = exMax + } + return func(value interface{}) bool { + // preprocessing error + if mustFail { + return false + } + // check type strValue, isString := value.(string) if !isString { return false } - // check fixed length - if fixedLengthMatches != nil { - - // incoherence fail - if len(fixedLengthMatches) < 2 { - return false - } - - // extract length - fixedLen, err := strconv.ParseUint(fixedLengthMatches[1], 10, 64) - if err != nil { - return false - } - - // check against value - return len(strValue) == int(fixedLen) + if isSimpleString { + return true } - // check variable length - if variableLengthMatches != nil { - - minLen, maxLen, ok := s.getVariableLength(variableLengthMatches) - if !ok { - return false - } - - // check against value - return len(strValue) >= minLen && len(strValue) <= maxLen - } - - // if should NEVER be here ; so fail - return false + // check length against previously extracted length + l := len(strValue) + return l >= min && l <= max } } @@ -79,8 +83,8 @@ func (String) getFixedLength(regexMatches []string) (int, bool) { } // extract length - fixedLength, err := strconv.ParseUint(regexMatches[1], 10, 64) - if err != nil { + fixedLength, err := strconv.ParseInt(regexMatches[1], 10, 64) + if err != nil || fixedLength < 0 { return 0, false } @@ -95,13 +99,13 @@ func (String) getVariableLength(regexMatches []string) (int, int, bool) { } // extract minimum length - minLen, err := strconv.ParseUint(regexMatches[1], 10, 64) - if err != nil { + minLen, err := strconv.ParseInt(regexMatches[1], 10, 64) + if err != nil || minLen < 0 { return 0, 0, false } // extract maximum length - maxLen, err := strconv.ParseUint(regexMatches[2], 10, 64) - if err != nil { + maxLen, err := strconv.ParseInt(regexMatches[2], 10, 64) + if err != nil || maxLen < 0 { return 0, 0, false } diff --git a/typecheck/builtin/string_test.go b/typecheck/builtin/string_test.go new file mode 100644 index 0000000..7794d1b --- /dev/null +++ b/typecheck/builtin/string_test.go @@ -0,0 +1,204 @@ +package builtin_test + +import ( + "testing" + + "git.xdrm.io/go/aicra/typecheck/builtin" +) + +func TestString_New(t *testing.T) { + inst := interface{}(builtin.NewString()) + + switch cast := inst.(type) { + case *builtin.String: + return + default: + t.Errorf("expect %T ; got %T", &builtin.String{}, cast) + } +} + +func TestString_AvailableTypes(t *testing.T) { + + inst := builtin.NewString() + + tests := []struct { + Type string + Handled bool + }{ + {"string", true}, + {"String", false}, + {"STRING", false}, + {" string", false}, + {"string ", false}, + {" string ", false}, + + {"string(1)", true}, + {"string( 1)", false}, + {"string(1 )", false}, + {"string( 1 )", false}, + + {"string(1,2)", true}, + {"string(1, 2)", true}, + {"string(1, 2)", false}, + {"string( 1,2)", false}, + {"string(1,2 )", false}, + {"string( 1,2 )", false}, + {"string( 1, 2)", false}, + {"string(1, 2 )", false}, + {"string( 1, 2 )", false}, + } + + for _, test := range tests { + checker := inst.Checker(test.Type) + + if checker == nil { + if test.Handled { + t.Errorf("expect %q to be handled", test.Type) + } + continue + } + + if !test.Handled { + t.Errorf("expect %q NOT to be handled", test.Type) + } + } + +} + +func TestString_AnyLength(t *testing.T) { + const typeName = "string" + + checker := builtin.NewString().Checker(typeName) + if checker == nil { + t.Errorf("expect %q to be handled", typeName) + t.Fail() + } + + tests := []struct { + Value interface{} + Valid bool + }{ + {"string", true}, + {[]byte("bytes"), false}, + {1, false}, + {0.1, false}, + {nil, false}, + } + + for i, test := range tests { + if checker(test.Value) { + if !test.Valid { + t.Errorf("%d: expect value to be invalid", i) + t.Fail() + } + continue + } + if test.Valid { + t.Errorf("%d: expect value to be valid", i) + t.Fail() + + } + } + +} +func TestString_FixedLength(t *testing.T) { + + tests := []struct { + Type string + Value interface{} + Valid bool + }{ + {"string(0)", "", true}, + {"string(0)", "", true}, + {"string(0)", "1", false}, + + {"string(16)", "1234567890123456", true}, + {"string(16)", "123456789012345", false}, + {"string(16)", "12345678901234567", false}, + + {"string(1000)", string(make([]byte, 1000)), true}, + {"string(1000)", string(make([]byte, 1000-1)), false}, + {"string(1000)", string(make([]byte, 1000+1)), false}, + } + + for i, test := range tests { + checker := builtin.NewString().Checker(test.Type) + if checker == nil { + t.Errorf("%d: expect %q to be handled", i, test.Type) + t.Fail() + continue + } + + if checker(test.Value) { + if !test.Valid { + t.Errorf("%d: expect value to be invalid", i) + t.Fail() + } + continue + } + if test.Valid { + t.Errorf("%d: expect value to be valid", i) + t.Fail() + + } + } + +} +func TestString_VariableLength(t *testing.T) { + + tests := []struct { + Type string + Value interface{} + Valid bool + }{ + {"string(0,0)", "", true}, + {"string(0,0)", "1", false}, + + {"string(0,1)", "", true}, + {"string(0,1)", "1", true}, + {"string(0,1)", "12", false}, + + {"string(5,16)", "1234", false}, + {"string(5,16)", "12345", true}, + {"string(5,16)", "123456", true}, + {"string(5,16)", "1234567", true}, + {"string(5,16)", "12345678", true}, + {"string(5,16)", "123456789", true}, + {"string(5,16)", "1234567890", true}, + {"string(5,16)", "12345678901", true}, + {"string(5,16)", "123456789012", true}, + {"string(5,16)", "1234567890123", true}, + {"string(5,16)", "12345678901234", true}, + {"string(5,16)", "123456789012345", true}, + {"string(5,16)", "1234567890123456", true}, + {"string(5,16)", "12345678901234567", false}, + + {"string(999,1000)", string(make([]byte, 998)), false}, + {"string(999,1000)", string(make([]byte, 999)), true}, + {"string(999,1000)", string(make([]byte, 1000)), true}, + {"string(999,1000)", string(make([]byte, 1001)), false}, + } + + for i, test := range tests { + checker := builtin.NewString().Checker(test.Type) + if checker == nil { + t.Errorf("%d: expect %q to be handled", i, test.Type) + t.Fail() + continue + } + + if checker(test.Value) { + if !test.Valid { + t.Errorf("%d: expect value to be invalid", i) + t.Fail() + } + continue + } + if test.Valid { + t.Errorf("%d: expect value to be valid", i) + t.Fail() + + } + } + +}