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

Add code generation for collection and object attributes with associated external type #75

Merged
merged 13 commits into from
Oct 25, 2023

Conversation

bendbennett
Copy link
Contributor

@bendbennett bendbennett commented Oct 23, 2023

Closes: #74

The following is illustrative for the handling of object attributes with associated external types. Analogous changes have been made for the handling of list, map, and set attributes with associated external types.

Given the following spec:

{
  "datasources": [
    {
      "name": "datasource",
      "schema": {
        "attributes": [
          {
            "name": "object_attribute",
            "object": {
              "associated_external_type": {
                "import": {
                  "path": "github.com/api"
                },
                "type": "*api.ObjectAttribute"
              },
              "computed_optional_required": "required",
              "attribute_types": [
                {
                  "name": "bool",
                  "bool": {}
                },
                {
                  "name": "float64",
                  "float64": {}
                },
                {
                  "name": "int64",
                  "int64": {}
                },
                {
                  "name": "number",
                  "number": {}
                },
                {
                  "name": "string",
                  "string": {}
                }
              ]
            }
          }
        ]
      }
    }
  ],
  "provider": {
    "name": "provider"
  },
  "version": "0.1"
}

Prior to the changes in this PR, the generated code would have looked as follows:

package datasource_datasource

import (
	"context"
	"github.com/hashicorp/terraform-plugin-framework/attr"
	"github.com/hashicorp/terraform-plugin-framework/types"

	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
)

func DatasourceDataSourceSchema(ctx context.Context) schema.Schema {
	return schema.Schema{
		Attributes: map[string]schema.Attribute{
			"object_attribute": schema.ObjectAttribute{
				AttributeTypes: map[string]attr.Type{
					"bool":    types.BoolType,
					"float64": types.Float64Type,
					"int64":   types.Int64Type,
					"number":  types.NumberType,
					"string":  types.StringType,
				},
				Required: true,
			},
		},
	}
}

type DatasourceModel struct {
	ObjectAttribute types.Object `tfsdk:"object_attribute"`
}

With the changes in this PR the generated code is now as follows:

package datasource_datasource

import (
	"context"
	"fmt"
	"github.com/api"
	"github.com/hashicorp/terraform-plugin-framework/attr"
	"github.com/hashicorp/terraform-plugin-framework/diag"
	"github.com/hashicorp/terraform-plugin-framework/types"
	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
	"github.com/hashicorp/terraform-plugin-go/tftypes"

	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
)

func DatasourceDataSourceSchema(ctx context.Context) schema.Schema {
	return schema.Schema{
		Attributes: map[string]schema.Attribute{
			"object_attribute": schema.ObjectAttribute{
				CustomType: ObjectAttributeType{
					types.ObjectType{
						AttrTypes: ObjectAttributeValue{}.AttributeTypes(ctx),
					},
				},
				Required: true,
			},
		},
	}
}

type DatasourceModel struct {
	ObjectAttribute ObjectAttributeValue `tfsdk:"object_attribute"`
}

var _ basetypes.ObjectTypable = ObjectAttributeType{}

type ObjectAttributeType struct {
	basetypes.ObjectType
}

func (t ObjectAttributeType) Equal(o attr.Type) bool {
	other, ok := o.(ObjectAttributeType)

	if !ok {
		return false
	}

	return t.ObjectType.Equal(other.ObjectType)
}

func (t ObjectAttributeType) String() string {
	return "ObjectAttributeType"
}

func (t ObjectAttributeType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
	return ObjectAttributeValue{
		ObjectValue: in,
	}, nil
}

func (t ObjectAttributeType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
	attrValue, err := t.ObjectType.ValueFromTerraform(ctx, in)

	if err != nil {
		return nil, err
	}

	objectValue, ok := attrValue.(basetypes.ObjectValue)

	if !ok {
		return nil, fmt.Errorf("unexpected value type of %T", attrValue)
	}

	objectValuable, diags := t.ValueFromObject(ctx, objectValue)

	if diags.HasError() {
		return nil, fmt.Errorf("unexpected error converting ObjectValue to ObjectValuable: %v", diags)
	}

	return objectValuable, nil
}

func (t ObjectAttributeType) ValueType(ctx context.Context) attr.Value {
	return ObjectAttributeValue{}
}

var _ basetypes.ObjectValuable = ObjectAttributeValue{}

type ObjectAttributeValue struct {
	basetypes.ObjectValue
}

func (v ObjectAttributeValue) Equal(o attr.Value) bool {
	other, ok := o.(ObjectAttributeValue)

	if !ok {
		return false
	}

	return v.ObjectValue.Equal(other.ObjectValue)
}

func (v ObjectAttributeValue) Type(ctx context.Context) attr.Type {
	return ObjectAttributeType{
		ObjectType: basetypes.ObjectType{
			AttrTypes: v.AttributeTypes(ctx),
		},
	}
}

func (v ObjectAttributeValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
	return map[string]attr.Type{
		"bool":    types.BoolType,
		"float64": types.Float64Type,
		"int64":   types.Int64Type,
		"number":  types.NumberType,
		"string":  types.StringType,
	}
}

func (v ObjectAttributeValue) ToApiObjectAttribute(ctx context.Context) (*api.ObjectAttribute, diag.Diagnostics) {
	var diags diag.Diagnostics

	if v.IsNull() {
		return nil, diags
	}

	if v.IsUnknown() {
		diags.Append(diag.NewErrorDiagnostic(
			"ObjectAttributeValue Value Is Unknown",
			`"ObjectAttributeValue" is unknown.`,
		))

		return nil, diags
	}

	var apiObjectAttribute api.ObjectAttribute

	d := v.As(ctx, &apiObjectAttribute, basetypes.ObjectAsOptions{})

	diags.Append(d...)

	if diags.HasError() {
		return nil, diags
	}

	return &apiObjectAttribute, diags
}

func (v ObjectAttributeValue) FromApiObjectAttribute(ctx context.Context, apiObject *api.ObjectAttribute) (ObjectAttributeValue, diag.Diagnostics) {
	var diags diag.Diagnostics

	if apiObject == nil {
		return ObjectAttributeValue{
			types.ObjectNull(v.AttributeTypes(ctx)),
		}, diags
	}

	o, d := basetypes.NewObjectValue(v.AttributeTypes(ctx), map[string]attr.Value{
		"bool":    types.BoolPointerValue(apiObject.Bool),
		"float64": types.Float64PointerValue(apiObject.Float64),
		"int64":   types.Int64PointerValue(apiObject.Int64),
		"number":  types.NumberValue(apiObject.Number),
		"string":  types.StringPointerValue(apiObject.String),
	})

	diags.Append(d...)

	if diags.HasError() {
		return ObjectAttributeValue{
			types.ObjectNull(v.AttributeTypes(ctx)),
		}, diags
	}

	return ObjectAttributeValue{
		o,
	}, diags
}

@bendbennett bendbennett added the enhancement New feature or request label Oct 23, 2023
@bendbennett bendbennett marked this pull request as ready for review October 23, 2023 17:15
@bendbennett bendbennett requested a review from a team as a code owner October 23, 2023 17:15
Copy link
Member

@austinvalle austinvalle left a comment

Choose a reason for hiding this comment

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

Couple questions, but overall lgtm 🚀

if e.List.CustomType != nil {
return e.List.CustomType.ValueType
}
return "types.ListType"
Copy link
Member

Choose a reason for hiding this comment

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

Was this meant to be: types.List ?

Same for the others below:

  • types.Map
  • types.Object
  • types.Set

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you! You're quite right. Have fixed.

diags.Append(d...)

if diags.HasError() {
return NewExampleValueNull(), diags
Copy link
Member

Choose a reason for hiding this comment

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

For the From* functions, if there is an error, do we want to default to returning Unknown? Just to be sure there is an error if the provider dev doesn't check the diags 😆

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good shout. I've updated List, Map, Object, and Set. I also updated for nested object (i.e., list, map, set, single nested attribute, and list, set, single nested block).

@bendbennett bendbennett merged commit b13d310 into main Oct 25, 2023
4 checks passed
@bendbennett bendbennett deleted the bendbennett/issues-74 branch October 25, 2023 08:04
Copy link

I'm going to lock this pull request because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active contributions.
If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 22, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add code generation for collection and object attributes with associated external type
2 participants