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

Generate "expand" and "flatten" functions for associated external types linked to list, map, set, single nested attributes and blocks #32

Merged
merged 28 commits into from
Aug 16, 2023

Conversation

bendbennett
Copy link
Contributor

@bendbennett bendbennett commented Aug 7, 2023

References: #31

This PR adds generation of "expand" and "flatten" functions for list, map, set, single nested attributes and blocks, with the following caveats:

  • Only "top-level" attributes and blocks are currently handled,
  • Only "primitive" attributes (Bool, Float64, Int64, Number, String) within list, map, set, single nested attributes and blocks are currently handled.

Related

The change introduced in internal/schema: add AttrImport for object model helper is also handled at the GeneratorAttribute/Block level in this PR. We can handle this either way, but I've left both sets of changes in place for now.

@bendbennett bendbennett changed the title Generate "expand" and "flatten" functions for associated external types Generate "expand" and "flatten" functions for associated external types linked to list, map, set, single nested attributes and blocks Aug 10, 2023
@bendbennett bendbennett marked this pull request as ready for review August 10, 2023 16:29
@bendbennett bendbennett requested a review from a team as a code owner August 10, 2023 16:29
Copy link
Contributor

@bflad bflad left a comment

Choose a reason for hiding this comment

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

Overall this looks like it is going in the right direction but noticed some things upfront which would apply across the board. 👍

func ToListNestedAttributeAssocExtType(ctx context.Context, tfList types.List) ([]*apisdk.Type, diag.Diagnostics) {
var diags diag.Diagnostics

if tfList.IsNull() || tfList.IsUnknown() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Unknown handling is an interesting case! This is logic is probably okay for now, but we could also consider raising an error diagnostic rather than potentially calling an API incorrectly (which might generate its own confusing error).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have added in raising error diagnostic.

return nil, diags
}

var tfModels []ListNestedAttributeAssocExtTypeModel
Copy link
Contributor

Choose a reason for hiding this comment

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

Collection nested attributes are technically a types. whatever collection of types.Object elements, where each of those types.Object can also be unknown. This can happen in Terraform with more complex configuration situations. So we'll need to be careful to ensure we handle unknowns for each of the elements as well. 👍

So yeah the logic awkwardly becomes the more complex: On the types.List call ElementsAs() into a []types.Object, range through those (now types.Object), handle null/unknown, then As() into the model type and set the values.

Yay for generating this code to remove that complexity!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have updated the logic here. Thoughts?

), diags
}

var tfModels []ListNestedAttributeAssocExtTypeModel
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this might need to be a pointer, or there needs to be nil handling in the range below, since there can be an input such as []*apisdk.Type{nil, nil, nil} 👍

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. Have updated to handle potential for nil(s) being returned.

@@ -853,3 +1282,364 @@ func (m SingleNestedBlockTwoSingleNestedBlockOneModel) ObjectValueFrom(ctx conte
data,
)
}

func ToListNestedAttributeAssocExtType(ctx context.Context, tfList types.List) ([]*apisdk.Type, diag.Diagnostics) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Bikeshed: If we're avoiding methods, which would give us some implicit function namespacing (but I also understand we don't need anything from the model), I think these function names will need to include the names of both sides of the conversion to make more sense. I personally am confused what a "to ListNestedAttributeAssocatedExtType" is doing by the name as it sounds like it'd be making the framework value for that attribute even though the function signature says otherwise. 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess we have a few options here:

  • ToType()
  • FromListNestedAttributeAssocType()
  • FromListNestedAttributeAssocTypeToType()
  • ExpandListNestedAttributeAssocType()
  • ExpandListNestedAttributeAssocTypeToType()

And the converse for the other side of the operation:

  • FromType()
  • ToListNestedAttributeAssocType()
  • ToListNestedAttributeAssocTypeFromType()
  • FlattenType()
  • FlattenTypeToListNestedAttributeAssocType()

I don't have any strong feelings on these options but I agree that the current implementation is confusing, should definitely be FromListNestedAttributeAssocType().

Let me know your thoughts and I'll fix this up 👍

Copy link
Contributor

@bflad bflad Aug 11, 2023

Choose a reason for hiding this comment

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

My initial thoughts are that --

We should avoid the legacy "flatten"/"expand" terminology that was originally in Terraform when the config/plan/state data was literally represented by a "flat map" (e.g. map[string]string). Provider code that went from that flat map data into API SDK types with real data structures was "expand"-ing the data and the opposite was "flatten"-ing the data. I'm not sure that terminology would be understood in the context of the framework or modern Terraform, which now uses real data structures for its data representations.

I worry about "to/from attribute" naming since there can be multiple API SDK types that need to be associated.

I like the succinctness of "to/from API SDK type" naming, since that's the functionality we're trying to achieve here, but I think that only makes sense if the naming is occurring as a method on something associated with the attribute (e.g. its model type). An option we could explore in this area is whether we could use the "model type" as a custom type implementation of basetypes.ObjectTypable/basetypes.ObjectValuable. In that design, instead of working with types.Object/basetypes.ObjectValue separately from a model type, the model types (now plural) work double duty as the schema type and value type with methods for creating/representing that value with each API SDK type.

The "from/to attribute to/from API SDK type" naming certainly gives the best chance out of these for reducing naming collisions, but its quite verbose, and still has the issue of there potentially be collisions. For true safety, there might need to be a concatenation of the prior attribute path, e.g. let's say there are two separate nested attributes with a duplicate named child attribute and both pointing at the same API SDK type. There's going to be duplicated generated code that cannot compile.

I think given these initial options, I'd sway towards either the verbose naming for its naming safety or, maybe preferably, trying to determine if custom types is feasible and potentially more desirable. At the end of the day though, verbosity in naming or creating types is not the biggest deal, because this is generated code after all. Maybe best to talk through this in person next week. 😄

Copy link
Contributor

@bflad bflad left a comment

Choose a reason for hiding this comment

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

I'm okay approving this as a first iteration with the understanding that some of the little details might change in the near future and to unblock some of the other planned work. 👍

Comment on lines 1310 to 1357
var tfModels []*ListNestedAttributeAssocExtTypeModel

for _, listObject := range listObjects {
if listObject.IsNull() {
tfModels = append(tfModels, nil)

continue
}

if listObject.IsUnknown() {
diags.Append(diag.NewErrorDiagnostic(
"Object Value Within List Is Unknown",
`Model field "ListNestedAttributeAssocExtType" contains an object which is unknown.`,
))

return nil, diags
}

var tfModel ListNestedAttributeAssocExtTypeModel

d := listObject.As(ctx, &tfModel, basetypes.ObjectAsOptions{})

diags.Append(d...)

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

tfModels = append(tfModels, &tfModel)
}

var apiObjects []*apisdk.Type

for _, tfModel := range tfModels {
if tfModel == nil {
apiObjects = append(apiObjects, nil)

continue
}

apiObjects = append(apiObjects, &apisdk.Type{
BoolAttribute: tfModel.BoolAttribute.ValueBoolPointer(),
Float64Attribute: tfModel.Float64Attribute.ValueFloat64Pointer(),
Int64Attribute: tfModel.Int64Attribute.ValueInt64Pointer(),
NumberAttribute: tfModel.NumberAttribute.ValueBigFloat(),
StringAttribute: tfModel.StringAttribute.ValueStringPointer(),
})
}
Copy link
Contributor

@bflad bflad Aug 11, 2023

Choose a reason for hiding this comment

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

Nit: Seems like this logic (and others below) can be consolidated into one loop:

	apiObjects := make([]*apisdk.Type, 0, len(listObjects))

	for _, listObject := range listObjects {
		if listObject.IsNull() {
			apiObjects = append(apiObjects, nil)

			continue
		}

		if listObject.IsUnknown() {
			diags.Append(diag.NewErrorDiagnostic(
				"Object Value Within List Is Unknown",
				`Model field "ListNestedAttributeAssocExtType" contains an object which is unknown.`,
			))

			return nil, diags
		}

		var tfModel ListNestedAttributeAssocExtTypeModel

		d := listObject.As(ctx, &tfModel, basetypes.ObjectAsOptions{})

		diags.Append(d...)

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

		apiObjects = append(apiObjects, &apisdk.Type{
			BoolAttribute:    tfModel.BoolAttribute.ValueBoolPointer(),
			Float64Attribute: tfModel.Float64Attribute.ValueFloat64Pointer(),
			Int64Attribute:   tfModel.Int64Attribute.ValueInt64Pointer(),
			NumberAttribute:  tfModel.NumberAttribute.ValueBigFloat(),
			StringAttribute:  tfModel.StringAttribute.ValueStringPointer(),
		})
	}

@@ -297,3 +308,25 @@ func CustomValidatorImports(cv *specschema.CustomValidator) *Imports {

return imports
}

func AssociatedExternalTypeImports(a *specschema.AssociatedExternalType) *Imports {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be a little easier if the AssocExtType had an additional Imports() *Imports method and the attributes implemented AssocExtType instead of the spec types?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've refactored along these lines.

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.

Looked through the changes and everything makes sense to me, great work! 👍🏻

@bendbennett bendbennett merged commit 61b856c into main Aug 16, 2023
@bendbennett bendbennett deleted the bendbennett/issues-31 branch August 16, 2023 12:21
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 23, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants