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 all 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/schemashim_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 TestSchemaShimRepresentations(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": {
"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))
})
}
}
21 changes: 12 additions & 9 deletions pkg/tfshim/shim.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,18 @@ type Schema interface {
ForceNew() bool
StateFunc() SchemaStateFunc

// s.Elem() may return a nil, a Schema value, or a Resource value.
//
// The design of Elem() follows Terraform Plugin SDK directly. Case analysis:
// s.Elem() may return a nil, a Schema value, or a Resource value [1].
//
// 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. SDKv2
// providers cannot represent single-nested blocks; this case is only used for Plugin Framework providers. SDKv2
// providers use a convention to declare a List-nested block with MaxItems=1 to model object types. Per [2]
// SDKv2 providers reinterpret case 2 as a string-string map for backwards compatibility.
//
// 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,13 @@ 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.
//
// A test suite [3] is provided to explore how Plugin Framework constructs map to Schema.
//
// See also: https://github.com/hashicorp/terraform-plugin-sdk/blob/main/helper/schema/schema.go#L231
// [1]: https://github.com/hashicorp/terraform-plugin-sdk/blob/main/helper/schema/schema.go#L231
// [2]: https://github.com/hashicorp/terraform-plugin-sdk/blob/main/helper/schema/core_schema_test.go#L220
// [3]: https://github.com/pulumi/pulumi-terraform-bridge/blob/master/pf/tests/schemashim_test.go#L34
Elem() interface{}

MaxItems() int
Expand Down