aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2025-10-30 11:55:19 -0700
committerRose Hogenson <rosehogenson@posteo.net>2025-10-30 11:55:19 -0700
commit3888d15001e4f6ee8319874184b78c072fca577e (patch)
tree56abc897f9c6ada68e68b484d74a4880361aaa94
parentb1e37119da439b52a2c1f1aca4df2fdc80462f97 (diff)
downloadccl-3888d15001e4f6ee8319874184b78c072fca577e.tar.zst
Allow all values to be pointers
This solves the issue of whether the field was populated or not, at the expense of some horrible memory patterns. But on the plus side it doesn't require any extra API so we might as well allow it.
-rw-r--r--ccl.go27
-rw-r--r--ccl_test.go19
2 files changed, 37 insertions, 9 deletions
diff --git a/ccl.go b/ccl.go
index 521fbba..fe67a72 100644
--- a/ccl.go
+++ b/ccl.go
@@ -592,6 +592,16 @@ func (p *parser) parse() (map[string]any, error) {
}
}
+func setPtr(val reflect.Value) reflect.Value {
+ if val.Kind() != reflect.Pointer {
+ return val
+ }
+ if val.IsNil() {
+ val.Set(reflect.New(val.Type().Elem()))
+ }
+ return val.Elem()
+}
+
func intLimits(kind reflect.Kind) (min, max uint64, ok bool) {
switch kind {
case reflect.Int:
@@ -622,6 +632,7 @@ func intLimits(kind reflect.Kind) (min, max uint64, ok bool) {
func unpackVal(fieldVal reflect.Value, fieldMap map[structField]int, val any, field string) error {
switch val := val.(type) {
case bool:
+ fieldVal := setPtr(fieldVal)
switch fieldVal.Kind() {
case reflect.Bool:
fieldVal.SetBool(val)
@@ -641,6 +652,7 @@ func unpackVal(fieldVal reflect.Value, fieldMap map[structField]int, val any, fi
return fmt.Errorf("field %q should have type bool", field)
}
case *integer:
+ fieldVal := setPtr(fieldVal)
switch fieldVal.Kind() {
case reflect.Float32, reflect.Float64:
fieldVal.SetFloat(float64(val.sgn) * float64(val.n))
@@ -659,6 +671,7 @@ func unpackVal(fieldVal reflect.Value, fieldMap map[structField]int, val any, fi
fieldVal.SetInt(int64(val.sgn) * int64(val.n))
}
case float64:
+ fieldVal := setPtr(fieldVal)
switch fieldVal.Kind() {
case reflect.Float32, reflect.Float64:
fieldVal.SetFloat(float64(val))
@@ -675,10 +688,11 @@ func unpackVal(fieldVal reflect.Value, fieldMap map[structField]int, val any, fi
if unmarshaler, ok := fieldVal.Addr().Interface().(encoding.TextUnmarshaler); ok {
return unmarshaler.UnmarshalText([]byte(val))
}
+ fieldVal := setPtr(fieldVal)
switch {
case fieldVal.Kind() == reflect.String:
fieldVal.SetString(val)
- case fieldVal.Kind() == reflect.Slice && fieldVal.Type().Elem() == reflect.TypeFor[byte]():
+ case fieldVal.Type() == reflect.TypeFor[[]byte]():
b, err := base64.StdEncoding.DecodeString(val)
if err != nil {
return fmt.Errorf("field %q: bad base64", field)
@@ -688,15 +702,10 @@ func unpackVal(fieldVal reflect.Value, fieldMap map[structField]int, val any, fi
return fmt.Errorf("field %q should have type string (got %s)", field, fieldVal.Type())
}
case map[string]any:
- if !(fieldVal.Kind() == reflect.Struct || fieldVal.Kind() == reflect.Pointer && fieldVal.Type().Elem().Kind() == reflect.Struct) {
+ fieldVal := setPtr(fieldVal)
+ if fieldVal.Kind() != reflect.Struct {
return fmt.Errorf("field %q should have type struct (got %s)", field, fieldVal.Type())
}
- if fieldVal.Kind() == reflect.Pointer {
- if fieldVal.IsNil() {
- fieldVal.Set(reflect.New(fieldVal.Type().Elem()))
- }
- fieldVal = fieldVal.Elem()
- }
if err := unpackStruct(fieldVal, fieldMap, val); err != nil {
return err
}
@@ -715,7 +724,7 @@ func unpackStruct(out reflect.Value, fieldMap map[structField]int, msg map[strin
return fmt.Errorf("no field named %q", field)
}
fieldVal := out.Field(fieldIdx)
- if fieldVal.Kind() == reflect.Slice && fieldVal.Type().Elem() != reflect.TypeFor[byte]() {
+ if fieldVal.Kind() == reflect.Slice && fieldVal.Type() != reflect.TypeFor[[]byte]() {
var vals []any
if l, ok := val.([]any); ok {
vals = l
diff --git a/ccl_test.go b/ccl_test.go
index 938af09..ab4833f 100644
--- a/ccl_test.go
+++ b/ccl_test.go
@@ -11,6 +11,7 @@ import (
func TestUnmarshal(t *testing.T) {
t.Parallel()
+ type byteSliceWrapper []byte
type nestedMessage struct {
Field int64 `ccl:"field"`
}
@@ -34,8 +35,11 @@ func TestUnmarshal(t *testing.T) {
Repeated []int64 `ccl:"repeated"`
RepeatedMessage []*nestedMessage `ccl:"repeated_message"`
Bytes []byte `ccl:"bytes"`
+ BytesWrapper byteSliceWrapper `ccl:"bytes_wrapper"`
Time time.Time `ccl:"time"`
TimePointer *time.Time `ccl:"time_pointer"`
+ IntPointer *int `ccl:"int_pointer"`
+ RepeatedPointer []*int `ccl:"repeated_pointer"`
Ignore map[int]int `ccl:"-,"` // unlike JSON this also means ignore
unexported int64
@@ -330,6 +334,10 @@ from string'`,
msg: `bytes:"dGVzdA=="`,
want: message{Bytes: []byte("test")},
}, {
+ desc: "NotBase64",
+ msg: `bytes_wrapper: [1, 2, 3]`,
+ want: message{BytesWrapper: byteSliceWrapper{1, 2, 3}},
+ }, {
desc: "TextUnmarshaler",
msg: `time:"2025-10-28T07:41:47Z"`,
want: message{Time: time.Date(2025, time.October, 28, 7, 41, 47, 0, time.UTC)},
@@ -337,6 +345,14 @@ from string'`,
desc: "TextUnmarshalerPointer",
msg: `time_pointer:"2025-10-28T07:41:47Z"`,
want: message{TimePointer: &[]time.Time{time.Date(2025, time.October, 28, 7, 41, 47, 0, time.UTC)}[0]},
+ }, {
+ desc: "IntPointer",
+ msg: `int_pointer: 5`,
+ want: message{IntPointer: &[]int{5}[0]},
+ }, {
+ desc: "RepeatedPointer",
+ msg: `repeated_pointer: [1, 2, 3]`,
+ want: message{RepeatedPointer: []*int{&[]int{1}[0], &[]int{2}[0], &[]int{3}[0]}},
}} {
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
@@ -455,6 +471,9 @@ func TestUnmarshal_Invalid(t *testing.T) {
desc: "Base64",
msg: `bytes:"dGVzdAo"`,
}, {
+ desc: "NotBase64",
+ msg: `bytes:[1,2,3]`,
+ }, {
desc: "BadField",
msg: `asdfasdfasdf:"asdf"`,
}, {