Skip to content

Commit

Permalink
ApplyT: Coerce new type wrappers
Browse files Browse the repository at this point in the history
Adds support to ApplyT to automatically coerce new type wrappers
when calling the provided functions.

With this change, the following is valid:

    var idout pu.IDOutput = ...
    idout.ApplyT(func(id string) string {
        // ...
    })

Note that this does not use `To{Type}Output` methods at this time.
We will likely want to add that in the future given #11750.
We can do that in a backwards compatible way.

This coerces only those values that are defined
with language-level type wrappers in the following form:

    type ID string

This *does not* coerce types with different undedrlying representations.
Specifically, the following conversons are not supported
even though they're supported by Go using the `T(v)` syntax.

    string => int
    string => []byte
    []byte => string

The choice to convert between these types
should be made explicitly by the user
so that they get the semantics they want.

Resolves #11784
  • Loading branch information
abhinav committed Jan 18, 2023
1 parent db199ea commit d0aac86
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 1 deletion.
@@ -0,0 +1,4 @@
changes:
- type: feat
scope: sdk/go
description: Coerces output values in ApplyT calls if the types are equivalent.
19 changes: 18 additions & 1 deletion sdk/go/pulumi/types.go
Expand Up @@ -380,6 +380,9 @@ type applier struct {
fn reflect.Value
ctx bool // whether fn accepts a context as its first input
err bool // whether fn return an err as its last result

// This is non-nil if the input value should be converted first.
convertTo reflect.Type
}

func newApplier(fn interface{}, elemType reflect.Type) (_ *applier, err error) {
Expand Down Expand Up @@ -426,7 +429,18 @@ func newApplier(fn interface{}, elemType reflect.Type) (_ *applier, err error) {
elemName = "second"
fallthrough // validate element type
case 1:
if t := ft.In(elemIdx); !elemType.AssignableTo(t) {
switch t := ft.In(elemIdx); {
case elemType.AssignableTo(t):
// Do nothing.
case elemType.ConvertibleTo(t) && elemType.Kind() == t.Kind():
// Support coercion if the types are the same kind.
// Types with different internal representations
// do not coerce for "free"
// (e.g. string([]byte{..}) allocates)
// and may not match user expectations
// (e.g. string(42) is "*", not "42").
ap.convertTo = t
default:
return nil, fmt.Errorf("applier's %s input parameter must be assignable from %v, got %v", elemName, elemType, t)
}
default:
Expand Down Expand Up @@ -459,6 +473,9 @@ func (ap *applier) Call(ctx context.Context, in reflect.Value) (reflect.Value, e
if ap.ctx {
args = append(args, reflect.ValueOf(ctx))
}
if ap.convertTo != nil {
in = in.Convert(ap.convertTo)
}
args = append(args, in)

var (
Expand Down
61 changes: 61 additions & 0 deletions sdk/go/pulumi/types_test.go
Expand Up @@ -1294,3 +1294,64 @@ func TestNewApplier_errors(t *testing.T) {
})
}
}

func TestApplyTCoerce(t *testing.T) {
t.Parallel()

t.Run("ID-string", func(t *testing.T) {
t.Parallel()

o := ID("hello").ToIDOutput()
assertApplied(t, o.ApplyT(func(s string) (interface{}, error) {
assert.Equal(t, "hello", s)
return nil, nil
}))
})

t.Run("string-ID", func(t *testing.T) {
t.Parallel()

o := String("world").ToStringOutput()
assertApplied(t, o.ApplyT(func(s string) (interface{}, error) {
assert.Equal(t, "world", s)
return nil, nil
}))
})

t.Run("custom", func(t *testing.T) {
t.Parallel()

type Foo struct{ v int }
type Bar Foo

type FooOutput struct{ *OutputState }

o := FooOutput{newOutputState(nil, reflect.TypeOf(Foo{}))}
go o.resolve(Foo{v: 42}, true, false, nil)

assertApplied(t, o.ApplyT(func(b Bar) (interface{}, error) {
assert.Equal(t, 42, b.v)
return nil, nil
}))
})
}

// Verifies that ApplyT does not allow applierse where the conversion
// would change the undedrlying representation.
func TestApplyTCoerceRejectDifferentKinds(t *testing.T) {
t.Parallel()

assert.Panics(t, func() {
String("foo").ToStringOutput().ApplyT(func([]byte) int {
t.Error("Should not be called")
return 42
})
}, "string-[]byte should not be allowed")

assert.Panics(t, func() {
Int(42).ToIntOutput().ApplyT(func(string) int {
t.Error("Should not be called")
return 42
})
}, "int-string should not be allowed")
}

0 comments on commit d0aac86

Please sign in to comment.