Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(go): register exported properties as callbacks #4104

Merged
merged 3 commits into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/@jsii/go-runtime-test/project/callbacks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tests
import (
"testing"

"github.com/aws/jsii-runtime-go"
calc "github.com/aws/jsii/jsii-calc/go/jsiicalc/v3"
)

Expand All @@ -18,10 +19,27 @@ func TestPureInterfacesCanBeUsedTransparently(t *testing.T) {
}
}

func TestPropertyAccessThroughAny(t *testing.T) {
any := &ABC{
PropA: "Hello",
ProbB: "World",
}
calc.AnyPropertyAccess_MutateProperties(any, jsii.String("a"), jsii.String("b"), jsii.String("result"))
if *any.PropC != "Hello+World" {
t.Errorf("Expected Hello+World; actual %v", any.PropC)
}
}

type StructReturningDelegate struct {
expected *calc.StructB
}

func (o *StructReturningDelegate) ReturnStruct() *calc.StructB {
return o.expected
}

type ABC struct {
PropA string `json:"a"`
ProbB string `json:"b"`
PropC *string `json:"result,omitempty"`
}
106 changes: 102 additions & 4 deletions packages/@jsii/go-runtime/jsii-runtime-go/internal/kernel/callbacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package kernel
import (
"fmt"
"reflect"
"strings"

"github.com/aws/jsii-runtime-go/internal/api"
)
Expand Down Expand Up @@ -78,9 +79,45 @@ func (g *getCallback) handle(cookie string) (retval reflect.Value, err error) {
client := GetClient()

receiver := reflect.ValueOf(client.GetObject(g.ObjRef))
method := receiver.MethodByName(cookie)

return client.invoke(method, nil)
if strings.HasPrefix(cookie, ".") {
// Ready to catch an error if the access panics...
defer func() {
if r := recover(); r != nil {
if err == nil {
var ok bool
if err, ok = r.(error); !ok {
err = fmt.Errorf("%v", r)
}
} else {
// This is not expected - so we panic!
panic(r)
}
}
}()

// Need to access the underlying struct...
receiver = receiver.Elem()
retval = receiver.FieldByName(cookie[1:])

if retval.IsZero() {
// Omit zero-values if a json tag instructs so...
field, _ := receiver.Type().FieldByName(cookie[1:])
if tag := field.Tag.Get("json"); tag != "" {
for _, attr := range strings.Split(tag, ",")[1:] {
if attr == "omitempty" {
retval = reflect.ValueOf(nil)
break
}
}
}
}

return
} else {
method := receiver.MethodByName(cookie)
return client.invoke(method, nil)
}
}

type setCallback struct {
Expand All @@ -93,9 +130,70 @@ func (s *setCallback) handle(cookie string) (retval reflect.Value, err error) {
client := GetClient()

receiver := reflect.ValueOf(client.GetObject(s.ObjRef))
method := receiver.MethodByName(fmt.Sprintf("Set%v", cookie))
if strings.HasPrefix(cookie, ".") {
// Ready to catch an error if the access panics...
defer func() {
if r := recover(); r != nil {
if err == nil {
var ok bool
if err, ok = r.(error); !ok {
err = fmt.Errorf("%v", r)
}
} else {
// This is not expected - so we panic!
panic(r)
}
}
}()

// Need to access the underlying struct...
receiver = receiver.Elem()
field := receiver.FieldByName(cookie[1:])
meta, _ := receiver.Type().FieldByName(cookie[1:])

field.Set(convert(reflect.ValueOf(s.Value), meta.Type))
// Both retval & err are set to zero values here...
return
} else {
method := receiver.MethodByName(fmt.Sprintf("Set%v", cookie))
return client.invoke(method, []interface{}{s.Value})
}
}

func convert(value reflect.Value, typ reflect.Type) reflect.Value {
retry:
vt := value.Type()

if vt.AssignableTo(typ) {
return value
}
if value.CanConvert(typ) {
return value.Convert(typ)
}

if typ.Kind() == reflect.Ptr {
switch value.Kind() {
case reflect.String:
str := value.String()
value = reflect.ValueOf(&str)
case reflect.Bool:
bool := value.Bool()
value = reflect.ValueOf(&bool)
case reflect.Int:
int := value.Int()
value = reflect.ValueOf(&int)
case reflect.Float64:
float := value.Float()
value = reflect.ValueOf(&float)
default:
iface := value.Interface()
value = reflect.ValueOf(&iface)
}
goto retry
}

return client.invoke(method, []interface{}{s.Value})
// Unsure what to do... let default behavior happen...
return value
}

func (c *Client) invoke(method reflect.Value, args []interface{}) (retval reflect.Value, err error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package kernel

import (
"fmt"
"reflect"
"strings"

"github.com/aws/jsii-runtime-go/internal/api"
)
Expand All @@ -18,6 +20,14 @@ func (c *Client) ManageObject(v reflect.Value) (ref api.ObjectRef, err error) {
}
interfaces, overrides := c.Types().DiscoverImplementation(vt)

found := make(map[string]bool)
for _, override := range overrides {
if prop, ok := override.(*api.PropertyOverride); ok {
found[prop.JsiiProperty] = true
}
}
overrides = appendExportedProperties(vt, overrides, found)

var resp CreateResponse
resp, err = c.Create(CreateProps{
FQN: objectFQN,
Expand All @@ -33,3 +43,49 @@ func (c *Client) ManageObject(v reflect.Value) (ref api.ObjectRef, err error) {

return
}

func appendExportedProperties(vt reflect.Type, overrides []api.Override, found map[string]bool) []api.Override {
if vt.Kind() == reflect.Ptr {
vt = vt.Elem()
}

if vt.Kind() == reflect.Struct {
for idx := 0; idx < vt.NumField(); idx++ {
field := vt.Field(idx)
// Unexported fields are not relevant here...
if !field.IsExported() {
continue
}

// Anonymous fields are embed, we traverse them for fields, too...
if field.Anonymous {
overrides = appendExportedProperties(field.Type, overrides, found)
continue
}

jsonName := field.Tag.Get("json")
if jsonName == "-" {
// Explicit omit via `json:"-"`
continue
} else if jsonName != "" {
// There could be attributes after the field name (e.g. `json:"foo,omitempty"`)
jsonName = strings.Split(jsonName, ",")[0]
}
// The default behavior is to use the field name as-is in JSON.
if jsonName == "" {
jsonName = field.Name
}

if !found[jsonName] {
overrides = append(overrides, &api.PropertyOverride{
JsiiProperty: jsonName,
// Using the "." prefix to signify this isn't actually a getter, just raw field access.
GoGetter: fmt.Sprintf(".%s", field.Name),
})
found[jsonName] = true
}
}
}

return overrides
}
21 changes: 21 additions & 0 deletions packages/jsii-calc/lib/compliance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3141,3 +3141,24 @@ export class PromiseNothing {
return PromiseNothing.promiseIt();
}
}

export class AnyPropertyAccess {
/**
* Sets obj[resultProp] to `${obj[propA]}+${obj[propB]}`.
*
* @param obj the receiver object.
* @param propA the first property to read.
* @param propB the second property to read.
* @param resultProp the property to write into.
*/
public static mutateProperties(
obj: any,
propA: string,
propB: string,
resultProp: string,
) {
obj[resultProp] = `${obj[propA]}+${obj[propB]}`;
}

private constructor() {}
}
68 changes: 67 additions & 1 deletion packages/jsii-calc/test/assembly.jsii
Original file line number Diff line number Diff line change
Expand Up @@ -1538,6 +1538,72 @@
"name": "AnonymousImplementationProvider",
"symbolId": "lib/compliance:AnonymousImplementationProvider"
},
"jsii-calc.AnyPropertyAccess": {
"assembly": "jsii-calc",
"docs": {
"stability": "stable"
},
"fqn": "jsii-calc.AnyPropertyAccess",
"kind": "class",
"locationInModule": {
"filename": "lib/compliance.ts",
"line": 3145
},
"methods": [
{
"docs": {
"stability": "stable",
"summary": "Sets obj[resultProp] to `${obj[propA]}+${obj[propB]}`."
},
"locationInModule": {
"filename": "lib/compliance.ts",
"line": 3154
},
"name": "mutateProperties",
"parameters": [
{
"docs": {
"summary": "the receiver object."
},
"name": "obj",
"type": {
"primitive": "any"
}
},
{
"docs": {
"summary": "the first property to read."
},
"name": "propA",
"type": {
"primitive": "string"
}
},
{
"docs": {
"summary": "the second property to read."
},
"name": "propB",
"type": {
"primitive": "string"
}
},
{
"docs": {
"summary": "the property to write into."
},
"name": "resultProp",
"type": {
"primitive": "string"
}
}
],
"static": true
}
],
"name": "AnyPropertyAccess",
"symbolId": "lib/compliance:AnyPropertyAccess"
},
"jsii-calc.AsyncVirtualMethods": {
"assembly": "jsii-calc",
"docs": {
Expand Down Expand Up @@ -18843,5 +18909,5 @@
}
},
"version": "3.20.120",
"fingerprint": "EH7xszNdCh9PCFUZ8Foi7g2CPhdrKeZm8CQaUCNv4GQ="
"fingerprint": "kOCIHox3N0mzJsbC3zUF0dELGRsdq5jP57dDIOu3fDE="
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading