-
Notifications
You must be signed in to change notification settings - Fork 18
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
Conversation
… type is not an empty string
… of import to include terraform-plugin-framework/types when custom type is nil
…ourcesModelsGenerator.Process()
…of conditions that are not permitted by the codegen-spec schema
There was a problem hiding this 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?
internal/cmd/cmd.go
Outdated
@@ -78,14 +78,27 @@ func (a AllCommand) Run(args []string) int { | |||
log.Fatal(err) | |||
} | |||
|
|||
// generate model code |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 👍🏻
There was a problem hiding this comment.
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"` |
There was a problem hiding this comment.
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. 👍
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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. 👍
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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 👍🏻
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 {
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 👍🏻
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"` |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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"` |
There was a problem hiding this comment.
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 🤣
There was a problem hiding this comment.
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 👍🏻
…rovided through stdin
…d resource attributes and blocks
There was a problem hiding this 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) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
.
internal/model/model.go
Outdated
|
||
type Model struct { | ||
Name string | ||
Fields string |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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. |
This PR:
"github.com/hashicorp/terraform-plugin-framework/types"
is included whenCustomType
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 theCustomType
is non-nil and theCustomType.ValueType
is an empty string as the JSON schema for the codegen-spec explicitly requires thatCustomType.ValueType
is a non-empty string.Questions
For example, with an intermediate representation that contains the following:
The generated models have a naming collision: