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 model generation #17

Merged
merged 36 commits into from
Jun 26, 2023
Merged

Add model generation #17

merged 36 commits into from
Jun 26, 2023

Conversation

bendbennett
Copy link
Contributor

@bendbennett bendbennett commented Jun 22, 2023

This PR:

  • Adds model generation
  • Modifies the imports so that "github.com/hashicorp/terraform-plugin-framework/types" is included when CustomType is not nil as this import is required for all of the "default" framework types that are used by the models (e.g., types.Bool for a boolean attribute, []types.Object for a list nested attribute etc).

Currently, this is only in-place for data sources, but will be extended to include provider and resources following initial review.

The unit tests in place for the Generator<Type>.ToModel() methods do not explicitly cover instances in which the CustomType is non-nil and the CustomType.ValueType is an empty string as the JSON schema for the codegen-spec explicitly requires that CustomType.ValueType is a non-empty string.

Questions

  • Do we want to extend the test coverage for DataSourceModelGenerator.Process() to include testing of recursion for nested attributes and nested blocks?
  • Should we consider prefixing the names of the models generated during recursion into nested attributes and nested blocks to avoid potential naming collisions? A separate issue has been created for consideration of the handling of naming collisions.

For example, with an intermediate representation that contains the following:

          {
            "name": "list_nested_list_nested_bool_attribute",
            "list_nested": {
              "computed_optional_required": "computed",
              "nested_object": {
                "attributes": [
                  {
                    "name": "list_nested_attribute",
                    "list_nested": {
                      "computed_optional_required": "computed",
                      "nested_object": {
                        "attributes": [
                          {
                            "name": "bool_attribute",
                            "bool": {
                              "computed_optional_required": "computed"
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              }
            }
          },
          {
            "name": "list_nested_list_nested_list_attribute",
            "list_nested": {
              "computed_optional_required": "computed",
              "nested_object": {
                "attributes": [
                  {
                    "name": "list_nested_attribute",
                    "list_nested": {
                      "computed_optional_required": "computed",
                      "nested_object": {
                        "attributes": [
                          {
                            "name": "list_attribute",
                            "list": {
                              "computed_optional_required": "computed",
                              "element_type": {
                                "string": {}
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              }
            }
          },

The generated models have a naming collision:

type listNestedAttributeModel struct {
	BoolAttribute types.Bool `tfsdk:"bool_attribute"`
}

type listNestedAttributeModel struct {
	ListAttribute types.List `tfsdk:"list_attribute"`
}

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.

Some initial thoughts! What do you think?

@@ -78,14 +78,27 @@ func (a AllCommand) Run(args []string) int {
log.Fatal(err)
}

// generate model code
Copy link
Contributor

Choose a reason for hiding this comment

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

Not necessarily for this PR (maybe as part of @austinvalle's command work?), but it'd be great if the "all" command could delegate to some separate logic that covers all data source generation (which itself handles models, schema, and formatting components), all resource generation (same components), etc.

Copy link
Member

@austinvalle austinvalle Jun 22, 2023

Choose a reason for hiding this comment

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

I can do this in my upcoming PR #18 👍🏻

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @austinvalle

BoolAttribute types.Bool `tfsdk:"bool_attribute"`
ListListAttribute types.List `tfsdk:"list_list_attribute"`
ListMapAttribute types.List `tfsdk:"list_map_attribute"`
ListNestedBoolAttribute []types.Object `tfsdk:"list_nested_bool_attribute"`
Copy link
Contributor

Choose a reason for hiding this comment

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

These should be types.List/types.Set as appropriate to handle unknown values. 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ooops........

GeneratorBoolAttribute
}{
Name: name,
DefaultType: "types.Bool",
Copy link
Contributor

Choose a reason for hiding this comment

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

Super nit: It might be good to move these to constants. 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

Given these templates are very tiny and delegating to Go functions anyways, would this potentially be easier to use strings.Builder in some cases and regular Go logic? It would help remove some of the "logic indirection" of needing to bounce between what is doing what and could help simplify the code everywhere. e.g. in this case

// ToModel()
fieldType := "types.Bool"

if g.CustomType != nil {
  fieldType = g.CustomType.ValueType
}

return fmt.Sprintf("%s %s `tfsdk:%q`", snakeCaseToCamelCase(name), fieldType, name), nil

Taking this further, it may actually be preferable to delegate this to its own Go type and method to centralize the logic more and give everything concise naming:

type ModelStructField struct {
  Name string
  TfsdkName string // debatable :)
  ValueType string
}

func (f ModelStructField) String() string {
  return fmt.Sprintf("%s %s `tfsdk:%q`", f.Name, f.ValueType, f.TfsdkName)
}

// ToModel()
field := ModelStructField{
  Name: snakeCaseToCamelCase(name),
  TfsdkName: name,
  ValueType: "types.Bool",
}

if g.CustomType != nil {
  field.ValueType = g.CustomType.ValueType
}

return field.String()

Copy link
Member

@austinvalle austinvalle Jun 22, 2023

Choose a reason for hiding this comment

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

Ben and I chatted about a similar topic in our 1 on 1. I'm still on the fence on whether it's easier to use Go templates vs. string builders for generating code in general.

If we plan to eventually allow "overriding" of the templates then it could be useful, otherwise it makes it a little less convenient to follow the code/read.

I'm not sure we landed in a definitive place on whether to favor templates or string builder, maybe a hybrid approach is fine? Worth a discussion for sure 👍🏻

Copy link
Contributor

Choose a reason for hiding this comment

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

Taking a hybrid approach seems very reasonable to me personally. While text/template is a generally acceptable tool for "larger scale" Go code generation in the Go community, I don't think its meant to be an always versus never situation, especially if the code generation makes sense to be decomposed into smaller implementation details.

My personal factors of whether to use a template here would include:

  • If the generated code snippet is large, such as a whole source code file
  • If the generated code snippet has many possible customizations and if they would be better served via:
    • Additional template functions, which tend to be less easily discoverable
    • Additional properties in the specification
    • A command line flag
    • A generator-specific configuration
  • If a text template is more or less readable than its Go code equivalent (also keeping in mind the logic indirection)

As a pro-template example, a template for generating a scaffold resource source code file likely makes sense, so a provider maintainer can give the best start for other developers by baking in provider-specific code details and comments.

Copy link
Contributor Author

@bendbennett bendbennett Jun 23, 2023

Choose a reason for hiding this comment

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

Thanks for the feedback and thoughts on template usage. I think I'm leaning towards removing the usage of templates for the generation of the models for the reasons discussed. I'll refactor to switch to using structs and String() methods as suggested.

@@ -243,3 +243,108 @@ var datasourceDataSourceSchema = schema.Schema{
},
},
}

type datasourceDataSourceModel struct {
Copy link
Member

Choose a reason for hiding this comment

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

Random thought that also applies to the schema variable above, but should we opt to just export these structs by default?

type DatasourceDataSourceModel struct {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmmmm........I'm not sure about this. Easily switched out. Do the structs and schema var need to be exported?

Copy link
Member

Choose a reason for hiding this comment

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

I'd lean towards always exporting, in-case developers want to keep generated code in it's own package. It's also not anything we need to change now 👍🏻

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd be 👍 for always exporting as well -- while most provider code is not meant to be exported for other Go modules, most provider developers opt to achieve this via internal/ packaging. At the end of the day though, that is their choice. I think as we'll see with #20, being able to generate multiple packages of code might be an important implementation detail.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair enough. Have made all of the generated models exported.

BoolAttribute types.Bool `tfsdk:"bool_attribute"`
ListListAttribute types.List `tfsdk:"list_list_attribute"`
ListMapAttribute types.List `tfsdk:"list_map_attribute"`
ListNestedBoolAttribute []types.Object `tfsdk:"list_nested_bool_attribute"`
Copy link
Member

Choose a reason for hiding this comment

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

Is it worth it to add comments mentioning which model represents the nested object like we do in the utility providers: https://github.com/hashicorp/terraform-provider-tls/blob/db1c1933f5410d86c2c40fedabe10d0942a71341/internal/provider/models.go#L25

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 think this is an interesting idea. If it's ok with you and Brian I'm going to punt on this but have added an issue for later consideration.

ListNestedBlockObjectAttributeListNestedNestedBlockListAttribute []types.Object `tfsdk:"list_nested_block_object_attribute_list_nested_nested_block_list_attribute"`
ListNestedListNestedBlockBoolAttribute []types.Object `tfsdk:"list_nested_list_nested_block_bool_attribute"`
SingleNestedBlockBoolAttribute types.Object `tfsdk:"single_nested_block_bool_attribute"`
SingleNestedBlockObjectAttributeSingleNestedListNestedBlockListAttribute types.Object `tfsdk:"single_nested_block_object_attribute_single_nested_list_nested_block_list_attribute"`
Copy link
Member

Choose a reason for hiding this comment

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

single_nested_block_object_attribute_single_nested_list_nested_block_list_attribute

This field name fills me with joy 🤣

Copy link
Member

@austinvalle austinvalle Jun 22, 2023

Choose a reason for hiding this comment

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

Ben and I chatted about a similar topic in our 1 on 1. I'm still on the fence on whether it's easier to use Go templates vs. string builders for generating code in general.

If we plan to eventually allow "overriding" of the templates then it could be useful, otherwise it makes it a little less convenient to follow the code/read.

I'm not sure we landed in a definitive place on whether to favor templates or string builder, maybe a hybrid approach is fine? Worth a discussion for sure 👍🏻

@bendbennett bendbennett marked this pull request as ready for review June 26, 2023 10:24
@bendbennett bendbennett requested a review from a team as a code owner June 26, 2023 10:24
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 looks really good! Some minor code considerations, if you're interested now or later. 😄

for name, schema := range schemas {
var buf bytes.Buffer

m, err := generateModel(name, schema.Attributes, schema.Blocks)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: It seems like this could be a method on GeneratorDataSourceSchema, e.g.

func (s GeneratorDataSourceSchema) Model(name string) (Model, 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.

Fair point. I've refactored:

func (g GeneratorResourceSchema) Model(name string) ([]model.Model, error)

As there are recursive calls for generating the "nested" models I'm returning a slice of model.Model.


type Model struct {
Name string
Fields string
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you think it would be better if this was Fields []StructField so code can append() directly into it and String() can handle the newlines in the string output?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems reasonable. Have refactored along those lines.

@bendbennett bendbennett merged commit afe26a0 into main Jun 26, 2023
@bendbennett bendbennett deleted the models branch June 26, 2023 16:47
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