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

Better typing for Go SDK #248

Closed
johanbrandhorst opened this issue Jun 19, 2018 · 5 comments · Fixed by pulumi/pulumi#3506
Closed

Better typing for Go SDK #248

johanbrandhorst opened this issue Jun 19, 2018 · 5 comments · Fixed by pulumi/pulumi#3506
Assignees
Labels
area/languages kind/enhancement Improvements or new features
Milestone

Comments

@johanbrandhorst
Copy link

Hi!

I'm really excited by this project, but I was immediately disappointed by the SDK types. Here's the first one I found:

type GetAmiArgs struct {
	// Limit search to users with *explicit* launch permission on
	// the image. Valid items are the numeric account ID or `self`.
	ExecutableUsers interface{}
	// One or more name/value pairs to filter off of. There are
	// several valid keys, for a full reference, check out
	// [describe-images in the AWS CLI reference][1].
	Filters interface{}
	// If more than one result is returned, use the most
	// recent AMI.
	MostRecent interface{}
	// A regex string to apply to the AMI list returned
	// by AWS. This allows more advanced filtering not supported from the AWS API. This
	// filtering is done locally on what AWS returns, and could have a performance
	// impact if the result is large. It is recommended to combine this with other
	// options to narrow down the list AWS returns.
	NameRegex interface{}
	// Limit search to specific AMI owners. Valid items are the numeric
	// account ID, `amazon`, or `self`.
	Owners interface{}
	Tags interface{}
}

I understand this was probably a compromise to get things ready for GA, but I'd like this issue to track the work on typing this stuff up, otherwise it's little better than the JS SDK (:scream:).

Thanks

@joeduffy joeduffy added kind/enhancement Improvements or new features area/languages labels Jun 20, 2018
@joeduffy
Copy link
Member

@johanbrandhorst thank you for filing this. As the guy who put this together, I can't agree more.

One of the challenges we have, which I would love feedback on since I ran out of ideas, is that it's tough to type these things. I'm not sure if you noticed all the specialized *XOutput types, which more strongly types resource properties after being provisioned. The challenge we have is that we want to permit, say, string and *StringOutput for string-based input properties. In TypeScript we use union types. In Go, I'd have liked to have an idiomatic discriminated union that can accept either, but it wasn't clear the best way to do this that enabled idiomatic calling code (without casts).

/cc @pgavlin also, since he is our resident Go idioms guy.

@lukehoban
Copy link
Member

lukehoban commented Jun 20, 2018

I wonder if we should consider functions like pulumi.String(str) which return a StringOutput and then have all these inputs accept StringOutput. (The name of StringOutput may need to change for this to feel right). These "Output"s wouldn't carry any dependencies.

This would be a lot like aws.String and friends in the AWS SDK as a way to lift values into the reference types need in that API. In our case, we would require lifting everything manually into the Output space.

It would be rather verbose, but in a way that is fairly idiomatic for Go.

@johanbrandhorst
Copy link
Author

@joeduffy @lukehoban thanks for your replies! I'm personally not a huge fan of the stuff the AWS SDK does (afaik it's just a helper for creating a pointer to a string literal). For discriminated unions I think the best you can do at the moment is using an unexported interface like the Go protobuf implementation:

type stringOption interface {
    func isStringOption()
}

type StringOutput struct {
    ...
}

func ( _ StringOutput) isStringOption() {}

Unfortunately this does not work with native strings of course... so you'd still need a pulumi.String(in string) stringOption. I suppose then you could have pulumi.CustomStringType.StringOutput() StringOutput for converting between output and inputs. That might be the best option after all.

@swgillespie
Copy link
Contributor

Go support is out of scope for M21.

@swgillespie swgillespie modified the milestones: 0.21, 0.22 Feb 1, 2019
@swgillespie swgillespie removed this from the 0.22 milestone Mar 18, 2019
@joeduffy joeduffy assigned ellismg and unassigned swgillespie May 28, 2019
@pgavlin pgavlin assigned pgavlin and unassigned ellismg Nov 11, 2019
@pgavlin pgavlin added this to the 0.29 milestone Nov 11, 2019
@lukehoban lukehoban modified the milestones: 0.29, 0.30 Nov 22, 2019
@lukehoban lukehoban modified the milestones: 0.30, 0.31 Jan 2, 2020
@lukehoban
Copy link
Member

Part of pulumi/pulumi#3506?

pgavlin added a commit to pulumi/pulumi that referenced this issue Jan 18, 2020
The redesign is focused around providing better static typings and
improved ease-of-use for the Go SDK. Most of the redesign revolves
around three pivots:
- Strongly-typed inputs, especially for nested types
- Struct-based resource and invoke APIs
- Ease-of-use of Apply

1. Strongly-typed inputs

Input is the type of a generic input value for a Pulumi resource.
This type is used in conjunction with Output to provide polymorphism
over strongly-typed input values.

The intended pattern for nested Pulumi value types is to define an
input interface and a plain, input, and output variant of the value
type that implement the input interface.

For example, given a nested Pulumi value type with the following shape:

```
type Nested struct {
    Foo int
    Bar string
}
```

We would define the following:

```
var nestedType = reflect.TypeOf((*Nested)(nil)).Elem()

type NestedInput interface {
    pulumi.Input

    ToNestedOutput() NestedOutput
    ToNestedOutputWithContext(context.Context) NestedOutput
}

type Nested struct {
    Foo int `pulumi:"foo"`
    Bar string `pulumi:"bar"`
}

type NestedInputValue struct {
    Foo pulumi.IntInput `pulumi:"foo"`
    Bar pulumi.StringInput `pulumi:"bar"`
}

func (NestedInputValue) ElementType() reflect.Type {
    return nestedType
}

func (v NestedInputValue) ToNestedOutput() NestedOutput {
    return pulumi.ToOutput(v).(NestedOutput)
}

func (v NestedInputValue) ToNestedOutputWithContext(ctx context.Context) NestedOutput {
    return pulumi.ToOutputWithContext(ctx, v).(NestedOutput)
}

type NestedOutput struct { *pulumi.OutputState }

func (NestedOutput) ElementType() reflect.Type {
    return nestedType
}

func (o NestedOutput) ToNestedOutput() NestedOutput {
    return o
}

func (o NestedOutput) ToNestedOutputWithContext(ctx context.Context) NestedOutput {
    return o
}

func (o NestedOutput) Foo() pulumi.IntOutput {
    return o.Apply(func (v Nested) int {
        return v.Foo
    }).(pulumi.IntOutput)
}

func (o NestedOutput) Bar() pulumi.StringOutput {
    return o.Apply(func (v Nested) string {
        return v.Bar
    }).(pulumi.StringOutput)
}
```

The SDK provides input and output types for primitives, arrays, and
maps.

2. Struct-based APIs

Instead of providing expected output properties in the input map passed
to {Read,Register}Resource and returning the outputs as a map, the user
now passes a pointer to a struct that implements one of the Resource
interfaces and has appropriately typed and tagged fields that represent
its output properties.

For example, given a custom resource with an int-typed output "foo" and
a string-typed output "bar", we would define the following
CustomResource type:

```
type MyResource struct {
    pulumi.CustomResourceState

    Foo pulumi.IntOutput    `pulumi:"foo"`
    Bar pulumi.StringOutput `pulumi:"bar"`
}
```

And invoke RegisterResource like so:

```
var resource MyResource
err := ctx.RegisterResource(tok, name, props, &resource, opts...)
```

Invoke arguments and results are also provided via structs, but use
plain-old Go types for their fields:

```
type MyInvokeArgs struct {
    Foo int `pulumi:"foo"`
}

type MyInvokeResult struct {
    Bar string `pulumi:"bar"`
}

var result MyInvokeResult
err := ctx.Invoke(tok, MyInvokeArgs{Foo: 42}, &result, opts...)
```

3. Ease-of-use of Apply

All `Apply` methods now accept an interface{} as the callback type.
The provided callback value must have one of the following signatures:

	func (v T) U
	func (v T) (U, error)
	func (ctx context.Context, v T) U
	func (ctx context.Context, v T) (U, error)

T must be assignable from the ElementType of the Output. If U is a type
that has a registered Output type, the result of the Apply will be the
corresponding Output type. Otherwise, the result of the Apply will be
AnyOutput.

Fixes #2149.
Fixes #3488.
Fixes #3487.
Fixes pulumi/pulumi-aws#248.
Fixes #3492.
Fixes #3491.
Fixes #3562.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/languages kind/enhancement Improvements or new features
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants