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)
This commit is contained in:
Adrien Marquès 2019-05-03 20:09:04 +02:00
parent bafaad97e6
commit 0780e8dd33
6 changed files with 466 additions and 44 deletions

View File

@ -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()
}
}
}

View File

@ -1,7 +1,7 @@
package builtin package builtin
import ( import (
"strconv" "encoding/json"
"git.xdrm.io/go/aicra/typecheck" "git.xdrm.io/go/aicra/typecheck"
) )
@ -41,7 +41,8 @@ func readFloat(value interface{}) (float64, bool) {
// serialized string -> try to convert to float // serialized string -> try to convert to float
case string: case string:
floatVal, err := strconv.ParseFloat(cast, 64) num := json.Number(cast)
floatVal, err := num.Float64()
return floatVal, err == nil return floatVal, err == nil
// unknown type // unknown type

View File

@ -1,6 +1,9 @@
package builtin package builtin
import ( import (
"encoding/json"
"math"
"git.xdrm.io/go/aicra/typecheck" "git.xdrm.io/go/aicra/typecheck"
) )
@ -19,12 +22,37 @@ func (Int) Checker(typeName string) typecheck.CheckerFunc {
return nil return nil
} }
return func(value interface{}) bool { 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))
} }
} }

View File

@ -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()
}
}
}

View File

@ -29,45 +29,49 @@ func (s String) Checker(typeName string) typecheck.CheckerFunc {
return nil 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 { return func(value interface{}) bool {
// preprocessing error
if mustFail {
return false
}
// check type // check type
strValue, isString := value.(string) strValue, isString := value.(string)
if !isString { if !isString {
return false return false
} }
// check fixed length if isSimpleString {
if fixedLengthMatches != nil { return true
// 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)
} }
// check variable length // check length against previously extracted length
if variableLengthMatches != nil { l := len(strValue)
return l >= min && l <= max
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
} }
} }
@ -79,8 +83,8 @@ func (String) getFixedLength(regexMatches []string) (int, bool) {
} }
// extract length // extract length
fixedLength, err := strconv.ParseUint(regexMatches[1], 10, 64) fixedLength, err := strconv.ParseInt(regexMatches[1], 10, 64)
if err != nil { if err != nil || fixedLength < 0 {
return 0, false return 0, false
} }
@ -95,13 +99,13 @@ func (String) getVariableLength(regexMatches []string) (int, int, bool) {
} }
// extract minimum length // extract minimum length
minLen, err := strconv.ParseUint(regexMatches[1], 10, 64) minLen, err := strconv.ParseInt(regexMatches[1], 10, 64)
if err != nil { if err != nil || minLen < 0 {
return 0, 0, false return 0, 0, false
} }
// extract maximum length // extract maximum length
maxLen, err := strconv.ParseUint(regexMatches[2], 10, 64) maxLen, err := strconv.ParseInt(regexMatches[2], 10, 64)
if err != nil { if err != nil || maxLen < 0 {
return 0, 0, false return 0, 0, false
} }

View File

@ -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()
}
}
}