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

Adjust documentation for Elem() #2187

Merged
merged 3 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions pf/tests/repr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// Copyright 2016-2024, 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 tfbridgetests

import (
"context"
"encoding/json"
"testing"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hexops/autogold/v2"
"github.com/pulumi/pulumi-terraform-bridge/pf/internal/schemashim"
pb "github.com/pulumi/pulumi-terraform-bridge/pf/tests/internal/providerbuilder"
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge"
"github.com/stretchr/testify/require"
)

// Test how various PF-based schemata translate to the shim.Schema layer.
func TestRepresentations(t *testing.T) {

type testCase struct {
name string
provider provider.Provider
expect autogold.Value
}

testCases := []testCase{
{
"single-nested-block",
&pb.Provider{
AllResources: []pb.Resource{{
ResourceSchema: schema.Schema{
Blocks: map[string]schema.Block{
"single_nested_block": schema.SingleNestedBlock{
Attributes: map[string]schema.Attribute{
"a1": schema.Float64Attribute{
Optional: true,
},
},
},
},
},
}},
},
autogold.Expect(`{
"resources": {
"_": {
"single_nested_block": {
"element": {
"resource": {
"a1": {
"optional": true,
"type": 3
}
}
},
"optional": true,
"type": 6
}
}
}
}`),
},
{
"list-nested-block",
&pb.Provider{
AllResources: []pb.Resource{{
ResourceSchema: schema.Schema{
Blocks: map[string]schema.Block{
"list_nested_block": schema.ListNestedBlock{
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"a1": schema.Float64Attribute{
Optional: true,
},
},
},
},
},
},
}},
},
autogold.Expect(`{
"resources": {
"_": {
"list_nested_block": {
"element": {
"resource": {
"a1": {
"optional": true,
"type": 3
}
}
},
"optional": true,
"type": 5
}
}
}
}`),
},
{
"map-nested-attribute",
&pb.Provider{
AllResources: []pb.Resource{{
ResourceSchema: schema.Schema{
Attributes: map[string]schema.Attribute{
"map_nested_attribute": schema.MapNestedAttribute{
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"a1": schema.StringAttribute{
Optional: true,
},
},
},
},
},
},
}},
},
autogold.Expect(`{
"resources": {
"_": {
"map_nested_attribute": {
"element": {
"schema": {
"element": {
"resource": {
"a1": {
"optional": true,
"type": 4
}
}
},
"type": 6
}
},
"type": 6
}
}
}
}`),
},
{
"object-attribute",
&pb.Provider{
AllResources: []pb.Resource{{
ResourceSchema: schema.Schema{
Attributes: map[string]schema.Attribute{
"object_attribute": schema.ObjectAttribute{
AttributeTypes: map[string]attr.Type{
"a1": types.StringType,
},
},
},
},
}},
},
autogold.Expect(`{
"resources": {
"_": {
"object_attribute": {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly enough this may look like a single-nested block to shim.Schema; it doesn't carry the distinction is it an attr or block in shim.Schema. May be allright for now but can cause some trouble in the corner cases perhaps.

"element": {
"resource": {
"a1": {
"type": 4
}
}
},
"type": 6
}
}
}
}`),
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
shimmedProvider := schemashim.ShimSchemaOnlyProvider(context.Background(), tc.provider)

m := tfbridge.MarshalProvider(shimmedProvider)
bytes, err := json.Marshal(m)
require.NoError(t, err)

var pretty map[string]any
err = json.Unmarshal(bytes, &pretty)
require.NoError(t, err)

prettyBytes, err := json.MarshalIndent(pretty, "", " ")
require.NoError(t, err)

tc.expect.Equal(t, string(prettyBytes))
})
}
}
18 changes: 11 additions & 7 deletions pkg/tfshim/shim.go
t0yv0 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,16 @@ type Schema interface {

// s.Elem() may return a nil, a Schema value, or a Resource value.
//
// The design of Elem() follows Terraform Plugin SDK directly. Case analysis:
//
// Case 1: s represents a compound type (s.Type() is one of TypeList, TypeSet or TypeMap), and s.Elem()
// represents the element of this type as a Schema value. That is, if s ~ List[String] then s.Elem() ~ String.
//
// Case 2: s represents a single-nested Terraform block. Logically this is like s having an anonymous object
// type such as s ~ {"x": Int, "y": String}. In this case s.Type() == TypeMap and s.Elem() is a Resource value.
// This value is not a real Resource and only implements the Schema field to enable inspecting s.Elem().Schema()
// to find out the names ("x", "y") and types (Int, String) of the block properties.
// This s.Elem() value is not a real Resource and only implements the Schema field to enable inspecting
// s.Elem().Schema() to find out the names ("x", "y") and types (Int, String) of the block properties. An
// additional distinction is that SDKv2 providers cannot represent single-nested blocks at all, so this case is
// only used for Plugin Framework providers. What SDKv2 providers typically do instead is declare a List-nested
// block with MaxItems=1.
//
// Case 3: s represents a list or set-nested Terraform block. That is, s ~ List[{"x": Int, "y": String}]. In
// this case s.Type() is one of TypeList, TypeSet, and s.Elem() is a Resource that encodes the object type
Expand All @@ -123,11 +124,14 @@ type Schema interface {
//
// Case 5: s.Elem() is nil but s.Type() is one of TypeList, TypeSet, TypeMap. The element type is unknown.
//
// This encoding cannot support map-nested blocks or object types as it would introduce confusion with Case 2,
// because Map[String, {"x": Int}] and {"x": Int} both have s.Type() = TypeMap and s.Elem() being a Resource.
// Following the Terraform design, only set and list-nested blocks are supported.
// This encoding cannot support map-nested blocks but it does not need to as those are not expressible in TF.
//
// See also: https://github.com/hashicorp/terraform-plugin-sdk/blob/main/helper/schema/schema.go#L231
//
// There is the additional complication for SDKv2-based providers, case 2 should not be used but is tolerated
// for backwards-compatibility per [2], but is interpreted to mean a string-string map.
//
// [2]: https://github.com/hashicorp/terraform-plugin-sdk/blob/main/helper/schema/core_schema_test.go#L220
Elem() interface{}

MaxItems() int
Expand Down
Loading