diff --git a/examples/file/main.go b/examples/file/main.go index 8704a1c9..7627554e 100644 --- a/examples/file/main.go +++ b/examples/file/main.go @@ -46,9 +46,9 @@ func (f *File) Annotate(a infer.Annotator) { } type FileArgs struct { - Path string `pulumi:"path,optional"` - Force bool `pulumi:"force,optional"` - Content string `pulumi:"content"` + Path infer.Output[string] `pulumi:"path,optional"` + Force infer.Output[bool] `pulumi:"force,optional"` + Content infer.Output[string] `pulumi:"content"` } func (f *FileArgs) Annotate(a infer.Annotator) { @@ -58,9 +58,9 @@ func (f *FileArgs) Annotate(a infer.Annotator) { } type FileState struct { - Path string `pulumi:"path"` - Force bool `pulumi:"force"` - Content string `pulumi:"content"` + Path infer.Output[string] `pulumi:"path,optional"` + Force infer.Output[bool] `pulumi:"force,optional"` + Content infer.Output[string] `pulumi:"content"` } func (f *FileState) Annotate(a infer.Annotator) { @@ -70,30 +70,40 @@ func (f *FileState) Annotate(a infer.Annotator) { } func (*File) Create(ctx p.Context, name string, input FileArgs, preview bool) (id string, output FileState, err error) { - if !input.Force { - _, err := os.Stat(input.Path) - if !os.IsNotExist(err) { - return "", FileState{}, fmt.Errorf("file already exists; pass force=true to override") + err = infer.Apply2Err(input.Path, input.Force, func(path string, force bool) (string, error) { + if force { + return "", nil } + _, err := os.Stat(path) + if os.IsNotExist(err) { + return "", nil + } else if err != nil { + return "", fmt.Errorf("discovering if %s exists: %w", path, err) + } + return "", fmt.Errorf("file already exists at %s; pass `force: true` to override", path) + }).Anchor() + if err != nil { + return "", FileState{}, err } - if preview { // Don't do the actual creating if in preview - return input.Path, FileState{}, nil + path, err := input.Path.GetMaybeUnknown() + if preview || err != nil { // Don't do the actual creating if in preview + return path, FileState{}, err } - f, err := os.Create(input.Path) + f, err := os.Create(input.Path.MustGetKnown()) if err != nil { return "", FileState{}, err } defer f.Close() - n, err := f.WriteString(input.Content) + n, err := f.WriteString(input.Content.MustGetKnown()) if err != nil { return "", FileState{}, err } - if n != len(input.Content) { - return "", FileState{}, fmt.Errorf("only wrote %d/%d bytes", n, len(input.Content)) + if n != len(input.Content.MustGetKnown()) { + return "", FileState{}, fmt.Errorf("only wrote %d/%d bytes", n, len(input.Content.MustGetKnown())) } - return input.Path, FileState{ + return input.Path.MustGetKnown(), FileState{ Path: input.Path, Force: input.Force, Content: input.Content, @@ -101,9 +111,13 @@ func (*File) Create(ctx p.Context, name string, input FileArgs, preview bool) (i } func (*File) Delete(ctx p.Context, id string, props FileState) error { - err := os.Remove(props.Path) + path, err := props.Path.GetKnown() + if err != nil { + return err + } + err = os.Remove(path) if os.IsNotExist(err) { - ctx.Logf(diag.Warning, "file %q already deleted", props.Path) + ctx.Logf(diag.Warning, "file %q already deleted", path) err = nil } return err @@ -117,38 +131,52 @@ func (*File) Check(ctx p.Context, name string, oldInputs, newInputs resource.Pro } func (*File) Update(ctx p.Context, id string, olds FileState, news FileArgs, preview bool) (FileState, error) { - if !preview && olds.Content != news.Content { - f, err := os.Create(olds.Path) - if err != nil { - return FileState{}, err - } - defer f.Close() - n, err := f.WriteString(news.Content) - if err != nil { - return FileState{}, err - } - if n != len(news.Content) { - return FileState{}, fmt.Errorf("only wrote %d/%d bytes", n, len(news.Content)) + err := infer.Apply2Err(olds.Path, news.Path, func(old, new string) (string, error) { + if old != new { + return "", fmt.Errorf("cannot change path") } + return "", nil + }).Anchor() + if err != nil { + return FileState{}, err } + _, err = infer.Apply3Err(olds.Content, news.Content, olds.Path, + func(path, oldContent, newContent string) (FileState, error) { + if preview || oldContent == newContent { + return FileState{}, nil + } + f, err := os.Create(path) + if err != nil { + return FileState{}, err + } + defer f.Close() + n, err := f.WriteString(newContent) + if err != nil { + return FileState{}, err + } + if n != len(newContent) { + return FileState{}, fmt.Errorf("only wrote %d/%d bytes", n, len(newContent)) + } + return FileState{}, nil + }).GetMaybeUnknown() return FileState{ Path: news.Path, Force: news.Force, Content: news.Content, - }, nil + }, err } func (*File) Diff(ctx p.Context, id string, olds FileState, news FileArgs) (p.DiffResponse, error) { diff := map[string]p.PropertyDiff{} - if news.Content != olds.Content { + if !news.Content.Equal(olds.Content) { diff["content"] = p.PropertyDiff{Kind: p.Update} } - if news.Force != olds.Force { + if !news.Force.Equal(olds.Force) { diff["force"] = p.PropertyDiff{Kind: p.Update} } - if news.Path != olds.Path { + if !news.Path.Equal(olds.Path) { diff["path"] = p.PropertyDiff{Kind: p.UpdateReplace} } return p.DiffResponse{ @@ -166,13 +194,13 @@ func (*File) Read(ctx p.Context, id string, inputs FileArgs, state FileState) (c } content := string(byteContent) return path, FileArgs{ - Path: path, - Force: inputs.Force && state.Force, - Content: content, + Path: infer.NewOutput(path), + Force: infer.Apply2(inputs.Force, state.Force, func(a, b bool) bool { return a || b }), + Content: infer.NewOutput(content), }, FileState{ - Path: path, - Force: inputs.Force && state.Force, - Content: content, + Path: infer.NewOutput(path), + Force: infer.Apply2(inputs.Force, state.Force, func(a, b bool) bool { return a || b }), + Content: infer.NewOutput(content), }, nil } diff --git a/infer/gen_apply/go.mod b/infer/gen_apply/go.mod new file mode 100644 index 00000000..ea06d277 --- /dev/null +++ b/infer/gen_apply/go.mod @@ -0,0 +1,3 @@ +module github.com/pulumi/pulumi-go-provider/infer/gen_apply + +go 1.21.0 diff --git a/infer/gen_apply/main.go b/infer/gen_apply/main.go new file mode 100644 index 00000000..0c576fc9 --- /dev/null +++ b/infer/gen_apply/main.go @@ -0,0 +1,114 @@ +// Copyright 2023, Pulumi Corporation. All rights reserved. + +package main + +import ( + "bytes" + "fmt" + "os" + "strings" + "text/template" +) + +func must[T any](t T, err error) T { + if err != nil { + panic(err) + } + return t +} + +var applyFuncs = must(template.New("apply").Parse( + ` +func Apply{{.inputs}}[{{.inputTypes}}U any]({{.inputArgsWithTypes}}f func({{.inputTypesNoComma}}) U) Output[U] { + return Apply{{.inputs}}Err({{.inputArgsNoTypes}}func({{.inputArgsRawTypes}}) (U, error) { + return f({{.inputArgsRaw}}), nil + }) +} + +func Apply{{.inputs}}Err[{{.inputTypes}}U any]({{.inputArgsWithTypes}}` + + `f func({{.inputTypesNoComma}}) (U, error)) Output[U] { + {{.ensureInputs}} + result := newOutput[U](nil, {{.orSecrets}}, {{.joinDeps}}) + go apply{{.inputs}}Result({{.inputArgsNoTypes}}result, f) + return result +} + +func apply{{.inputs}}Result[{{.inputTypes}}U any]({{.inputArgsWithTypes}}to Output[U],` + + ` f func({{.inputTypesNoComma}}) (U, error)) { + if {{.orCanResolve}} { + return + } + {{.wait}} + + // Propagate the change + to.join.L.Lock() + defer to.join.L.Unlock() + + if err := errors.Join({{.errs}}); err == nil { + tmp, err := f({{.values}}) + if err == nil { + to.value = &tmp + } else { + to.err = err + } + } else { + to.err = err + } + to.resolved = true + to.join.Broadcast() +} +`)) + +func main() { + b := new(bytes.Buffer) + b.WriteString(`// Copyright 2023, Pulumi Corporation. All rights reserved. + +// Code generated by gen_apply/main; DO NOT EDIT. + +package infer + +import "errors" + +`) + for i := 0; i < 9; i++ { + var inputs string + if i > 0 { + inputs = fmt.Sprintf("%d", i+1) + } + err := applyFuncs.Execute(b, map[string]string{ + "inputs": inputs, // A number, like "3" + "inputTypes": formatN(i, "T%d, ", ""), // "T1, T2, T3, " + "inputTypesNoComma": formatN(i, "T%d", ", "), // "T1, T2, T3" + // "o1 Output[T1], o2 Output[T2], o3 Output[T3], " + "inputArgsWithTypes": formatN(i, "o%[1]d Output[T%[1]d], ", ""), + "inputArgsNoTypes": formatN(i, "o%d, ", ""), // "o1, o2, " + "inputArgsRawTypes": formatN(i, "v%[1]d T%[1]d", ", "), // "v1 T1, v2 T2" + "inputArgsRaw": formatN(i, "v%d", ", "), // "v1, v2" + "orSecrets": formatN(i, "o%d.secret", " || "), // "o1.secret || o2.secret" + // "!o1.deps.canResolve() || !o1.deps.canResolve()" + "orCanResolve": formatN(i, "!o%d.resolvable", " || "), + "wait": formatN(i, "o%d.wait()", "\n\t"), // "o1.wait()\n\to2.wait()" + "errs": formatN(i, "o%d.err", ", "), // "o1.err, o2.err" + "values": formatN(i, "*o%d.value", ", "), // "*o1.value, *o2.value" + "joinDeps": formatN(i, "o%d.resolvable", " && "), // "o1.resolvable && o2.resolvable" + "ensureInputs": formatN(i, "o%d.ensure()", "\n\t"), // "o1.ensure()\n\to2.ensure()" + }) + if err != nil { + panic(err) + } + } + + fmt.Printf("Writing to %s\n", os.Args[2]) + err := os.WriteFile(os.Args[2], b.Bytes(), 0600) + if err != nil { + panic(err) + } +} + +func formatN(to int, msg, join string) string { + var arr []string + for i := 1; i <= to+1; i++ { + arr = append(arr, fmt.Sprintf(msg, i)) + } + return strings.Join(arr, join) +} diff --git a/infer/internal/ende/ende.go b/infer/internal/ende/ende.go index 1b7db8b5..b65f15df 100644 --- a/infer/internal/ende/ende.go +++ b/infer/internal/ende/ende.go @@ -17,10 +17,11 @@ package ende import ( "reflect" - "github.com/pulumi/pulumi-go-provider/internal/introspect" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" - "github.com/pulumi/pulumi/sdk/v3/go/common/util/mapper" + pmapper "github.com/pulumi/pulumi/sdk/v3/go/common/util/mapper" + + "github.com/pulumi/pulumi-go-provider/internal/introspect" ) type Encoder struct{ *ende } @@ -33,32 +34,32 @@ type Encoder struct{ *ende } // var value T // encoder, _ := Decode(m, &value) // m, _ = encoder.Encode(value) -func Decode[T any](m resource.PropertyMap, dst T) (Encoder, mapper.MappingError) { +func Decode[T any](m resource.PropertyMap, dst T) (Encoder, pmapper.MappingError) { return decode(m, dst, false, false) } // DecodeTolerateMissing is like Decode, but doesn't return an error for a missing value. -func DecodeTolerateMissing[T any](m resource.PropertyMap, dst T) (Encoder, mapper.MappingError) { +func DecodeTolerateMissing[T any](m resource.PropertyMap, dst T) (Encoder, pmapper.MappingError) { return decode(m, dst, false, true) } -func DecodeConfig[T any](m resource.PropertyMap, dst T) (Encoder, mapper.MappingError) { +func DecodeConfig[T any](m resource.PropertyMap, dst T) (Encoder, pmapper.MappingError) { return decode(m, dst, true, false) } func decode( m resource.PropertyMap, dst any, ignoreUnrecognized, allowMissing bool, -) (Encoder, mapper.MappingError) { +) (Encoder, pmapper.MappingError) { e := new(ende) target := reflect.ValueOf(dst) for target.Type().Kind() == reflect.Pointer && !target.IsNil() { target = target.Elem() } m = e.simplify(m, target.Type()) - return Encoder{e}, mapper.New(&mapper.Opts{ - IgnoreUnrecognized: ignoreUnrecognized, + return Encoder{e}, decodeProperty(m, target.Addr(), mapperOpts{ IgnoreMissing: allowMissing, - }).Decode(m.Mappable(), target.Addr().Interface()) + IgnoreUnrecognized: ignoreUnrecognized, + }) } @@ -140,29 +141,33 @@ func (e *ende) walk( } } - switch { - case v.IsSecret(): - // To allow full fidelity reconstructing maps, we extract nested secrets - // first. We then extract the top level secret. We need this ordering to - // re-embed nested secrets. - el := e.walk(v.SecretValue().Element, path, typ, alignTypes) - e.mark(change{path: path, secret: true}) - return el - case v.IsComputed(): - el := e.walk(v.Input().Element, path, typ, true) - e.mark(change{path: path, computed: true}) - return el - case v.IsOutput(): - output := v.OutputValue() - el := e.walk(output.Element, path, typ, !output.Known) - e.mark(change{ - path: path, - computed: !output.Known, - secret: output.Secret, - forceOutput: true, - }) - - return el + // If a type implements unmarshalFromPropertyValueType, we should leave these + // alone. + if typ == nil || !reflect.PtrTo(typ).Implements(EnDePropertyValueType) { + switch { + case v.IsSecret(): + // To allow full fidelity reconstructing maps, we extract nested secrets + // first. We then extract the top level secret. We need this ordering to + // re-embed nested secrets. + el := e.walk(v.SecretValue().Element, path, typ, alignTypes) + e.mark(change{path: path, secret: true}) + return el + case v.IsComputed(): + el := e.walk(v.Input().Element, path, typ, true) + e.mark(change{path: path, computed: true}) + return el + case v.IsOutput(): + output := v.OutputValue() + el := e.walk(output.Element, path, typ, !output.Known) + e.mark(change{ + path: path, + computed: !output.Known, + secret: output.Secret, + forceOutput: true, + }) + + return el + } } var elemType reflect.Type @@ -288,38 +293,32 @@ func (e *ende) walkMap( return resource.NewObjectProperty(result) } -func (e *ende) Encode(src any) (resource.PropertyMap, mapper.MappingError) { - props, err := mapper.New(&mapper.Opts{ - IgnoreMissing: true, - }).Encode(src) +func (e *ende) Encode(src any) (resource.PropertyMap, pmapper.MappingError) { + props, err := encodeProperty(src, mapperOpts{IgnoreMissing: true}) if err != nil { return nil, err } - m := resource.NewObjectProperty( - resource.NewPropertyMapFromMap(props), - ) - contract.Assertf(!m.ContainsUnknowns(), - "NewPropertyMapFromMap cannot produce unknown values") - contract.Assertf(!m.ContainsSecrets(), - "NewPropertyMapFromMap cannot produce secrets") - for _, s := range e.changes { - v, ok := s.path.Get(m) - if !ok && s.emptyAction == isNil { - continue - } + m := resource.NewObjectProperty(props) + if e != nil { + for _, s := range e.changes { + v, ok := s.path.Get(m) + if !ok && s.emptyAction == isNil { + continue + } - if s.emptyAction != isNil && v.IsNull() { - switch s.emptyAction { - case isEmptyMap: - v = resource.NewObjectProperty(resource.PropertyMap{}) - case isEmptyArr: - v = resource.NewArrayProperty([]resource.PropertyValue{}) - default: - panic(s.emptyAction) + if s.emptyAction != isNil && v.IsNull() { + switch s.emptyAction { + case isEmptyMap: + v = resource.NewObjectProperty(resource.PropertyMap{}) + case isEmptyArr: + v = resource.NewArrayProperty([]resource.PropertyValue{}) + default: + panic(s.emptyAction) + } } - } - s.path.Set(m, s.apply(v)) + s.path.Set(m, s.apply(v)) + } } return m.ObjectValue(), nil } diff --git a/infer/internal/ende/ende_test.go b/infer/internal/ende/ende_test.go index 302fa642..27b6c454 100644 --- a/infer/internal/ende/ende_test.go +++ b/infer/internal/ende/ende_test.go @@ -69,22 +69,29 @@ func TestRapidRoundTrip(t *testing.T) { func TestRapidDeepEqual(t *testing.T) { t.Parallel() // Check that a value always equals itself - rapid.Check(t, func(t *rapid.T) { - value := rResource.PropertyValue(5).Draw(t, "value") + t.Run("identity", func(t *testing.T) { + t.Parallel() + rapid.Check(t, func(t *rapid.T) { + value := rResource.PropertyValue(5).Draw(t, "value") - assert.True(t, DeepEquals(value, value)) + assert.True(t, DeepEquals(value, value)) + }) }) // Check that "distinct" values never equal themselves. - rapid.Check(t, func(t *rapid.T) { - values := rapid.SliceOfNDistinct(rResource.PropertyValue(5), 2, 2, - func(v r.PropertyValue) string { - return v.String() - }).Draw(t, "distinct") - assert.False(t, DeepEquals(values[0], values[1])) + t.Run("distinct", func(t *testing.T) { + t.Parallel() + rapid.Check(t, func(t *rapid.T) { + values := rapid.SliceOfNDistinct(rResource.PropertyValue(5), 2, 2, + func(v r.PropertyValue) string { + return v.String() + }).Draw(t, "distinct") + assert.False(t, DeepEquals(values[0], values[1])) + }) }) t.Run("folding", func(t *testing.T) { + t.Parallel() assert.True(t, DeepEquals( r.MakeComputed(r.MakeSecret(r.NewStringProperty("hi"))), r.MakeSecret(r.MakeComputed(r.NewStringProperty("hi"))))) diff --git a/infer/internal/ende/mapper.go b/infer/internal/ende/mapper.go new file mode 100644 index 00000000..94f6f3b2 --- /dev/null +++ b/infer/internal/ende/mapper.go @@ -0,0 +1,373 @@ +// Copyright 2023, Pulumi Corporation. All rights reserved. + +package ende + +import ( + "fmt" + "reflect" + + "github.com/pulumi/pulumi-go-provider/internal/introspect" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" + pmapper "github.com/pulumi/pulumi/sdk/v3/go/common/util/mapper" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +type mapperOpts struct { + IgnoreUnrecognized bool + IgnoreMissing bool +} + +func decodeProperty(from resource.PropertyMap, to reflect.Value, opts mapperOpts) pmapper.MappingError { + contract.Assertf(to.Kind() == reflect.Ptr && !to.IsNil() && to.Elem().CanSet(), + "Target %v must be a non-nil, settable pointer", to.Type()) + toType := to.Type().Elem() + contract.Assertf(toType.Kind() == reflect.Struct && !to.IsNil(), + "Target %v must be a struct type with `pulumi:\"x\"` tags to direct decoding", toType) + + ctx := &mapCtx{ty: toType, opts: opts} + + ctx.decodeStruct(from, to.Elem()) + + if len(ctx.errors) > 0 { + return pmapper.NewMappingError(ctx.errors) + } + return nil +} + +func (m *mapCtx) decodeStruct(from resource.PropertyMap, to reflect.Value) { + addressed := map[string]struct{}{} + for _, f := range reflect.VisibleFields(to.Type()) { + tag, err := introspect.ParseTag(f) + if err != nil { + m.errors = append(m.errors, + pmapper.NewFieldError(m.ty.String(), f.Name, err)) + continue + } + if tag.Internal { + continue + } + value, ok := from[resource.PropertyKey(tag.Name)] + if ok { + addressed[tag.Name] = struct{}{} + m.decodeValue(tag.Name, value, to.FieldByIndex(f.Index)) + } else if !(m.opts.IgnoreMissing || tag.Optional) { + m.errors = append(m.errors, pmapper.NewMissingError(m.ty, f.Name)) + } + } + + if !m.opts.IgnoreUnrecognized { + for k := range from { + if _, ok := addressed[string(k)]; ok { + continue + } + m.errors = append(m.errors, pmapper.NewUnrecognizedError(m.ty, string(k))) + } + } +} + +func (m *mapCtx) decodePrimitive(fieldName string, from any, to reflect.Value) { + elem := hydrateMaybePointer(to) + fromV := reflect.ValueOf(from) + if !fromV.CanConvert(elem.Type()) { + m.errors = append(m.errors, pmapper.NewWrongTypeError(m.ty, + fieldName, elem.Type(), fromV.Type())) + return + } + elem.Set(fromV.Convert(elem.Type())) +} + +type PropertyValue interface { + DecodeFromPropertyValue(resource.PropertyValue, func(resource.PropertyValue, reflect.Value)) + EncodeToPropertyValue(func(any) resource.PropertyValue) resource.PropertyValue + + // This method might be called on a zero value instance: + // + // t = reflect.New(t).Elem().Interface().(ende.EnDePropertyValue).UnderlyingSchemaType() + // + UnderlyingSchemaType() reflect.Type +} + +var EnDePropertyValueType = reflect.TypeOf((*PropertyValue)(nil)).Elem() + +func (m *mapCtx) decodeValue(fieldName string, from resource.PropertyValue, to reflect.Value) { + if to := to.Addr(); to.CanInterface() && to.Type().Implements(EnDePropertyValueType) { + to.Interface().(PropertyValue).DecodeFromPropertyValue(from, + func(from resource.PropertyValue, to reflect.Value) { + m.decodeValue(fieldName, from, to) + }) + return + } + + switch { + // Primitives + case from.IsBool(): + m.decodePrimitive(fieldName, from.BoolValue(), to) + case from.IsNumber(): + m.decodePrimitive(fieldName, from.NumberValue(), to) + case from.IsString(): + m.decodePrimitive(fieldName, from.StringValue(), to) + + // Collections + case from.IsArray(): + arr := from.ArrayValue() + elem := hydrateMaybePointer(to) + if elem.Kind() != reflect.Slice { + m.errors = append(m.errors, pmapper.NewWrongTypeError(m.ty, + fieldName, elem.Type(), reflect.TypeOf(arr))) + return + } + length := len(arr) + elem.Set(reflect.MakeSlice(elem.Type(), length, length)) + for i, v := range arr { + m.decodeValue(fmt.Sprintf("%s[%d]", fieldName, i), v, elem.Index(i)) + } + case from.IsObject(): + obj := from.ObjectValue() + elem := hydrateMaybePointer(to) + switch elem.Kind() { + case reflect.Struct: + m.decodeStruct(obj, elem) + case reflect.Map: + if key := elem.Type().Key(); key.Kind() != reflect.String { + m.errors = append(m.errors, pmapper.NewWrongTypeError(m.ty, + fieldName, reflect.TypeOf(""), key)) + return + } + elem.Set(reflect.MakeMapWithSize(elem.Type(), len(obj))) + for k, v := range obj { + place := reflect.New(elem.Type().Elem()).Elem() + m.decodeValue(fmt.Sprintf("%s[%s]", fieldName, string(k)), v, place) + elem.SetMapIndex(reflect.ValueOf(string(k)), place) + } + default: + m.errors = append(m.errors, pmapper.NewWrongTypeError(m.ty, + fieldName, elem.Type(), reflect.TypeOf(obj))) + } + + // Markers + case from.IsSecret(): + m.decodeValue(fieldName, from.SecretValue().Element, to) + case from.IsComputed(): + m.decodeValue(fieldName, from.OutputValue().Element, to) + + // Special values + case from.IsAsset(): + elem := hydrateMaybePointer(to) + if !elem.Type().Implements(assetType) { + m.errors = append(m.errors, pmapper.NewWrongTypeError(m.ty, + fieldName, elem.Type(), assetType)) + return + } + assetV := from.AssetValue() + var asset pulumi.Asset + switch { + case assetV.IsPath(): + asset = pulumi.NewFileAsset(assetV.Path) + case assetV.IsURI(): + asset = pulumi.NewRemoteAsset(assetV.URI) + case assetV.IsText(): + asset = pulumi.NewStringAsset(assetV.Text) + default: + m.errors = append(m.errors, pmapper.NewTypeFieldError(m.ty, + fieldName, fmt.Errorf("unrecognized Asset type"))) + return + } + elem.Set(reflect.ValueOf(asset)) + case from.IsArchive(): + elem := hydrateMaybePointer(to) + if !elem.Type().Implements(archiveType) { + m.errors = append(m.errors, pmapper.NewWrongTypeError(m.ty, + fieldName, elem.Type(), assetType)) + return + } + archiveV := from.ArchiveValue() + var archive pulumi.Archive + switch { + case archiveV.IsURI(): + archive = pulumi.NewRemoteArchive(archiveV.URI) + case archiveV.IsPath(): + archive = pulumi.NewFileArchive(archiveV.Path) + case archiveV.IsAssets(): + archive = pulumi.NewAssetArchive(archiveV.Assets) + default: + m.errors = append(m.errors, pmapper.NewTypeFieldError(m.ty, + fieldName, fmt.Errorf("unrecognized Archive type"))) + return + } + elem.Set(reflect.ValueOf(archive)) + case from.IsNull(): + // No-op + default: + contract.Failf("Unknown property kind: %#v", from) + } +} + +var ( + assetType = reflect.TypeOf((*pulumi.Asset)(nil)).Elem() + archiveType = reflect.TypeOf((*pulumi.Archive)(nil)).Elem() +) + +func hydrateMaybePointer(to reflect.Value) reflect.Value { + contract.Assertf(to.CanSet(), "must be able to set to hydrate") + for to.Kind() == reflect.Ptr { + if to.IsNil() { + to.Set(reflect.New(to.Type().Elem())) + } + to = to.Elem() + } + return to +} + +func encodeProperty(from any, opts mapperOpts) (resource.PropertyMap, pmapper.MappingError) { + if from == nil { + return nil, nil + } + + fromV := reflect.ValueOf(from) + fromT := fromV.Type() + for fromT.Kind() == reflect.Ptr { + fromT = fromT.Elem() + fromV = fromV.Elem() + } + contract.Assertf(fromT.Kind() == reflect.Struct, "expect to encode a struct") + + mapCtx := &mapCtx{ty: fromT, opts: opts} + pMap := mapCtx.encodeStruct(fromV) + if len(mapCtx.errors) > 0 { + return nil, pmapper.NewMappingError(mapCtx.errors) + } + return pMap, nil +} + +type mapCtx struct { + opts mapperOpts + ty reflect.Type + errors []error +} + +func (m *mapCtx) encodeStruct(from reflect.Value) resource.PropertyMap { + visableFields := reflect.VisibleFields(from.Type()) + obj := make(resource.PropertyMap, len(visableFields)) + for _, f := range visableFields { + tag, err := introspect.ParseTag(f) + if err != nil { + m.errors = append(m.errors, + pmapper.NewFieldError(m.ty.String(), f.Name, err)) + continue + } + if tag.Internal { + continue + } + key := resource.PropertyKey(tag.Name) + value := m.encodeValue(from.FieldByIndex(f.Index)) + if value.IsNull() && tag.Optional { + continue + } + obj[key] = value + } + return obj +} + +func (m *mapCtx) encodeMap(from reflect.Value) resource.PropertyMap { + wMap := make(resource.PropertyMap, from.Len()) + iter := from.MapRange() + for iter.Next() { + key := iter.Key() + if key.Kind() != reflect.String { + panic("unexpected key type") + } + value := m.encodeValue(iter.Value()) + if value.IsNull() { + continue + } + wMap[resource.PropertyKey(key.String())] = value + } + return wMap +} + +func (m *mapCtx) encodeValue(from reflect.Value) resource.PropertyValue { + for from.Kind() == reflect.Ptr { + if from.IsNil() { + return resource.NewNullProperty() + } + from = from.Elem() + } + + if reflect.PtrTo(from.Type()).Implements(EnDePropertyValueType) { + v := reflect.New(from.Type()) + v.Elem().Set(from) + return v.Interface().(PropertyValue).EncodeToPropertyValue(func(a any) resource.PropertyValue { + return m.encodeValue(reflect.ValueOf(&a).Elem()) + }) + } + + switch { + case from.Type().ConvertibleTo(assetType) && !from.IsNil(): + asset := from.Convert(assetType).Interface().(pulumi.Asset) + var assetV *resource.Asset + var err error + switch { + case asset.URI() != "": + assetV, err = resource.NewURIAsset(asset.URI()) + case asset.Path() != "": + assetV, err = resource.NewPathAsset(asset.Path()) + case asset.Text() != "": + assetV, err = resource.NewTextAsset(asset.Text()) + } + if err != nil { + m.errors = append(m.errors, err) + } + return resource.NewAssetProperty(assetV) + case from.Type().ConvertibleTo(archiveType) && !from.IsNil(): + archive := from.Convert(archiveType).Interface().(pulumi.Archive) + var archiveV *resource.Archive + var err error + switch { + case archive.URI() != "": + archiveV, err = resource.NewURIArchive(archive.URI()) + case archive.Path() != "": + archiveV, err = resource.NewPathArchive(archive.Path()) + case archive.Assets() != nil: + archiveV, err = resource.NewAssetArchive(archive.Assets()) + } + if err != nil { + m.errors = append(m.errors, err) + } + return resource.NewArchiveProperty(archiveV) + } + + switch from.Kind() { + case reflect.String: + return resource.NewStringProperty(from.String()) + case reflect.Bool: + return resource.NewBoolProperty(from.Bool()) + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, + reflect.Float32, reflect.Float64: + return resource.NewNumberProperty(from.Convert(reflect.TypeOf(float64(0))).Float()) + case reflect.Slice, reflect.Array: + if from.IsNil() { + return resource.NewNullProperty() + } + arr := make([]resource.PropertyValue, from.Len()) + for i := 0; i < from.Len(); i++ { + arr[i] = m.encodeValue(from.Index(i)) + } + return resource.NewArrayProperty(arr) + case reflect.Struct: + return resource.NewObjectProperty(m.encodeStruct(from)) + case reflect.Map: + if from.IsNil() { + return resource.NewNullProperty() + } + obj := m.encodeMap(from) + return resource.NewObjectProperty(obj) + case reflect.Interface: + if from.IsNil() { + return resource.NewNullProperty() + } + return m.encodeValue(from.Elem()) + default: + panic("Unknown type: " + from.Type().String()) + } +} diff --git a/infer/internal/ende/mapper_test.go b/infer/internal/ende/mapper_test.go new file mode 100644 index 00000000..70a234dc --- /dev/null +++ b/infer/internal/ende/mapper_test.go @@ -0,0 +1,117 @@ +// Copyright 2023, Pulumi Corporation. All rights reserved. + +package ende_test + +import ( + "testing" + + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/pulumi/pulumi-go-provider/infer" + "github.com/pulumi/pulumi-go-provider/infer/internal/ende" +) + +func TestEnDeValue(t *testing.T) { + t.Parallel() + t.Run("asset", func(t *testing.T) { + t.Parallel() + + text := "some-text" + textAsset, err := resource.NewTextAsset(text) + require.NoError(t, err) + + uri := "https://example.com/uri" + uriAsset, err := resource.NewURIAsset(uri) + require.NoError(t, err) + + path := "./mapper_test.go" + pathAsset, err := resource.NewPathAsset(path) + require.NoError(t, err) + + type hasAsset struct { + Text pulumi.Asset `pulumi:"text"` + URI pulumi.Asset `pulumi:"uri"` + Path pulumi.Asset `pulumi:"path"` + + Optional pulumi.Asset `pulumi:"optional,optional"` + } + + initialMap := resource.PropertyMap{ + "text": resource.NewAssetProperty(textAsset), + "uri": resource.NewAssetProperty(uriAsset), + "path": resource.NewAssetProperty(pathAsset), + + "optional": resource.NewNullProperty(), + } + + target := new(hasAsset) + e, mErr := ende.Decode(initialMap, target) + require.NoError(t, mErr) + + assert.Equal(t, text, target.Text.Text()) + assert.Equal(t, uri, target.URI.URI()) + assert.Equal(t, path, target.Path.Path()) + + actualMap, err := e.Encode(target) + require.NoError(t, err) + delete(initialMap, "optional") + assert.Equal(t, initialMap, actualMap) + }) + + t.Run("output", func(t *testing.T) { + t.Parallel() + + type nested struct { + N string `pulumi:"n"` + F float64 `pulumi:"f"` + } + + s := "some string" + i := float64(42) + m := map[string]any{"yes": true, "no": false} + a := []string{"zero", "one", "two"} + n := nested{ + N: "nested string", + F: 1.2, + } + + type hasOutputs struct { + S infer.Output[string] `pulumi:"s"` + I infer.Output[int] `pulumi:"i"` + M infer.Output[map[string]bool] `pulumi:"m"` + A infer.Output[[]string] `pulumi:"a"` + N infer.Output[nested] `pulumi:"n"` + } + + initialMap := resource.PropertyMap{ + "s": resource.NewStringProperty(s), + "i": resource.NewNumberProperty(i), + "m": resource.NewObjectProperty(resource.NewPropertyMapFromMap(m)), + "a": resource.NewArrayProperty(fmap(a, resource.NewStringProperty)), + "n": resource.NewObjectProperty(resource.NewPropertyMap(n)), + } + + target := new(hasOutputs) + + e, mErr := ende.Decode(initialMap, target) + require.NoError(t, mErr) + + assert.Equal(t, s, target.S.MustGetKnown()) + + actualMap, err := e.Encode(target) + require.NoError(t, err) + assert.Equal(t, initialMap, actualMap) + + }) +} + +func fmap[T, U any](arr []T, f func(T) U) []U { + out := make([]U, len(arr)) + for i, v := range arr { + out[i] = f(v) + } + return out +} diff --git a/infer/output.go b/infer/output.go new file mode 100644 index 00000000..b9b10f3a --- /dev/null +++ b/infer/output.go @@ -0,0 +1,190 @@ +// Copyright 2023, Pulumi Corporation. All rights reserved. + +package infer + +import ( + "reflect" + "sync" + + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" + + "github.com/pulumi/pulumi-go-provider/infer/internal/ende" +) + +//go:generate go run ./gen_apply/main.go -- output_apply.go + +type Output[T any] struct { + *state[T] + + _ []struct{} // Make Output[T] uncomparable +} + +func NewOutput[T any](value T) Output[T] { return newOutput(&value, false, true) } + +func (o Output[T]) IsSecret() bool { + o.ensure() + return o.secret +} + +func (o Output[T]) Equal(other Output[T]) bool { + return reflect.DeepEqual(o, other) +} + +// Return an equivalent output that is secret. +// +// AsSecret is idempotent. +func (o Output[T]) AsSecret() Output[T] { + o.ensure() + r := o.copyOutput() + r.secret = true + return r +} + +// Return an equivalent output that is not secret, even if it's inputs were secret. +// +// AsPublic is idempotent. +func (o Output[T]) AsPublic() Output[T] { + o.ensure() + r := o.copyOutput() + r.secret = false + return r +} + +// Blocks until an output resolves if the output will resolve. +// +// When running with preview=false, this will always block until the output resolves. +func (o Output[T]) Anchor() error { + if o.resolvable { + o.wait() + } + return o.err +} + +// Get the value inside, or the zero value if none is available. +func (o Output[T]) GetMaybeUnknown() (T, error) { + o.ensure() + err := o.Anchor() + if o.resolvable { + return *o.value, err + } + var v T + return v, err +} + +func (o Output[T]) MustGetKnown() T { + o.ensure() + v, err := o.GetKnown() + contract.AssertNoErrorf(err, "Output[T].MustGetKnown()") + return v +} + +func (o Output[T]) GetKnown() (T, error) { + o.ensure() + if !o.resolvable { + panic("Attempted to get a known value from an unresolvable Output[T]") + } + o.wait() + return *o.value, o.err +} + +func (o Output[T]) copyOutput() Output[T] { + return Apply(o, func(x T) T { return x }) +} + +type state[T any] struct { + value *T // The resolved value + err error // An error encountered when resolving the value + resolved bool // If the value is fully resolved + resolvable bool // If the value can be resolved + secret bool // If the value is secret + + join *sync.Cond +} + +func (s *state[T]) wait() { + contract.Assertf(s.resolvable, "awaiting output that will never resolve") + s.join.L.Lock() + defer s.join.L.Unlock() + for !s.resolved { + s.join.Wait() + } +} + +func (o Output[T]) field() string { return "" } + +func newOutput[T any](value *T, secret, resolvable bool) Output[T] { + m := new(sync.Mutex) + state := &state[T]{ + value: value, + resolved: value != nil, + // An Output[T] is resolvable if it has dependencies that can be resolved, + // or if the value is non-nil. + // + // An Output[T] with no dependencies that is not resolved will never + // resolve. + resolvable: resolvable, + secret: secret, + join: sync.NewCond(m), + } + + return Output[T]{state, nil} +} + +// ensure that the output is in a valid state. +// +// Output[T] is often left as its zero value: `Output[T]{state: nil}`. This happens when +// an optional input value is left empty. Empty means not computed, so we set the value to +// contain the resolved, public zero value of T. +func (o *Output[T]) ensure() { + if o.state == nil { + var t T + *o = newOutput[T](&t, false, true) + } +} + +var _ = (ende.PropertyValue)((*Output[string])(nil)) + +// Name is tied to ende/decode implementation +func (o *Output[T]) DecodeFromPropertyValue( + value resource.PropertyValue, + assignInner func(resource.PropertyValue, reflect.Value), +) { + secret := ende.IsSecret(value) + if ende.IsComputed(value) { + *o = newOutput[T](nil, secret, false) + return + } + + var t T + dstValue := reflect.ValueOf(&t).Elem() + value = ende.MakePublic(value) + assignInner(value, dstValue) + + contract.Assertf(!value.IsSecret() && !value.IsComputed(), + "We should have unwrapped all secrets at this point") + + *o = newOutput[T](&t, secret, true) +} + +func (o *Output[T]) EncodeToPropertyValue(f func(any) resource.PropertyValue) resource.PropertyValue { + if o == nil || o.state == nil { + return ende.MakeComputed(resource.NewNullProperty()) + } + if o.resolvable { + o.wait() + } + + prop := resource.NewNullProperty() + if o.resolved { + prop = f(*o.value) + } else { + prop = ende.MakeComputed(prop) + } + if o.secret { + prop = ende.MakeSecret(prop) + } + return prop +} + +func (*Output[T]) UnderlyingSchemaType() reflect.Type { return reflect.TypeOf((*T)(nil)).Elem() } diff --git a/infer/output_apply.go b/infer/output_apply.go new file mode 100644 index 00000000..36bdcf01 --- /dev/null +++ b/infer/output_apply.go @@ -0,0 +1,413 @@ +// Copyright 2023, Pulumi Corporation. All rights reserved. + +// Code generated by gen_apply/main; DO NOT EDIT. + +package infer + +import "errors" + + +func Apply[T1, U any](o1 Output[T1], f func(T1) U) Output[U] { + return ApplyErr(o1, func(v1 T1) (U, error) { + return f(v1), nil + }) +} + +func ApplyErr[T1, U any](o1 Output[T1], f func(T1) (U, error)) Output[U] { + o1.ensure() + result := newOutput[U](nil, o1.secret, o1.resolvable) + go applyResult(o1, result, f) + return result +} + +func applyResult[T1, U any](o1 Output[T1], to Output[U], f func(T1) (U, error)) { + if !o1.resolvable { + return + } + o1.wait() + + // Propagate the change + to.join.L.Lock() + defer to.join.L.Unlock() + + if err := errors.Join(o1.err); err == nil { + tmp, err := f(*o1.value) + if err == nil { + to.value = &tmp + } else { + to.err = err + } + } else { + to.err = err + } + to.resolved = true + to.join.Broadcast() +} + +func Apply2[T1, T2, U any](o1 Output[T1], o2 Output[T2], f func(T1, T2) U) Output[U] { + return Apply2Err(o1, o2, func(v1 T1, v2 T2) (U, error) { + return f(v1, v2), nil + }) +} + +func Apply2Err[T1, T2, U any](o1 Output[T1], o2 Output[T2], f func(T1, T2) (U, error)) Output[U] { + o1.ensure() + o2.ensure() + result := newOutput[U](nil, o1.secret || o2.secret, o1.resolvable && o2.resolvable) + go apply2Result(o1, o2, result, f) + return result +} + +func apply2Result[T1, T2, U any](o1 Output[T1], o2 Output[T2], to Output[U], f func(T1, T2) (U, error)) { + if !o1.resolvable || !o2.resolvable { + return + } + o1.wait() + o2.wait() + + // Propagate the change + to.join.L.Lock() + defer to.join.L.Unlock() + + if err := errors.Join(o1.err, o2.err); err == nil { + tmp, err := f(*o1.value, *o2.value) + if err == nil { + to.value = &tmp + } else { + to.err = err + } + } else { + to.err = err + } + to.resolved = true + to.join.Broadcast() +} + +func Apply3[T1, T2, T3, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], f func(T1, T2, T3) U) Output[U] { + return Apply3Err(o1, o2, o3, func(v1 T1, v2 T2, v3 T3) (U, error) { + return f(v1, v2, v3), nil + }) +} + +func Apply3Err[T1, T2, T3, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], f func(T1, T2, T3) (U, error)) Output[U] { + o1.ensure() + o2.ensure() + o3.ensure() + result := newOutput[U](nil, o1.secret || o2.secret || o3.secret, o1.resolvable && o2.resolvable && o3.resolvable) + go apply3Result(o1, o2, o3, result, f) + return result +} + +func apply3Result[T1, T2, T3, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], to Output[U], f func(T1, T2, T3) (U, error)) { + if !o1.resolvable || !o2.resolvable || !o3.resolvable { + return + } + o1.wait() + o2.wait() + o3.wait() + + // Propagate the change + to.join.L.Lock() + defer to.join.L.Unlock() + + if err := errors.Join(o1.err, o2.err, o3.err); err == nil { + tmp, err := f(*o1.value, *o2.value, *o3.value) + if err == nil { + to.value = &tmp + } else { + to.err = err + } + } else { + to.err = err + } + to.resolved = true + to.join.Broadcast() +} + +func Apply4[T1, T2, T3, T4, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], f func(T1, T2, T3, T4) U) Output[U] { + return Apply4Err(o1, o2, o3, o4, func(v1 T1, v2 T2, v3 T3, v4 T4) (U, error) { + return f(v1, v2, v3, v4), nil + }) +} + +func Apply4Err[T1, T2, T3, T4, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], f func(T1, T2, T3, T4) (U, error)) Output[U] { + o1.ensure() + o2.ensure() + o3.ensure() + o4.ensure() + result := newOutput[U](nil, o1.secret || o2.secret || o3.secret || o4.secret, o1.resolvable && o2.resolvable && o3.resolvable && o4.resolvable) + go apply4Result(o1, o2, o3, o4, result, f) + return result +} + +func apply4Result[T1, T2, T3, T4, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], to Output[U], f func(T1, T2, T3, T4) (U, error)) { + if !o1.resolvable || !o2.resolvable || !o3.resolvable || !o4.resolvable { + return + } + o1.wait() + o2.wait() + o3.wait() + o4.wait() + + // Propagate the change + to.join.L.Lock() + defer to.join.L.Unlock() + + if err := errors.Join(o1.err, o2.err, o3.err, o4.err); err == nil { + tmp, err := f(*o1.value, *o2.value, *o3.value, *o4.value) + if err == nil { + to.value = &tmp + } else { + to.err = err + } + } else { + to.err = err + } + to.resolved = true + to.join.Broadcast() +} + +func Apply5[T1, T2, T3, T4, T5, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], o5 Output[T5], f func(T1, T2, T3, T4, T5) U) Output[U] { + return Apply5Err(o1, o2, o3, o4, o5, func(v1 T1, v2 T2, v3 T3, v4 T4, v5 T5) (U, error) { + return f(v1, v2, v3, v4, v5), nil + }) +} + +func Apply5Err[T1, T2, T3, T4, T5, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], o5 Output[T5], f func(T1, T2, T3, T4, T5) (U, error)) Output[U] { + o1.ensure() + o2.ensure() + o3.ensure() + o4.ensure() + o5.ensure() + result := newOutput[U](nil, o1.secret || o2.secret || o3.secret || o4.secret || o5.secret, o1.resolvable && o2.resolvable && o3.resolvable && o4.resolvable && o5.resolvable) + go apply5Result(o1, o2, o3, o4, o5, result, f) + return result +} + +func apply5Result[T1, T2, T3, T4, T5, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], o5 Output[T5], to Output[U], f func(T1, T2, T3, T4, T5) (U, error)) { + if !o1.resolvable || !o2.resolvable || !o3.resolvable || !o4.resolvable || !o5.resolvable { + return + } + o1.wait() + o2.wait() + o3.wait() + o4.wait() + o5.wait() + + // Propagate the change + to.join.L.Lock() + defer to.join.L.Unlock() + + if err := errors.Join(o1.err, o2.err, o3.err, o4.err, o5.err); err == nil { + tmp, err := f(*o1.value, *o2.value, *o3.value, *o4.value, *o5.value) + if err == nil { + to.value = &tmp + } else { + to.err = err + } + } else { + to.err = err + } + to.resolved = true + to.join.Broadcast() +} + +func Apply6[T1, T2, T3, T4, T5, T6, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], o5 Output[T5], o6 Output[T6], f func(T1, T2, T3, T4, T5, T6) U) Output[U] { + return Apply6Err(o1, o2, o3, o4, o5, o6, func(v1 T1, v2 T2, v3 T3, v4 T4, v5 T5, v6 T6) (U, error) { + return f(v1, v2, v3, v4, v5, v6), nil + }) +} + +func Apply6Err[T1, T2, T3, T4, T5, T6, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], o5 Output[T5], o6 Output[T6], f func(T1, T2, T3, T4, T5, T6) (U, error)) Output[U] { + o1.ensure() + o2.ensure() + o3.ensure() + o4.ensure() + o5.ensure() + o6.ensure() + result := newOutput[U](nil, o1.secret || o2.secret || o3.secret || o4.secret || o5.secret || o6.secret, o1.resolvable && o2.resolvable && o3.resolvable && o4.resolvable && o5.resolvable && o6.resolvable) + go apply6Result(o1, o2, o3, o4, o5, o6, result, f) + return result +} + +func apply6Result[T1, T2, T3, T4, T5, T6, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], o5 Output[T5], o6 Output[T6], to Output[U], f func(T1, T2, T3, T4, T5, T6) (U, error)) { + if !o1.resolvable || !o2.resolvable || !o3.resolvable || !o4.resolvable || !o5.resolvable || !o6.resolvable { + return + } + o1.wait() + o2.wait() + o3.wait() + o4.wait() + o5.wait() + o6.wait() + + // Propagate the change + to.join.L.Lock() + defer to.join.L.Unlock() + + if err := errors.Join(o1.err, o2.err, o3.err, o4.err, o5.err, o6.err); err == nil { + tmp, err := f(*o1.value, *o2.value, *o3.value, *o4.value, *o5.value, *o6.value) + if err == nil { + to.value = &tmp + } else { + to.err = err + } + } else { + to.err = err + } + to.resolved = true + to.join.Broadcast() +} + +func Apply7[T1, T2, T3, T4, T5, T6, T7, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], o5 Output[T5], o6 Output[T6], o7 Output[T7], f func(T1, T2, T3, T4, T5, T6, T7) U) Output[U] { + return Apply7Err(o1, o2, o3, o4, o5, o6, o7, func(v1 T1, v2 T2, v3 T3, v4 T4, v5 T5, v6 T6, v7 T7) (U, error) { + return f(v1, v2, v3, v4, v5, v6, v7), nil + }) +} + +func Apply7Err[T1, T2, T3, T4, T5, T6, T7, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], o5 Output[T5], o6 Output[T6], o7 Output[T7], f func(T1, T2, T3, T4, T5, T6, T7) (U, error)) Output[U] { + o1.ensure() + o2.ensure() + o3.ensure() + o4.ensure() + o5.ensure() + o6.ensure() + o7.ensure() + result := newOutput[U](nil, o1.secret || o2.secret || o3.secret || o4.secret || o5.secret || o6.secret || o7.secret, o1.resolvable && o2.resolvable && o3.resolvable && o4.resolvable && o5.resolvable && o6.resolvable && o7.resolvable) + go apply7Result(o1, o2, o3, o4, o5, o6, o7, result, f) + return result +} + +func apply7Result[T1, T2, T3, T4, T5, T6, T7, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], o5 Output[T5], o6 Output[T6], o7 Output[T7], to Output[U], f func(T1, T2, T3, T4, T5, T6, T7) (U, error)) { + if !o1.resolvable || !o2.resolvable || !o3.resolvable || !o4.resolvable || !o5.resolvable || !o6.resolvable || !o7.resolvable { + return + } + o1.wait() + o2.wait() + o3.wait() + o4.wait() + o5.wait() + o6.wait() + o7.wait() + + // Propagate the change + to.join.L.Lock() + defer to.join.L.Unlock() + + if err := errors.Join(o1.err, o2.err, o3.err, o4.err, o5.err, o6.err, o7.err); err == nil { + tmp, err := f(*o1.value, *o2.value, *o3.value, *o4.value, *o5.value, *o6.value, *o7.value) + if err == nil { + to.value = &tmp + } else { + to.err = err + } + } else { + to.err = err + } + to.resolved = true + to.join.Broadcast() +} + +func Apply8[T1, T2, T3, T4, T5, T6, T7, T8, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], o5 Output[T5], o6 Output[T6], o7 Output[T7], o8 Output[T8], f func(T1, T2, T3, T4, T5, T6, T7, T8) U) Output[U] { + return Apply8Err(o1, o2, o3, o4, o5, o6, o7, o8, func(v1 T1, v2 T2, v3 T3, v4 T4, v5 T5, v6 T6, v7 T7, v8 T8) (U, error) { + return f(v1, v2, v3, v4, v5, v6, v7, v8), nil + }) +} + +func Apply8Err[T1, T2, T3, T4, T5, T6, T7, T8, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], o5 Output[T5], o6 Output[T6], o7 Output[T7], o8 Output[T8], f func(T1, T2, T3, T4, T5, T6, T7, T8) (U, error)) Output[U] { + o1.ensure() + o2.ensure() + o3.ensure() + o4.ensure() + o5.ensure() + o6.ensure() + o7.ensure() + o8.ensure() + result := newOutput[U](nil, o1.secret || o2.secret || o3.secret || o4.secret || o5.secret || o6.secret || o7.secret || o8.secret, o1.resolvable && o2.resolvable && o3.resolvable && o4.resolvable && o5.resolvable && o6.resolvable && o7.resolvable && o8.resolvable) + go apply8Result(o1, o2, o3, o4, o5, o6, o7, o8, result, f) + return result +} + +func apply8Result[T1, T2, T3, T4, T5, T6, T7, T8, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], o5 Output[T5], o6 Output[T6], o7 Output[T7], o8 Output[T8], to Output[U], f func(T1, T2, T3, T4, T5, T6, T7, T8) (U, error)) { + if !o1.resolvable || !o2.resolvable || !o3.resolvable || !o4.resolvable || !o5.resolvable || !o6.resolvable || !o7.resolvable || !o8.resolvable { + return + } + o1.wait() + o2.wait() + o3.wait() + o4.wait() + o5.wait() + o6.wait() + o7.wait() + o8.wait() + + // Propagate the change + to.join.L.Lock() + defer to.join.L.Unlock() + + if err := errors.Join(o1.err, o2.err, o3.err, o4.err, o5.err, o6.err, o7.err, o8.err); err == nil { + tmp, err := f(*o1.value, *o2.value, *o3.value, *o4.value, *o5.value, *o6.value, *o7.value, *o8.value) + if err == nil { + to.value = &tmp + } else { + to.err = err + } + } else { + to.err = err + } + to.resolved = true + to.join.Broadcast() +} + +func Apply9[T1, T2, T3, T4, T5, T6, T7, T8, T9, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], o5 Output[T5], o6 Output[T6], o7 Output[T7], o8 Output[T8], o9 Output[T9], f func(T1, T2, T3, T4, T5, T6, T7, T8, T9) U) Output[U] { + return Apply9Err(o1, o2, o3, o4, o5, o6, o7, o8, o9, func(v1 T1, v2 T2, v3 T3, v4 T4, v5 T5, v6 T6, v7 T7, v8 T8, v9 T9) (U, error) { + return f(v1, v2, v3, v4, v5, v6, v7, v8, v9), nil + }) +} + +func Apply9Err[T1, T2, T3, T4, T5, T6, T7, T8, T9, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], o5 Output[T5], o6 Output[T6], o7 Output[T7], o8 Output[T8], o9 Output[T9], f func(T1, T2, T3, T4, T5, T6, T7, T8, T9) (U, error)) Output[U] { + o1.ensure() + o2.ensure() + o3.ensure() + o4.ensure() + o5.ensure() + o6.ensure() + o7.ensure() + o8.ensure() + o9.ensure() + result := newOutput[U](nil, o1.secret || o2.secret || o3.secret || o4.secret || o5.secret || o6.secret || o7.secret || o8.secret || o9.secret, o1.resolvable && o2.resolvable && o3.resolvable && o4.resolvable && o5.resolvable && o6.resolvable && o7.resolvable && o8.resolvable && o9.resolvable) + go apply9Result(o1, o2, o3, o4, o5, o6, o7, o8, o9, result, f) + return result +} + +func apply9Result[T1, T2, T3, T4, T5, T6, T7, T8, T9, U any](o1 Output[T1], o2 Output[T2], o3 Output[T3], o4 Output[T4], o5 Output[T5], o6 Output[T6], o7 Output[T7], o8 Output[T8], o9 Output[T9], to Output[U], f func(T1, T2, T3, T4, T5, T6, T7, T8, T9) (U, error)) { + if !o1.resolvable || !o2.resolvable || !o3.resolvable || !o4.resolvable || !o5.resolvable || !o6.resolvable || !o7.resolvable || !o8.resolvable || !o9.resolvable { + return + } + o1.wait() + o2.wait() + o3.wait() + o4.wait() + o5.wait() + o6.wait() + o7.wait() + o8.wait() + o9.wait() + + // Propagate the change + to.join.L.Lock() + defer to.join.L.Unlock() + + if err := errors.Join(o1.err, o2.err, o3.err, o4.err, o5.err, o6.err, o7.err, o8.err, o9.err); err == nil { + tmp, err := f(*o1.value, *o2.value, *o3.value, *o4.value, *o5.value, *o6.value, *o7.value, *o8.value, *o9.value) + if err == nil { + to.value = &tmp + } else { + to.err = err + } + } else { + to.err = err + } + to.resolved = true + to.join.Broadcast() +} diff --git a/infer/output_test.go b/infer/output_test.go new file mode 100644 index 00000000..f0a1cfab --- /dev/null +++ b/infer/output_test.go @@ -0,0 +1,86 @@ +// Copyright 2023, Pulumi Corporation. All rights reserved. + +package infer + +import ( + "testing" + + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/pulumi/pulumi-go-provider/infer/internal/ende" +) + +func TestOutputMapping(t *testing.T) { + t.Parallel() + p := func() resource.PropertyMap { + return resource.PropertyMap{ + "sec": resource.MakeSecret(resource.NewStringProperty("foo")), + "unkn": resource.MakeComputed(resource.NewNullProperty()), + "out": resource.NewOutputProperty(resource.Output{ + Known: false, + Secret: true, + }), + "plain": resource.NewStringProperty("known and public"), + } + } + + type decodeTarget struct { + Sec Output[string] `pulumi:"sec"` + Unkn Output[bool] `pulumi:"unkn"` + Out Output[int] `pulumi:"out"` + Plain Output[string] `pulumi:"plain"` + } + + target := decodeTarget{} + enc, err := ende.Decode(p(), &target) + require.NoError(t, err) + + assert.Falsef(t, target.Unkn.resolvable, "unknown properties serialize to unresolvable types") + assert.True(t, target.Sec.resolvable, "secret properties serialize to knownable types") + assert.True(t, target.Sec.resolved, "secret properties serialize to known types") + assert.True(t, target.Sec.IsSecret(), "secret properties serialize to secret types") + + t.Run("derived outputs", func(t *testing.T) { + unkn := Apply(target.Unkn, func(bool) string { + assert.Fail(t, "Ran func on unknown value") + return "FAILED" + }) + assert.False(t, unkn.resolvable) + + kn := Apply(target.Sec, func(s string) bool { return s == "foo" }) + assert.True(t, kn.resolvable) + + combined := Apply2(target.Unkn, target.Sec, func(bool, string) int { + assert.Fail(t, "Ran func on unknown value") + return 0 + }) + assert.False(t, combined.resolvable) + + actual, err := ende.Encoder{}.Encode(struct { + Unkn Output[string] `pulumi:"unkn"` + Kn Output[bool] `pulumi:"kn"` + Comb Output[int] `pulumi:"comb"` + Pub Output[string] `pulumi:"pub"` + Sec Output[string] `pulumi:"asSec"` + }{unkn, kn, combined, target.Sec.AsPublic(), target.Plain.AsSecret()}) + require.NoError(t, err) + + assert.Equal(t, resource.PropertyMap{ + "unkn": resource.MakeComputed(resource.NewNullProperty()), + "kn": resource.MakeSecret(resource.NewBoolProperty(true)), + "comb": resource.NewOutputProperty(resource.Output{ + Secret: true, + Known: false, + }), + "pub": resource.NewStringProperty("foo"), + "asSec": resource.MakeSecret(resource.NewStringProperty("known and public")), + }, actual) + }) + + actual, err := enc.Encode(target) + require.NoError(t, err) + + assert.Equal(t, p(), actual) +} diff --git a/infer/schema.go b/infer/schema.go index 32b4fcd0..55129dbb 100644 --- a/infer/schema.go +++ b/infer/schema.go @@ -24,6 +24,7 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/pulumi/pulumi-go-provider/infer/internal/ende" "github.com/pulumi/pulumi-go-provider/internal/introspect" sch "github.com/pulumi/pulumi-go-provider/middleware/schema" ) @@ -199,6 +200,13 @@ func underlyingType(t reflect.Type) (reflect.Type, bool, error) { for t != nil && t.Kind() == reflect.Pointer { t = t.Elem() } + + var isInferOutput bool + if reflect.PtrTo(t).Implements(ende.EnDePropertyValueType) { + t = reflect.New(t).Interface().(ende.PropertyValue).UnderlyingSchemaType() + isInferOutput = true + } + isInputType := t.Implements(reflect.TypeOf(new(pulumi.Input)).Elem()) isOutputType := t.Implements(reflect.TypeOf(new(pulumi.Output)).Elem()) @@ -238,7 +246,7 @@ func underlyingType(t reflect.Type) (reflect.Type, bool, error) { for t != nil && t.Kind() == reflect.Pointer { t = t.Elem() } - return t, isOutputType || isInputType, nil + return t, isOutputType || isInputType || isInferOutput, nil } func propertyListFromType(typ reflect.Type, indicatePlain bool) (