hashicorp / terraform-plugin-framework

A next-generation framework for building Terraform providers.
https://developer.hashicorp.com/terraform/plugin/framework
Mozilla Public License 2.0
303 stars 93 forks source link

Consider Object Nested Attribute Default Implementation #777

Open bflad opened 1 year ago

bflad commented 1 year ago

Module version

v1.3.1

Use-cases

When working with nested attributes, it may be desirable to automatically have an object default based on all the nested attribute default values for when the object is null. Today, this is a manual process to re-create the object default value based on the nested attribute defaults.

schema.SingleNestedAttribute{
  // ... other fields ...
  Attributes: map[string]schema.Attribute{
    "attribute_one": schema.StringAttribute{
      // ... other fields ...
      Default: stringdefault.StaticString("one"),
    }
    "attribute_two": schema.StringAttribute{
      // ... other fields ...
      Default: stringdefault.StaticString("two"),
    }
  },
  Default: objectdefault.StaticValue(
    map[string]attr.Type{
      "attribute_one": types.StringType,
      "attribute_two": types.StringType,
    },
    map[string]attr.Value{
      "attribute_one": types.StringValue("one"),
      "attribute_two": types.StringValue("two"),
    },
  )
}

Proposal

NOTE: NestedAttributes is not possible at the moment due to import cycle.

Add an AttributeTypes field and NestedAttributes field to defaults.ObjectRequest:

type ObjectRequest struct {
    // AttributeTypes returns the mapping of nested attribute
    // names to attribute types.
    AttributeTypes map[string]attr.Type

    // NestedAttributes is populated with the schema's nested
    // attributes for this object, if the object itself was a
    // NestedAttributeObject.
    NestedAttributes map[string]fwschema.Attribute

    // Path contains the path of the attribute for setting the
    // default value. Use this path for any response diagnostics.
    Path path.Path
}

In internal/fwschemadata, update the object handling to something like:

case fwschema.AttributeWithObjectDefaultValue:
    defaultValue := a.ObjectDefaultValue()

    if defaultValue != nil {
        req := defaults.ObjectRequest{
            AttributeTypes: a.GetType(), // real implementation is more complicated
            Path: fwPath,
        }

        if na, ok := a.(fwschema.NestedAttribute); ok {
            req.NestedAttributes = na.GetNestedObject().GetAttributes()
        }

        resp := defaults.ObjectResponse{}

        defaultValue.DefaultObject(ctx, req, &resp)

        logging.FrameworkTrace(ctx, fmt.Sprintf("setting attribute %s to default value: %s", fwPath, resp.PlanValue))

        return resp.PlanValue.ToTerraformValue(ctx)
    }

Create a new resource/schema/objectdefault implementation for instantiating the object using all the nested attribute defaults, e.g.

func NestedAttributeDefaults() defaults.Object {/* ... */}

It may also be worth considering whether there should be a simplified object value default function that only takes in the map[string]attr.Value, since we can theoretically have the map[string]attr.Type already available.

References

pksunkara commented 1 year ago

NestedAttributeDefaults looks like a good proposal to me and will be useful.

pksunkara commented 1 year ago

Below is an example that contains nested properties that are default null and unknown. I just want to make sure this edgecase is covered during the implementation.

"backlog_workflow_state": schema.SingleNestedAttribute{
    MarkdownDescription: "Settings for the `backlog` workflow state that is created by default for the team. *Position is always `0`. This can not be deleted.*",
    Optional:            true,
    Computed:            true,
    Default: objectdefault.StaticValue(
        types.ObjectValueMust(
            workflowStateAttrTypes,
            map[string]attr.Value{
                "id":          types.StringUnknown(),
                "position":    types.Float64Unknown(),
                "name":        types.StringValue("Backlog"),
                "color":       types.StringValue("#bec2c8"),
                "description": types.StringNull(),
            },
        ),
    ),
    Attributes: map[string]schema.Attribute{
        "id": schema.StringAttribute{
            MarkdownDescription: "Identifier of the workflow state.",
            Computed:            true,
            PlanModifiers: []planmodifier.String{
                stringplanmodifier.UseStateForUnknown(),
            },
        },
        "position": schema.Float64Attribute{
            MarkdownDescription: "Position of the workflow state.",
            Computed:            true,
            PlanModifiers: []planmodifier.Float64{
                float64planmodifier.UseStateForUnknown(),
            },
        },
        "name": schema.StringAttribute{
            MarkdownDescription: "Name of the workflow state. **Default** `Backlog`.",
            Optional:            true,
            Computed:            true,
            Default:             stringdefault.StaticString("Backlog"),
        },
        "color": schema.StringAttribute{
            MarkdownDescription: "Color of the workflow state. **Default** `#bec2c8`.",
            Optional:            true,
            Computed:            true,
            Default:             stringdefault.StaticString("#bec2c8"),
            Validators: []validator.String{
                stringvalidator.RegexMatches(colorRegex(), "must be a hex color"),
            },
        },
        "description": schema.StringAttribute{
            MarkdownDescription: "Description of the workflow state.",
            Optional:            true,
        },
    },
},
pksunkara commented 1 year ago

Another edgecase I have is, a single nested attribute inside single nested attribute.