diff --git a/serializer/serix/decode.go b/serializer/serix/decode.go index cd785ba5..1a6f1b54 100644 --- a/serializer/serix/decode.go +++ b/serializer/serix/decode.go @@ -472,7 +472,7 @@ func (api *API) decodeMap(ctx context.Context, b []byte, value reflect.Value, return consumedBytes, err } - if err := api.checkMapMinMaxBounds(value.Len(), ts); err != nil { + if err := api.checkMinMaxBounds(value, ts); err != nil { return consumedBytes, err } diff --git a/serializer/serix/encode.go b/serializer/serix/encode.go index 51e87301..759c047b 100644 --- a/serializer/serix/encode.go +++ b/serializer/serix/encode.go @@ -342,13 +342,12 @@ func (api *API) encodeMapKVPair(ctx context.Context, key, val reflect.Value, ts func (api *API) encodeMap(ctx context.Context, value reflect.Value, valueType reflect.Type, ts TypeSettings, opts *options) ([]byte, error) { - size := value.Len() - if err := api.checkMapMinMaxBounds(size, ts); err != nil { + if err := api.checkMinMaxBounds(value, ts); err != nil { return nil, err } - data := make([][]byte, size) + data := make([][]byte, value.Len()) iter := value.MapRange() for i := 0; iter.Next(); i++ { key := iter.Key() diff --git a/serializer/serix/map_decode.go b/serializer/serix/map_decode.go index 97402a45..282c404a 100644 --- a/serializer/serix/map_decode.go +++ b/serializer/serix/map_decode.go @@ -382,7 +382,7 @@ func (api *API) mapDecodeStructFields( continue } - fieldKey := fieldKeyString(sField.name) + fieldKey := FieldKeyString(sField.name) if sField.settings.ts.fieldKey != nil { fieldKey = sField.settings.ts.MustFieldKey() } @@ -484,7 +484,7 @@ func (api *API) mapDecodeMap(ctx context.Context, mapVal any, value reflect.Valu value.SetMapIndex(keyValue, elemValue) } - if err := api.checkMapMinMaxBounds(value.Len(), ts); err != nil { + if err := api.checkMinMaxBounds(value, ts); err != nil { return err } diff --git a/serializer/serix/map_encode.go b/serializer/serix/map_encode.go index 2a95727a..79dfe75b 100644 --- a/serializer/serix/map_encode.go +++ b/serializer/serix/map_encode.go @@ -227,7 +227,7 @@ func (api *API) mapEncodeStructFields( obj.Set(k, lo.Return1(castedEleOut.Get(k))) } default: - obj.Set(fieldKeyString(sField.name), eleOut) + obj.Set(FieldKeyString(sField.name), eleOut) } } @@ -299,9 +299,7 @@ func (api *API) mapEncodeMapKVPair(ctx context.Context, key, val reflect.Value, } func (api *API) mapEncodeMap(ctx context.Context, value reflect.Value, ts TypeSettings, opts *options) (*orderedmap.OrderedMap, error) { - size := value.Len() - - if err := api.checkMapMinMaxBounds(size, ts); err != nil { + if err := api.checkMinMaxBounds(value, ts); err != nil { return nil, err } diff --git a/serializer/serix/serix.go b/serializer/serix/serix.go index 656efdc9..460a37b7 100644 --- a/serializer/serix/serix.go +++ b/serializer/serix/serix.go @@ -7,7 +7,7 @@ In order for a field to be detected by serix it must have `serix` struct tag set serix traverses all fields and handles them in the order specified in the struct. Apart from the required position you can provide the following settings to serix via struct tags: "optional" - means that field might be nil. Only valid for pointers or interfaces: `serix:"example,optional"` -"lengthPrefixType=uint32" - provide serializer.SeriLengthPrefixType for that field: `serix:"example,lengthPrefixType=unint32"` +"lenPrefix=uint32" - provide serializer.SeriLengthPrefixType for that field: `serix:"example,lenPrefix=unint32"` "nest" - handle embedded/anonymous field as a nested field: `serix:"example,nest"` See serix_text.go for more detail. */ @@ -31,10 +31,6 @@ import ( ) var ( - // ErrMapValidationMinElementsNotReached gets returned if the count of elements is too small. - ErrMapValidationMinElementsNotReached = ierrors.New("min count of elements within the map not reached") - // ErrMapValidationMaxElementsExceeded gets returned if the count of elements is too big. - ErrMapValidationMaxElementsExceeded = ierrors.New("max count of elements within the map exceeded") // ErrMapValidationMaxBytesExceeded gets returned if the serialized byte size of the map is too big. ErrMapValidationMaxBytesExceeded = ierrors.New("max bytes size of the map exceeded") // ErrMapValidationViolatesUniqueness gets returned if the map elements are not unique. @@ -204,21 +200,6 @@ func (api *API) checkMinMaxBounds(v reflect.Value, ts TypeSettings) error { return nil } -// checkMapMinMaxBounds checks whether the given map is within its defined bounds in case it has defined map rules. -func (api *API) checkMapMinMaxBounds(length int, ts TypeSettings) error { - if ts.mapRules != nil { - switch { - case ts.mapRules.MaxEntries > 0 && uint(length) > ts.mapRules.MaxEntries: - return ierrors.Wrapf(ErrMapValidationMaxElementsExceeded, "map (len %d) exceeds max length of %d ", length, ts.mapRules.MaxEntries) - - case ts.mapRules.MinEntries > 0 && uint(length) < ts.mapRules.MinEntries: - return ierrors.Wrapf(ErrMapValidationMinElementsNotReached, "map (len %d) is less than min length of %d ", length, ts.mapRules.MinEntries) - } - } - - return nil -} - // checkMapMaxByteSize checks whether the given map is within its defined max byte size in case it has defined map rules. func (api *API) checkMapMaxByteSize(byteSize int, ts TypeSettings) error { if ts.mapRules != nil && ts.mapRules.MaxByteSize > 0 && byteSize > int(ts.mapRules.MaxByteSize) { @@ -863,6 +844,13 @@ func parseStructTag(tag string, serixPosition int) (tagSettings, error) { case "omitempty": settings.omitEmpty = true + case "lenPrefix": + value, err := parseStructTagValuePrefixType("lenPrefix", keyValue, currentPart) + if err != nil { + return tagSettings{}, err + } + settings.ts = settings.ts.WithLengthPrefixType(value) + case "minLen": value, err := parseStructTagValueUint("minLen", keyValue, currentPart) if err != nil { @@ -877,27 +865,6 @@ func parseStructTag(tag string, serixPosition int) (tagSettings, error) { } settings.ts = settings.ts.WithMaxLen(value) - case "lengthPrefixType": - value, err := parseStructTagValuePrefixType("lengthPrefixType", keyValue, currentPart) - if err != nil { - return tagSettings{}, err - } - settings.ts = settings.ts.WithLengthPrefixType(value) - - case "mapMinEntries": - value, err := parseStructTagValueUint("mapMinEntries", keyValue, currentPart) - if err != nil { - return tagSettings{}, err - } - settings.ts = settings.ts.WithMapMinEntries(value) - - case "mapMaxEntries": - value, err := parseStructTagValueUint("mapMaxEntries", keyValue, currentPart) - if err != nil { - return tagSettings{}, err - } - settings.ts = settings.ts.WithMapMaxEntries(value) - case "mapMaxByteSize": value, err := parseStructTagValueUint("mapMaxByteSize", keyValue, currentPart) if err != nil { @@ -905,8 +872,8 @@ func parseStructTag(tag string, serixPosition int) (tagSettings, error) { } settings.ts = settings.ts.WithMapMaxByteSize(value) - case "mapKeyLengthPrefixType": - value, err := parseStructTagValuePrefixType("mapKeyLengthPrefixType", keyValue, currentPart) + case "mapKeyLenPrefix": + value, err := parseStructTagValuePrefixType("mapKeyLenPrefix", keyValue, currentPart) if err != nil { return tagSettings{}, err } @@ -926,8 +893,8 @@ func parseStructTag(tag string, serixPosition int) (tagSettings, error) { } settings.ts = settings.ts.WithMapKeyMaxLen(value) - case "mapValueLengthPrefixType": - value, err := parseStructTagValuePrefixType("mapValueLengthPrefixType", keyValue, currentPart) + case "mapValueLenPrefix": + value, err := parseStructTagValuePrefixType("mapValueLenPrefix", keyValue, currentPart) if err != nil { return tagSettings{}, err } @@ -950,6 +917,7 @@ func parseStructTag(tag string, serixPosition int) (tagSettings, error) { default: return tagSettings{}, ierrors.Errorf("unknown tag part: %s", currentPart) } + seenParts[partName] = struct{}{} } @@ -1032,6 +1000,18 @@ func getNumberTypeToConvert(kind reflect.Kind) (int, reflect.Type, reflect.Type) return bitSize, numberType, reflect.PointerTo(numberType) } -func fieldKeyString(str string) string { +// FieldKeyString converts the given string to camelCase. +// Special keywords like ID or URL are converted to only first letter upper case. +func FieldKeyString(str string) string { + for _, keyword := range []string{"ID", "URL"} { + if !strings.Contains(str, keyword) { + continue + } + + // first keyword letter upper case, rest lower case + str = strings.ReplaceAll(str, keyword, string(keyword[0])+strings.ToLower(keyword)[1:]) + } + + // first letter lower case return strings.ToLower(str[:1]) + str[1:] } diff --git a/serializer/serix/serix_serializable_test.go b/serializer/serix/serix_serializable_test.go index 82113b12..278149ee 100644 --- a/serializer/serix/serix_serializable_test.go +++ b/serializer/serix/serix_serializable_test.go @@ -82,8 +82,8 @@ func (bs Bools) FromSerializables(seris serializer.Serializables) { type SimpleStruct struct { Bool bool `serix:"bool"` Uint uint64 `serix:"uint"` - String string `serix:"string,lengthPrefixType=uint16"` - Bytes []byte `serix:"bytes,lengthPrefixType=uint32"` + String string `serix:"string,lenPrefix=uint16"` + Bytes []byte `serix:"bytes,lenPrefix=uint32"` BytesArray [16]byte `serix:"bytesarray"` BigInt *big.Int `serix:"bigint"` Time time.Time `serix:"time"` diff --git a/serializer/serix/serix_test.go b/serializer/serix/serix_test.go index 79fdaa14..858499e2 100644 --- a/serializer/serix/serix_test.go +++ b/serializer/serix/serix_test.go @@ -39,7 +39,7 @@ func TestMinMax(t *testing.T) { name: "ok - string in bounds", paras: func() paras { type example struct { - Str string `serix:"str,minLen=5,maxLen=10,lengthPrefixType=uint8"` + Str string `serix:"str,minLen=5,maxLen=10,lenPrefix=uint8"` } api := serix.NewAPI() @@ -57,7 +57,7 @@ func TestMinMax(t *testing.T) { name: "err - string out of bounds", paras: func() paras { type example struct { - Str string `serix:"str,minLen=5,maxLen=10,lengthPrefixType=uint8"` + Str string `serix:"str,minLen=5,maxLen=10,lenPrefix=uint8"` } api := serix.NewAPI() @@ -75,7 +75,7 @@ func TestMinMax(t *testing.T) { name: "ok - slice in bounds", paras: func() paras { type example struct { - Slice []byte `serix:"slice,minLen=0,maxLen=10,lengthPrefixType=uint8"` + Slice []byte `serix:"slice,minLen=0,maxLen=10,lenPrefix=uint8"` } api := serix.NewAPI() @@ -93,7 +93,7 @@ func TestMinMax(t *testing.T) { name: "err - slice out of bounds", paras: func() paras { type example struct { - Slice []byte `serix:"slice,minLen=0,maxLen=3,lengthPrefixType=uint8"` + Slice []byte `serix:"slice,minLen=0,maxLen=3,lenPrefix=uint8"` } api := serix.NewAPI() @@ -181,7 +181,7 @@ func TestSerixMapSerialize(t *testing.T) { type MyMapType map[string]string type MapStruct struct { - MyMap MyMapType `serix:"myMap,lengthPrefixType=uint8,mapMinEntries=2,mapMaxEntries=4,mapMaxByteSize=50,mapKeyLengthPrefixType=uint16,mapKeyMinLen=2,mapKeyMaxLen=5,mapValueLengthPrefixType=uint32,mapValueMinLen=1,mapValueMaxLen=6"` + MyMap MyMapType `serix:"myMap,lenPrefix=uint8,minLen=2,maxLen=4,mapMaxByteSize=50,mapKeyLenPrefix=uint16,mapKeyMinLen=2,mapKeyMaxLen=5,mapValueLenPrefix=uint32,mapValueMinLen=1,mapValueMaxLen=6"` } testAPI.RegisterTypeSettings(MapStruct{}, serix.TypeSettings{}) @@ -207,7 +207,7 @@ func TestSerixMapSerialize(t *testing.T) { }, target: &MapStruct{}, size: 0, - seriErr: serix.ErrMapValidationMinElementsNotReached, + seriErr: serializer.ErrArrayValidationMinElementsNotReached, }, { name: "fail - too many entries", @@ -222,7 +222,7 @@ func TestSerixMapSerialize(t *testing.T) { }, target: &MapStruct{}, size: 0, - seriErr: serix.ErrMapValidationMaxElementsExceeded, + seriErr: serializer.ErrArrayValidationMaxElementsExceeded, }, { name: "fail - too big", @@ -341,12 +341,12 @@ func TestSerixMapDeserialize(t *testing.T) { // used to create test data type TestVectorMapStruct struct { - MyMap MyMapType `serix:"myMap,lengthPrefixType=uint8,mapMinEntries=1,mapMaxEntries=5,mapMaxByteSize=100,mapKeyLengthPrefixType=uint16,mapKeyMinLen=1,mapKeyMaxLen=7,mapValueLengthPrefixType=uint32,mapValueMinLen=0,mapValueMaxLen=10"` + MyMap MyMapType `serix:"myMap,lenPrefix=uint8,minLen=1,maxLen=5,mapMaxByteSize=100,mapKeyLenPrefix=uint16,mapKeyMinLen=1,mapKeyMaxLen=7,mapValueLenPrefix=uint32,mapValueMinLen=0,mapValueMaxLen=10"` } testAPI.RegisterTypeSettings(TestVectorMapStruct{}, serix.TypeSettings{}) type MapStruct struct { - MyMap MyMapType `serix:"myMap,lengthPrefixType=uint8,mapMinEntries=2,mapMaxEntries=4,mapMaxByteSize=50,mapKeyLengthPrefixType=uint16,mapKeyMinLen=2,mapKeyMaxLen=5,mapValueLengthPrefixType=uint32,mapValueMinLen=1,mapValueMaxLen=6"` + MyMap MyMapType `serix:"myMap,lenPrefix=uint8,minLen=2,maxLen=4,mapMaxByteSize=50,mapKeyLenPrefix=uint16,mapKeyMinLen=2,mapKeyMaxLen=5,mapValueLenPrefix=uint32,mapValueMinLen=1,mapValueMaxLen=6"` } testAPI.RegisterTypeSettings(MapStruct{}, serix.TypeSettings{}) @@ -372,7 +372,7 @@ func TestSerixMapDeserialize(t *testing.T) { }, target: &MapStruct{}, size: 0, - deSeriErr: serix.ErrMapValidationMinElementsNotReached, + deSeriErr: serializer.ErrArrayValidationMinElementsNotReached, }, { name: "fail - too many entries", @@ -387,7 +387,7 @@ func TestSerixMapDeserialize(t *testing.T) { }, target: &MapStruct{}, size: 0, - deSeriErr: serix.ErrMapValidationMaxElementsExceeded, + deSeriErr: serializer.ErrArrayValidationMaxElementsExceeded, }, { name: "fail - too big", @@ -457,3 +457,75 @@ func TestSerixMapDeserialize(t *testing.T) { t.Run(tt.name, tt.run) } } + +func TestSerixFieldKeyString(t *testing.T) { + type test struct { + name string + source string + target string + } + + tests := []*test{ + { + name: "single char", + source: "A", + target: "a", + }, + { + name: "all upper case", + source: "MYTEST", + target: "mYTEST", + }, + { + name: "all lower case", + source: "mytest", + target: "mytest", + }, + { + name: "mixed case", + source: "MyTest", + target: "myTest", + }, + { + name: "mixed case with numbers", + source: "MyTest123", + target: "myTest123", + }, + { + name: "mixed case with numbers and underscore", + source: "MyTest_123", + target: "myTest_123", + }, + { + name: "mixed case with numbers and underscore and dash", + source: "MyTest_123-", + target: "myTest_123-", + }, + { + name: "mixed case with special keyword 'id'", + source: "MyTestID", + target: "myTestId", + }, + { + name: "mixed case with special keyword 'URL'", + source: "MyTestURL", + target: "myTestUrl", + }, + { + name: "mixed case with special keyword 'ID' but lowercase", + source: "MyTestid", + target: "myTestid", + }, + { + name: "mixed case with special keyword 'URL' but lowercase", + source: "MyTesturl", + target: "myTesturl", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.target, serix.FieldKeyString(tt.source)) + }) + } +} diff --git a/serializer/serix/type_settings.go b/serializer/serix/type_settings.go index 6a9c1092..90488b6d 100644 --- a/serializer/serix/type_settings.go +++ b/serializer/serix/type_settings.go @@ -59,10 +59,6 @@ func (m *MapElementRules) ToTypeSettings() TypeSettings { // MapRules defines rules around a to be deserialized map. type MapRules struct { - // MinEntries defines the min entries for the map. - MinEntries uint - // MaxEntries defines the max entries for the map. 0 means unbounded. - MaxEntries uint // MaxByteSize defines the max serialized byte size for the map. 0 means unbounded. MaxByteSize uint @@ -90,6 +86,31 @@ type TypeSettings struct { mapRules *MapRules } +// WithFieldKey specifies the key for the field. +func (ts TypeSettings) WithFieldKey(fieldKey string) TypeSettings { + ts.fieldKey = &fieldKey + + return ts +} + +// FieldKey returns the key for the field. +func (ts TypeSettings) FieldKey() (string, bool) { + if ts.fieldKey == nil { + return "", false + } + + return *ts.fieldKey, true +} + +// MustFieldKey must return a key for the field. +func (ts TypeSettings) MustFieldKey() string { + if ts.fieldKey == nil { + panic("no field key set") + } + + return *ts.fieldKey +} + // WithLengthPrefixType specifies LengthPrefixType. func (ts TypeSettings) WithLengthPrefixType(lpt LengthPrefixType) TypeSettings { ts.lengthPrefixType = &lpt @@ -137,31 +158,6 @@ func (ts TypeSettings) LexicalOrdering() (val bool, set bool) { return *ts.lexicalOrdering, true } -// WithFieldKey specifies the key for the field. -func (ts TypeSettings) WithFieldKey(fieldKey string) TypeSettings { - ts.fieldKey = &fieldKey - - return ts -} - -// FieldKey returns the key for the field. -func (ts TypeSettings) FieldKey() (string, bool) { - if ts.fieldKey == nil { - return "", false - } - - return *ts.fieldKey, true -} - -// MustFieldKey must return a key for the field. -func (ts TypeSettings) MustFieldKey() string { - if ts.fieldKey == nil { - panic("no field key set") - } - - return *ts.fieldKey -} - // WithArrayRules specifies serializer.ArrayRules. func (ts TypeSettings) WithArrayRules(rules *ArrayRules) TypeSettings { ts.arrayRules = rules @@ -238,26 +234,6 @@ func (ts TypeSettings) MapRules() *MapRules { return ts.mapRules } -// WithMapMinEntries specifies the min entries for the map. -func (ts TypeSettings) WithMapMinEntries(l uint) TypeSettings { - if ts.mapRules == nil { - ts.mapRules = new(MapRules) - } - ts.mapRules.MinEntries = l - - return ts -} - -// WithMapMaxEntries specifies the max entries for the map. -func (ts TypeSettings) WithMapMaxEntries(l uint) TypeSettings { - if ts.mapRules == nil { - ts.mapRules = new(MapRules) - } - ts.mapRules.MaxEntries = l - - return ts -} - // WithMapMaxByteSize specifies max serialized byte size for the map. 0 means unbounded. func (ts TypeSettings) WithMapMaxByteSize(l uint) TypeSettings { if ts.mapRules == nil {