diff --git a/ucl/objs.go b/ucl/objs.go index 804ff71..b82bb42 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -110,6 +110,8 @@ func (b boolObject) Truthy() bool { func toGoValue(obj object) (interface{}, bool) { switch v := obj.(type) { + case OpaqueObject: + return v.v, true case nil: return nil, true case strObject: @@ -149,6 +151,8 @@ func toGoValue(obj object) (interface{}, bool) { func fromGoValue(v any) (object, error) { switch t := v.(type) { + case OpaqueObject: + return t, nil case nil: return nil, nil case string: @@ -476,6 +480,22 @@ func (s structProxyObject) Each(fn func(k string, v object) error) error { return nil } +type OpaqueObject struct { + v any +} + +func Opaque(v any) OpaqueObject { + return OpaqueObject{v: v} +} + +func (p OpaqueObject) String() string { + return fmt.Sprintf("opaque{%T}", p.v) +} + +func (p OpaqueObject) Truthy() bool { + return p.v != nil +} + type errBreak struct { isCont bool ret object diff --git a/ucl/userbuiltin.go b/ucl/userbuiltin.go index 625bea8..b7480dc 100644 --- a/ucl/userbuiltin.go +++ b/ucl/userbuiltin.go @@ -121,6 +121,23 @@ func (ca CallArgs) bindArg(v interface{}, arg object) error { } switch t := arg.(type) { + case OpaqueObject: + if v == nil { + return errors.New("opaque object not bindable to nil") + } + + vr := reflect.ValueOf(v) + tr := reflect.ValueOf(t.v) + if vr.Kind() != reflect.Pointer { + return errors.New("expected pointer for an opaque object bind") + } + + if !tr.Type().AssignableTo(vr.Elem().Type()) { + return errors.New("opaque object not assignable to passed in value") + } + + vr.Elem().Set(tr) + return nil case proxyObject: return bindProxyObject(v, reflect.ValueOf(t.p)) case listableProxyObject: @@ -142,6 +159,18 @@ func canBindArg(v interface{}, arg object) bool { } switch t := arg.(type) { + case OpaqueObject: + vr := reflect.ValueOf(v) + tr := reflect.ValueOf(t.v) + if vr.Kind() != reflect.Pointer { + return false + } + + if !tr.Type().AssignableTo(vr.Elem().Type()) { + return false + } + + return true case proxyObject: return canBindProxyObject(v, reflect.ValueOf(t.p)) case listableProxyObject: diff --git a/ucl/userbuiltin_test.go b/ucl/userbuiltin_test.go index a4bd231..eea982b 100644 --- a/ucl/userbuiltin_test.go +++ b/ucl/userbuiltin_test.go @@ -183,6 +183,105 @@ func TestInst_SetBuiltin(t *testing.T) { }) } }) + + t.Run("opaques returned as is", func(t *testing.T) { + type opaqueThingType struct { + x string + y string + z string + } + opaqueThing := &opaqueThingType{x: "do", y: "not", z: "touch"} + + tests := []struct { + descr string + expr string + wantErr bool + }{ + {descr: "return as is", expr: `getOpaque`, wantErr: false}, + {descr: "carry around ok", expr: `set x (getOpaque) ; $x`, wantErr: false}, + {descr: "iterate over", expr: `foreach (countTo3) { |x| echo $x }`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + + outW := bytes.NewBuffer(nil) + inst := ucl.New(ucl.WithOut(outW)) + + inst.SetBuiltin("getOpaque", func(ctx context.Context, args ucl.CallArgs) (any, error) { + return ucl.Opaque(opaqueThing), nil + }) + + res, err := inst.Eval(context.Background(), tt.expr) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Same(t, opaqueThing, res) + } + }) + } + }) + + t.Run("operate on opaques", func(t *testing.T) { + type opaqueThingType struct { + x string + y string + z string + } + opaqueThing := &opaqueThingType{x: "do", y: "not", z: "touch"} + + tests := []struct { + descr string + expr string + want opaqueThingType + }{ + {descr: "return as is", expr: `getOpaque`, want: *opaqueThing}, + {descr: "update pointer 1", expr: `set x (getOpaque) ; setProp $x -x "do" -y "touch" -z "this"`, want: opaqueThingType{x: "do", y: "touch", z: "this"}}, + {descr: "update pointer 2", expr: `set x (getOpaque) ; setProp $x -x "yes" ; setProp $x -y "this" -z "too"`, want: opaqueThingType{x: "yes", y: "this", z: "too"}}, + } + + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + + outW := bytes.NewBuffer(nil) + inst := ucl.New(ucl.WithOut(outW)) + + inst.SetBuiltin("getOpaque", func(ctx context.Context, args ucl.CallArgs) (any, error) { + return ucl.Opaque(opaqueThing), nil + }) + inst.SetBuiltin("setProp", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var o *opaqueThingType + + if err := args.Bind(&o); err != nil { + return nil, err + } + + if args.HasSwitch("x") { + var s string + _ = args.BindSwitch("x", &s) + o.x = s + } + if args.HasSwitch("y") { + var s string + _ = args.BindSwitch("y", &s) + o.y = s + } + if args.HasSwitch("z") { + var s string + _ = args.BindSwitch("z", &s) + o.z = s + } + + return nil, nil + }) + + _, err := inst.Eval(context.Background(), tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, *opaqueThing) + }) + } + }) } func TestCallArgs_Bind(t *testing.T) {