pulumi/pkg/importer/hcl2_test.go

942 lines
29 KiB
Go

// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package importer
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
"github.com/pulumi/pulumi/pkg/v3/codegen/pcl"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/pkg/v3/codegen/testing/utils"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
"github.com/pulumi/pulumi/pkg/v3/resource/stack"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/stretchr/testify/assert"
"github.com/zclconf/go-cty/cty"
)
var testdataPath = filepath.Join("..", "codegen", "testing", "test", "testdata")
const (
parentName = "parent"
providerName = "provider"
logicalName = "logical"
)
var (
parentURN = resource.NewURN("stack", "project", "", "my::parent", "parent")
providerURN = resource.NewURN("stack", "project", "", providers.MakeProviderType("pkg"), "provider")
logicalURN = resource.NewURN("stack", "project", "", "random:index/randomId:RandomId", "strange logical name")
)
var names = NameTable{
parentURN: parentName,
providerURN: providerName,
logicalURN: logicalName,
}
func renderExpr(t *testing.T, x model.Expression) resource.PropertyValue {
switch x := x.(type) {
case *model.LiteralValueExpression:
return renderLiteralValue(t, x)
case *model.ScopeTraversalExpression:
return renderScopeTraversal(t, x)
case *model.TemplateExpression:
return renderTemplate(t, x)
case *model.TupleConsExpression:
return renderTupleCons(t, x)
case *model.ObjectConsExpression:
return renderObjectCons(t, x)
case *model.FunctionCallExpression:
return renderFunctionCall(t, x)
default:
assert.Failf(t, "", "unexpected expression of type %T", x)
return resource.NewNullProperty()
}
}
func renderLiteralValue(t *testing.T, x *model.LiteralValueExpression) resource.PropertyValue {
switch x.Value.Type() {
case cty.Bool:
return resource.NewBoolProperty(x.Value.True())
case cty.Number:
f, _ := x.Value.AsBigFloat().Float64()
return resource.NewNumberProperty(f)
case cty.String:
return resource.NewStringProperty(x.Value.AsString())
default:
assert.Failf(t, "", "unexpected literal of type %v", x.Value.Type())
return resource.NewNullProperty()
}
}
func renderTemplate(t *testing.T, x *model.TemplateExpression) resource.PropertyValue {
if len(x.Parts) == 1 {
return renderLiteralValue(t, x.Parts[0].(*model.LiteralValueExpression))
}
b := ""
for _, p := range x.Parts {
b += p.(*model.LiteralValueExpression).Value.AsString()
}
return resource.NewStringProperty(b)
}
func renderObjectCons(t *testing.T, x *model.ObjectConsExpression) resource.PropertyValue {
obj := resource.PropertyMap{}
for _, item := range x.Items {
kv := renderExpr(t, item.Key)
if !assert.True(t, kv.IsString()) {
continue
}
obj[resource.PropertyKey(kv.StringValue())] = renderExpr(t, item.Value)
}
return resource.NewObjectProperty(obj)
}
func renderScopeTraversal(t *testing.T, x *model.ScopeTraversalExpression) resource.PropertyValue {
if !assert.Len(t, x.Traversal, 1) {
return resource.NewNullProperty()
}
switch x.RootName {
case "parent":
return resource.NewStringProperty(string(parentURN))
case "provider":
return resource.NewStringProperty(string(providerURN))
default:
assert.Failf(t, "", "unexpected variable reference %v", x.RootName)
return resource.NewNullProperty()
}
}
func renderTupleCons(t *testing.T, x *model.TupleConsExpression) resource.PropertyValue {
arr := make([]resource.PropertyValue, len(x.Expressions))
for i, x := range x.Expressions {
arr[i] = renderExpr(t, x)
}
return resource.NewArrayProperty(arr)
}
func renderFunctionCall(t *testing.T, x *model.FunctionCallExpression) resource.PropertyValue {
switch x.Name {
case "fileArchive":
if !assert.Len(t, x.Args, 1) {
return resource.NewNullProperty()
}
expr := renderExpr(t, x.Args[0])
if !assert.True(t, expr.IsString()) {
return resource.NewNullProperty()
}
return resource.NewStringProperty(expr.StringValue())
case "fileAsset":
if !assert.Len(t, x.Args, 1) {
return resource.NewNullProperty()
}
expr := renderExpr(t, x.Args[0])
if !assert.True(t, expr.IsString()) {
return resource.NewNullProperty()
}
return resource.NewStringProperty(expr.StringValue())
case "secret":
if !assert.Len(t, x.Args, 1) {
return resource.NewNullProperty()
}
return resource.MakeSecret(renderExpr(t, x.Args[0]))
default:
assert.Failf(t, "", "unexpected call to %v", x.Name)
return resource.NewNullProperty()
}
}
func renderResource(t *testing.T, r *pcl.Resource) *resource.State {
inputs := resource.PropertyMap{}
for _, attr := range r.Inputs {
inputs[resource.PropertyKey(attr.Name)] = renderExpr(t, attr.Value)
}
protect := false
var parent resource.URN
var providerRef string
if r.Options != nil {
if r.Options.Protect != nil {
v, diags := r.Options.Protect.Evaluate(&hcl.EvalContext{})
if assert.Len(t, diags, 0) && assert.Equal(t, cty.Bool, v.Type()) {
protect = v.True()
}
}
if r.Options.Parent != nil {
v := renderExpr(t, r.Options.Parent)
if assert.True(t, v.IsString()) {
parent = resource.URN(v.StringValue())
}
}
if r.Options.Provider != nil {
v := renderExpr(t, r.Options.Provider)
if assert.True(t, v.IsString()) {
providerRef = v.StringValue() + "::id"
}
}
}
// Pull the raw token from the resource.
token := tokens.Type(r.Definition.Labels[1])
var parentType tokens.Type
if parent != "" {
parentType = parent.QualifiedType()
}
return &resource.State{
Type: token,
URN: resource.NewURN("stack", "project", parentType, token, r.LogicalName()),
Custom: true,
Inputs: inputs,
Parent: parent,
Provider: providerRef,
Protect: protect,
}
}
type testCases struct {
Resources []apitype.ResourceV3 `json:"resources"`
}
func readTestCases(path string) (testCases, error) {
f, err := os.Open(path)
if err != nil {
return testCases{}, err
}
defer contract.IgnoreClose(f)
var cases testCases
if err = json.NewDecoder(f).Decode(&cases); err != nil {
return testCases{}, err
}
return cases, nil
}
func TestGenerateHCL2Definition(t *testing.T) {
t.Parallel()
loader := schema.NewPluginLoader(utils.NewHost(testdataPath))
cases, err := readTestCases("testdata/cases.json")
if !assert.NoError(t, err) {
t.Fatal()
}
//nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg
for _, s := range cases.Resources {
s := s
t.Run(string(s.URN), func(t *testing.T) {
state, err := stack.DeserializeResource(s, config.NopDecrypter, config.NopEncrypter)
if !assert.NoError(t, err) {
t.Fatal()
}
snapshot := []*resource.State{
{
ID: "123",
Custom: true,
Type: "pulumi:providers:aws",
URN: "urn:pulumi:stack::project::pulumi:providers:aws::default_123",
},
{
ID: "123",
Custom: true,
Type: "pulumi:providers:random",
URN: "urn:pulumi:stack::project::pulumi:providers:random::default_123",
},
{
ID: "id",
Custom: true,
Type: "pulumi:providers:pkg",
URN: "urn:pulumi:stack::project::pulumi:providers:pkg::provider",
},
}
importState := ImportState{
Names: names,
Snapshot: snapshot,
}
block, _, err := GenerateHCL2Definition(loader, state, importState)
if !assert.NoError(t, err) {
t.Fatal()
}
text := fmt.Sprintf("%v", block)
parser := syntax.NewParser()
err = parser.ParseFile(strings.NewReader(text), string(state.URN)+".pp")
if !assert.NoError(t, err) || !assert.False(t, parser.Diagnostics.HasErrors()) {
t.Fatal()
}
p, diags, err := pcl.BindProgram(parser.Files, pcl.Loader(loader), pcl.AllowMissingVariables)
assert.NoError(t, err)
assert.False(t, diags.HasErrors())
if !assert.Len(t, p.Nodes, 1) {
t.Fatal()
}
res, isResource := p.Nodes[0].(*pcl.Resource)
if !assert.True(t, isResource) {
t.Fatal()
}
actualState := renderResource(t, res)
assert.Equal(t, state.Type, actualState.Type)
assert.Equal(t, state.URN, actualState.URN)
assert.Equal(t, state.Parent, actualState.Parent)
if !strings.Contains(state.Provider, "::default_") {
assert.Equal(t, state.Provider, actualState.Provider)
}
assert.Equal(t, state.Protect, actualState.Protect)
if !assert.True(t, actualState.Inputs.DeepEquals(state.Inputs)) {
actual, err := stack.SerializeResource(context.Background(), actualState, config.NopEncrypter, false)
contract.IgnoreError(err)
sb, err := json.MarshalIndent(s, "", " ")
contract.IgnoreError(err)
ab, err := json.MarshalIndent(actual, "", " ")
contract.IgnoreError(err)
t.Logf("%v", text)
// We know this will fail, but we want the diff
assert.Equal(t, string(sb), string(ab))
}
})
}
}
func TestGenerateHCL2DefinitionsWithDependantResources(t *testing.T) {
t.Parallel()
loader := schema.NewPluginLoader(utils.NewHost(testdataPath))
snapshot := []*resource.State{
{
ID: "123",
Custom: true,
Type: "pulumi:providers:aws",
URN: "urn:pulumi:stack::project::pulumi:providers:aws::default_123",
},
}
resources := []apitype.ResourceV3{
{
URN: "urn:pulumi:stack::project::aws:s3/bucket:Bucket::exampleBucket",
ID: "provider-generated-bucket-id-abc123",
Custom: true,
Type: "aws:s3/bucket:Bucket",
Provider: fmt.Sprintf("%s::%s", snapshot[0].URN, snapshot[0].ID),
},
{
URN: "urn:pulumi:stack::project::aws:s3/bucketObject:BucketObject::exampleBucketObject",
ID: "provider-generated-bucket-object-id-abc123",
Custom: true,
Type: "aws:s3/bucketObject:BucketObject",
Inputs: map[string]interface{}{
// this will be replaced with a reference to exampleBucket.id in the generated code
"bucket": "provider-generated-bucket-id-abc123",
"storageClass": "STANDARD",
},
Provider: fmt.Sprintf("%s::%s", snapshot[0].URN, snapshot[0].ID),
},
}
states := make([]*resource.State, 0)
for _, r := range resources {
state, err := stack.DeserializeResource(r, config.NopDecrypter, config.NopEncrypter)
if !assert.NoError(t, err) {
t.Fatal()
}
states = append(states, state)
}
importState := createImportState(states, snapshot, names)
var hcl2Text strings.Builder
for i, state := range states {
hcl2Def, _, err := GenerateHCL2Definition(loader, state, importState)
if err != nil {
t.Fatal(err)
}
pre := ""
if i > 0 {
pre = "\n"
}
_, err = fmt.Fprintf(&hcl2Text, "%s%v", pre, hcl2Def)
contract.IgnoreError(err)
}
expectedCode := `resource exampleBucket "aws:s3/bucket:Bucket" {
}
resource exampleBucketObject "aws:s3/bucketObject:BucketObject" {
bucket = exampleBucket.id
storageClass = "STANDARD"
}
`
assert.Equal(t, expectedCode, hcl2Text.String(), "Generated HCL2 code does not match expected code")
}
func TestGenerateHCL2DefinitionsWithDependantResourcesUsingNameOrArnProperty(t *testing.T) {
t.Parallel()
loader := schema.NewPluginLoader(utils.NewHost(testdataPath))
snapshot := []*resource.State{
{
ID: "123",
Custom: true,
Type: "pulumi:providers:aws",
URN: "urn:pulumi:stack::project::pulumi:providers:aws::default_123",
},
}
resources := []apitype.ResourceV3{
{
Provider: fmt.Sprintf("%s::%s", snapshot[0].URN, snapshot[0].ID),
URN: "urn:pulumi:stack::project::aws:s3/bucket:Bucket::exampleBucket",
ID: "provider-generated-bucket-id-abc123",
Custom: true,
Type: "aws:s3/bucket:Bucket",
Outputs: map[string]interface{}{
"name": "bucketName-12345",
"arn": "arn:aws:s3:bucket-12345",
},
},
{
Provider: fmt.Sprintf("%s::%s", snapshot[0].URN, snapshot[0].ID),
URN: "urn:pulumi:stack::project::aws:s3/bucketObject:BucketObject::exampleBucketObject",
ID: "provider-generated-bucket-object-id-abc123",
Custom: true,
Type: "aws:s3/bucketObject:BucketObject",
Inputs: map[string]interface{}{
// this will be replaced with a reference to exampleBucket.name in the generated code
"bucket": "bucketName-12345",
"storageClass": "STANDARD",
},
},
{
Provider: fmt.Sprintf("%s::%s", snapshot[0].URN, snapshot[0].ID),
URN: "urn:pulumi:stack::project::aws:s3/bucketObject:BucketObject::exampleBucketObjectUsingArn",
ID: "provider-generated-bucket-object-id-abc123",
Custom: true,
Type: "aws:s3/bucketObject:BucketObject",
Inputs: map[string]interface{}{
// this will be replaced with a reference to exampleBucket.arn in the generated code
"bucket": "arn:aws:s3:bucket-12345",
"storageClass": "STANDARD",
},
},
}
states := make([]*resource.State, 0)
for _, r := range resources {
state, err := stack.DeserializeResource(r, config.NopDecrypter, config.NopEncrypter)
if !assert.NoError(t, err) {
t.Fatal()
}
states = append(states, state)
}
importState := createImportState(states, snapshot, names)
var hcl2Text strings.Builder
for i, state := range states {
hcl2Def, _, err := GenerateHCL2Definition(loader, state, importState)
if err != nil {
t.Fatal(err)
}
pre := ""
if i > 0 {
pre = "\n"
}
_, err = fmt.Fprintf(&hcl2Text, "%s%v", pre, hcl2Def)
contract.IgnoreError(err)
}
expectedCode := `resource exampleBucket "aws:s3/bucket:Bucket" {
}
resource exampleBucketObject "aws:s3/bucketObject:BucketObject" {
bucket = exampleBucket.name
storageClass = "STANDARD"
}
resource exampleBucketObjectUsingArn "aws:s3/bucketObject:BucketObject" {
bucket = exampleBucket.arn
storageClass = "STANDARD"
}
`
assert.Equal(t, expectedCode, hcl2Text.String(), "Generated HCL2 code does not match expected code")
}
func TestGenerateHCL2DefinitionsWithAmbiguousReferencesMaintainsLiteralValue(t *testing.T) {
t.Parallel()
loader := schema.NewPluginLoader(utils.NewHost(testdataPath))
snapshot := []*resource.State{
{
ID: "123",
Custom: true,
Type: "pulumi:providers:aws",
URN: "urn:pulumi:stack::project::pulumi:providers:aws::default_123",
},
}
resources := []apitype.ResourceV3{
{
Provider: fmt.Sprintf("%s::%s", snapshot[0].URN, snapshot[0].ID),
URN: "urn:pulumi:stack::project::aws:s3/bucket:Bucket::firstBucket",
ID: "provider-generated-bucket-id-abc123",
Custom: true,
Type: "aws:s3/bucket:Bucket",
},
{
Provider: fmt.Sprintf("%s::%s", snapshot[0].URN, snapshot[0].ID),
URN: "urn:pulumi:stack::project::aws:s3/bucket:Bucket::secondBucket",
ID: "provider-generated-bucket-id-abc123",
Custom: true,
Type: "aws:s3/bucket:Bucket",
},
{
Provider: fmt.Sprintf("%s::%s", snapshot[0].URN, snapshot[0].ID),
URN: "urn:pulumi:stack::project::aws:s3/bucketObject:BucketObject::exampleBucketObject",
ID: "provider-generated-bucket-object-id-abc123",
Custom: true,
Type: "aws:s3/bucketObject:BucketObject",
Inputs: map[string]interface{}{
// this will *NOT* be replaced with a reference to either firstBucket.id or secondBucket.id
// because both have the same ID and it would be ambiguous
"bucket": "provider-generated-bucket-id-abc123",
"storageClass": "STANDARD",
},
},
}
states := make([]*resource.State, 0)
for _, r := range resources {
state, err := stack.DeserializeResource(r, config.NopDecrypter, config.NopEncrypter)
if !assert.NoError(t, err) {
t.Fatal()
}
states = append(states, state)
}
importState := createImportState(states, snapshot, names)
var hcl2Text strings.Builder
for i, state := range states {
hcl2Def, _, err := GenerateHCL2Definition(loader, state, importState)
if err != nil {
t.Fatal(err)
}
pre := ""
if i > 0 {
pre = "\n"
}
_, err = fmt.Fprintf(&hcl2Text, "%s%v", pre, hcl2Def)
contract.IgnoreError(err)
}
expectedCode := `resource firstBucket "aws:s3/bucket:Bucket" {
}
resource secondBucket "aws:s3/bucket:Bucket" {
}
resource exampleBucketObject "aws:s3/bucketObject:BucketObject" {
bucket = "provider-generated-bucket-id-abc123"
storageClass = "STANDARD"
}
`
assert.Equal(t, expectedCode, hcl2Text.String(), "Generated HCL2 code does not match expected code")
}
func TestGenerateHCL2DefinitionsDoesNotMakeSelfReferences(t *testing.T) {
t.Parallel()
loader := schema.NewPluginLoader(utils.NewHost(testdataPath))
snapshot := []*resource.State{
{
ID: "123",
Custom: true,
Type: "pulumi:providers:aws",
URN: "urn:pulumi:stack::project::pulumi:providers:aws::default_123",
},
}
resources := []apitype.ResourceV3{
{
Provider: fmt.Sprintf("%s::%s", snapshot[0].URN, snapshot[0].ID),
URN: "urn:pulumi:stack::project::aws:s3/bucketObject:BucketObject::exampleBucketObject",
ID: "provider-generated-bucket-object-id-abc123",
Custom: true,
Type: "aws:s3/bucketObject:BucketObject",
Inputs: map[string]interface{}{
// this literal value will stay as is since it shouldn't self-reference the bucket object itself
"bucket": "provider-generated-bucket-object-id-abc123",
"storageClass": "STANDARD",
},
},
}
states := make([]*resource.State, 0)
for _, r := range resources {
state, err := stack.DeserializeResource(r, config.NopDecrypter, config.NopEncrypter)
if !assert.NoError(t, err) {
t.Fatal()
}
states = append(states, state)
}
importState := createImportState(states, snapshot, names)
var hcl2Text strings.Builder
for i, state := range states {
hcl2Def, _, err := GenerateHCL2Definition(loader, state, importState)
if err != nil {
t.Fatal(err)
}
pre := ""
if i > 0 {
pre = "\n"
}
_, err = fmt.Fprintf(&hcl2Text, "%s%v", pre, hcl2Def)
contract.IgnoreError(err)
}
expectedCode := `resource exampleBucketObject "aws:s3/bucketObject:BucketObject" {
bucket = "provider-generated-bucket-object-id-abc123"
storageClass = "STANDARD"
}
`
assert.Equal(t, expectedCode, hcl2Text.String(), "Generated HCL2 code does not match expected code")
}
func TestSimplerType(t *testing.T) {
t.Parallel()
types := []schema.Type{
schema.BoolType,
schema.IntType,
schema.NumberType,
schema.StringType,
schema.AssetType,
schema.ArchiveType,
schema.JSONType,
&schema.ArrayType{ElementType: schema.BoolType},
&schema.ArrayType{ElementType: schema.IntType},
&schema.MapType{ElementType: schema.BoolType},
&schema.MapType{ElementType: schema.IntType},
&schema.ObjectType{},
&schema.ObjectType{
Properties: []*schema.Property{
{
Name: "foo",
Type: schema.BoolType,
},
},
},
&schema.ObjectType{
Properties: []*schema.Property{
{
Name: "foo",
Type: schema.IntType,
},
},
},
&schema.ObjectType{
Properties: []*schema.Property{
{
Name: "foo",
Type: schema.IntType,
},
{
Name: "bar",
Type: schema.IntType,
},
},
},
&schema.UnionType{ElementTypes: []schema.Type{schema.BoolType, schema.IntType}},
&schema.UnionType{ElementTypes: []schema.Type{schema.IntType, schema.JSONType}},
&schema.UnionType{ElementTypes: []schema.Type{schema.NumberType, schema.StringType}},
schema.AnyType,
}
assert.True(t, sort.SliceIsSorted(types, func(i, j int) bool {
return simplerType(types[i], types[j])
}))
}
func makeUnionType(types ...schema.Type) *schema.UnionType {
return &schema.UnionType{ElementTypes: types}
}
func makeArrayType(elementType schema.Type) *schema.ArrayType {
return &schema.ArrayType{ElementType: elementType}
}
func makeProperty(name string, t schema.Type) *schema.Property {
return &schema.Property{Name: name, Type: t}
}
func makeObjectType(properties ...*schema.Property) *schema.ObjectType {
return &schema.ObjectType{Properties: properties}
}
func makeOptionalType(t schema.Type) schema.Type {
return &schema.OptionalType{ElementType: t}
}
func makeObject(input map[string]resource.PropertyValue) resource.PropertyValue {
properties := make(map[resource.PropertyKey]resource.PropertyValue)
for key, value := range input {
properties[resource.PropertyKey(key)] = value
}
return resource.NewObjectProperty(properties)
}
func TestStructuralTypeChecks(t *testing.T) {
t.Run("String", func(t *testing.T) {
t.Parallel()
value := resource.NewStringProperty("foo")
assert.True(t, valueStructurallyTypedAs(value, schema.StringType))
assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.StringType)))
assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.StringType, schema.NumberType)))
assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.NumberType, schema.StringType)))
assert.False(t, valueStructurallyTypedAs(value, schema.BoolType))
assert.False(t, valueStructurallyTypedAs(value, schema.NumberType))
assert.False(t, valueStructurallyTypedAs(value, makeUnionType(schema.BoolType, schema.NumberType)))
})
t.Run("Bool", func(t *testing.T) {
t.Parallel()
value := resource.NewBoolProperty(true)
assert.True(t, valueStructurallyTypedAs(value, schema.BoolType))
assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.BoolType)))
assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.BoolType, schema.NumberType)))
assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.NumberType, schema.BoolType)))
assert.False(t, valueStructurallyTypedAs(value, schema.StringType))
assert.False(t, valueStructurallyTypedAs(value, schema.NumberType))
assert.False(t, valueStructurallyTypedAs(value, makeUnionType(schema.StringType, schema.NumberType)))
})
t.Run("Number", func(t *testing.T) {
t.Parallel()
value := resource.NewNumberProperty(42)
assert.True(t, valueStructurallyTypedAs(value, schema.NumberType))
assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.NumberType)))
assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.NumberType, schema.StringType)))
assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.StringType, schema.NumberType)))
assert.False(t, valueStructurallyTypedAs(value, schema.StringType))
assert.False(t, valueStructurallyTypedAs(value, schema.BoolType))
assert.False(t, valueStructurallyTypedAs(value, makeUnionType(schema.StringType, schema.BoolType)))
})
t.Run("Array", func(t *testing.T) {
t.Parallel()
value := resource.NewArrayProperty([]resource.PropertyValue{
resource.NewStringProperty("foo"),
resource.NewStringProperty("bar"),
})
assert.True(t, valueStructurallyTypedAs(value, makeArrayType(schema.StringType)))
assert.True(t, valueStructurallyTypedAs(value, makeUnionType(makeArrayType(schema.StringType))))
assert.True(t, valueStructurallyTypedAs(value, makeUnionType(makeArrayType(schema.StringType), schema.NumberType)))
assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.NumberType, makeArrayType(schema.StringType))))
assert.True(t, valueStructurallyTypedAs(value, makeUnionType(
makeArrayType(schema.StringType),
makeArrayType(schema.NumberType))))
assert.True(t, valueStructurallyTypedAs(value, makeUnionType(
makeArrayType(schema.NumberType),
makeArrayType(schema.StringType))))
assert.False(t, valueStructurallyTypedAs(value, makeArrayType(schema.BoolType)))
assert.False(t, valueStructurallyTypedAs(value, makeArrayType(schema.NumberType)))
assert.False(t, valueStructurallyTypedAs(value, makeUnionType(makeArrayType(schema.BoolType), schema.NumberType)))
assert.False(t, valueStructurallyTypedAs(value, makeUnionType(schema.NumberType, makeArrayType(schema.BoolType))))
})
t.Run("ArrayMixedTypes", func(t *testing.T) {
t.Parallel()
value := resource.NewArrayProperty([]resource.PropertyValue{
resource.NewStringProperty("foo"),
resource.NewNumberProperty(42),
})
// base case: value of type array[union[string, number]]
assert.True(t, valueStructurallyTypedAs(value, makeArrayType(makeUnionType(schema.StringType, schema.NumberType))))
assert.False(t, valueStructurallyTypedAs(value, makeArrayType(schema.NumberType)))
assert.False(t, valueStructurallyTypedAs(value, makeUnionType(makeArrayType(schema.StringType))))
assert.False(t, valueStructurallyTypedAs(value, makeUnionType(makeArrayType(schema.NumberType))))
assert.False(t, valueStructurallyTypedAs(value, makeUnionType(makeArrayType(schema.StringType), schema.NumberType)))
assert.False(t, valueStructurallyTypedAs(value, makeUnionType(schema.NumberType, makeArrayType(schema.StringType))))
})
t.Run("Object", func(t *testing.T) {
t.Parallel()
value := makeObject(map[string]resource.PropertyValue{
"foo": resource.NewStringProperty("foo"),
"bar": resource.NewNumberProperty(42),
})
assert.True(t, valueStructurallyTypedAs(value, makeObjectType(
makeProperty("foo", schema.StringType),
makeProperty("bar", schema.NumberType),
)))
assert.False(t, valueStructurallyTypedAs(value, makeObjectType(
makeProperty("foo", schema.StringType),
makeProperty("bar", schema.StringType),
)))
anotherValue := makeObject(map[string]resource.PropertyValue{
"a": resource.NewStringProperty("A"),
})
// property "a" is missing from the type
assert.False(t, valueStructurallyTypedAs(anotherValue, makeObjectType(
makeProperty("b", schema.StringType),
)))
objectA := makeObject(map[string]resource.PropertyValue{
"foo": resource.NewStringProperty("foo"),
})
objectATypeWithRequiredPropertyBar := makeObjectType(
makeProperty("foo", schema.StringType),
makeProperty("bar", schema.NumberType))
// property "bar" is missing from the value
// but the type requires it to be present
// so the value is _not_ structurally typed
assert.False(t, valueStructurallyTypedAs(objectA, objectATypeWithRequiredPropertyBar))
objectATypeWithOptionalPropertyBar := makeObjectType(
makeProperty("foo", schema.StringType),
makeProperty("bar", makeOptionalType(schema.NumberType)))
// property "bar" is missing from the value, but it is optional
// so the value is structurally typed just fine
assert.True(t, valueStructurallyTypedAs(objectA, objectATypeWithOptionalPropertyBar))
complexUnionOfObjects := makeUnionType(
makeObjectType(
makeProperty("foo", schema.StringType),
makeProperty("bar", schema.NumberType),
),
makeObjectType(
makeProperty("foo", makeUnionType(schema.NumberType, schema.StringType)),
))
// fits the second object of the union
complexFittingValue := makeObject(map[string]resource.PropertyValue{
"foo": resource.NewNumberProperty(100),
})
assert.True(t, valueStructurallyTypedAs(complexFittingValue, complexUnionOfObjects))
})
}
func TestReduceUnionTypeEliminatesUnionsBasicCase(t *testing.T) {
t.Parallel()
value := resource.NewStringProperty("hello")
unionTypeA := makeUnionType(schema.StringType, schema.NumberType)
unionTypeB := makeUnionType(schema.BoolType, schema.StringType)
reducedA := reduceUnionType(unionTypeA, value)
reducedB := reduceUnionType(unionTypeB, value)
assert.Equal(t, schema.StringType, reducedA)
assert.Equal(t, schema.StringType, reducedB)
}
func TestReduceUnionTypeEliminatesUnionsRecursively(t *testing.T) {
t.Parallel()
value := resource.NewStringProperty("hello")
unionType := makeUnionType(
makeUnionType(schema.NumberType, schema.BoolType),
makeUnionType(
makeUnionType(
schema.StringType,
schema.BoolType)))
reduced := reduceUnionType(unionType, value)
assert.Equal(t, schema.StringType, reduced)
}
func TestReduceUnionTypeWorksWithArrayOfUnions(t *testing.T) {
t.Parallel()
// array[union[string, number]]
mixedTypeArray := resource.NewArrayProperty([]resource.PropertyValue{
resource.NewStringProperty("hello"),
resource.NewNumberProperty(42),
})
// union[array[union[string, number]]]
arrayType := makeUnionType(&schema.ArrayType{
ElementType: makeUnionType(schema.StringType, schema.NumberType),
})
reduced := reduceUnionType(arrayType, mixedTypeArray)
schemaArrayType, isArray := reduced.(*schema.ArrayType)
assert.True(t, isArray)
unionType, isUnion := schemaArrayType.ElementType.(*schema.UnionType)
assert.True(t, isUnion)
assert.Equal(t, schema.StringType, unionType.ElementTypes[0])
assert.Equal(t, schema.NumberType, unionType.ElementTypes[1])
}