Skip to content

Commit

Permalink
feat: add Plugin Framework resource and datasource support for test t…
Browse files Browse the repository at this point in the history
…emplate rendering package (#2088)
  • Loading branch information
rriski authored Mar 11, 2025
1 parent 87377ad commit 8da6570
Show file tree
Hide file tree
Showing 15 changed files with 1,579 additions and 465 deletions.
2 changes: 1 addition & 1 deletion internal/acctest/template/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (b *CompositionBuilder) Remove(resourcePath string) *CompositionBuilder {
resourceType := parts[0]

// Filter out the composition entry that matches both resource type and name
var filtered = make([]compositionEntry, 0, len(b.compositions))
filtered := make([]compositionEntry, 0, len(b.compositions))
for _, comp := range b.compositions {
// Skip if this is the entry we want to remove
if comp.ResourceType == resourceType {
Expand Down
1 change: 0 additions & 1 deletion internal/acctest/template/error.go

This file was deleted.

273 changes: 273 additions & 0 deletions internal/acctest/template/field_extractor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
package template

import (
"fmt"
"reflect"
"sort"

datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
resourceschema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
sdkschema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// FieldType represents the type of a field in a schema
type FieldType int

const (
// https://developer.hashicorp.com/terraform/language/expressions/types
FieldTypeUnknown FieldType = iota
FieldTypeString
FieldTypeNumber
FieldTypeBool
FieldTypeMap
FieldTypeCollection
FieldTypeObject
)

// TemplateField represents a field in a schema with its properties
type TemplateField struct {
Name string
Required bool
Optional bool
Computed bool
FieldType FieldType
NestedFields []TemplateField
IsObject bool
IsCollection bool
IsMap bool
}

// FrameworkFieldExtractor extracts fields from Framework schema
type FrameworkFieldExtractor struct{}

// NewFrameworkFieldExtractor creates a new extractor for Framework schemas
func NewFrameworkFieldExtractor() *FrameworkFieldExtractor {
return &FrameworkFieldExtractor{}
}

var _ SchemaFieldExtractor = &FrameworkFieldExtractor{}

// ExtractFields implements SchemaFieldExtractor for Framework schemas
func (e *FrameworkFieldExtractor) ExtractFields(schema interface{}) ([]TemplateField, error) {
var fields []TemplateField

switch s := schema.(type) {
case resourceschema.Schema:
fields = e.extractFields(s.Attributes)
case datasourceschema.Schema:
fields = e.extractFields(s.Attributes)
default:
return nil, fmt.Errorf("unsupported schema type: %T", schema)
}

// Check if we extracted zero fields, which indicates an invalid or empty schema
if len(fields) == 0 {
return nil, fmt.Errorf("no fields could be extracted from framework schema, schema may be empty or nil")
}

// Sort fields to ensure consistent order
fields = sortTemplateFields(fields)

return fields, nil
}

// extractFields extracts fields from either resource or datasource attributes
func (e *FrameworkFieldExtractor) extractFields(attributes interface{}) []TemplateField {
fields := make([]TemplateField, 0)

// Handle the different attribute map types with a type switch
switch attrs := attributes.(type) {
case map[string]resourceschema.Attribute:
for name, attr := range attrs {
e.processField(name, attr, &fields)
}
case map[string]datasourceschema.Attribute:
for name, attr := range attrs {
e.processField(name, attr, &fields)
}
}

return fields
}

// processField processes a single field and adds it to the fields list if not skipped
func (e *FrameworkFieldExtractor) processField(name string, attr interface{}, fields *[]TemplateField) {
// Create field with common properties
field := TemplateField{
Name: name,
Required: e.isRequired(attr),
Optional: e.isOptional(attr),
Computed: e.isComputed(attr),
FieldType: FieldTypeUnknown, // Default to unknown
}

switch a := attr.(type) {
case resourceschema.BoolAttribute, datasourceschema.BoolAttribute:
field.FieldType = FieldTypeBool
case resourceschema.Int64Attribute, resourceschema.Float64Attribute, resourceschema.NumberAttribute,
datasourceschema.Int64Attribute, datasourceschema.Float64Attribute, datasourceschema.NumberAttribute:
field.FieldType = FieldTypeNumber
case resourceschema.MapAttribute, datasourceschema.MapAttribute:
field.IsMap = true
field.FieldType = FieldTypeMap
case resourceschema.ListAttribute, resourceschema.SetAttribute,
datasourceschema.ListAttribute, datasourceschema.SetAttribute:
field.IsCollection = true
field.FieldType = FieldTypeCollection
case resourceschema.ListNestedAttribute, resourceschema.SetNestedAttribute:
field.IsCollection = true
field.IsObject = true
field.FieldType = FieldTypeObject
// Type assertion is needed to access the NestedObject.Attributes
switch nested := a.(type) {
case resourceschema.ListNestedAttribute:
field.NestedFields = e.extractFields(nested.NestedObject.Attributes)
case resourceschema.SetNestedAttribute:
field.NestedFields = e.extractFields(nested.NestedObject.Attributes)
}
case datasourceschema.ListNestedAttribute, datasourceschema.SetNestedAttribute:
field.IsCollection = true
field.IsObject = true
field.FieldType = FieldTypeObject
// Type assertion is needed to access the NestedObject.Attributes
switch nested := a.(type) {
case datasourceschema.ListNestedAttribute:
field.NestedFields = e.extractFields(nested.NestedObject.Attributes)
case datasourceschema.SetNestedAttribute:
field.NestedFields = e.extractFields(nested.NestedObject.Attributes)
}
case resourceschema.SingleNestedAttribute:
field.IsObject = true
field.FieldType = FieldTypeObject
field.NestedFields = e.extractFields(a.Attributes)
case datasourceschema.SingleNestedAttribute:
field.IsObject = true
field.FieldType = FieldTypeObject
field.NestedFields = e.extractFields(a.Attributes)
}

*fields = append(*fields, field)
}

// getFieldBool checks if a boolean field is set on an attribute
func (e *FrameworkFieldExtractor) getFieldBool(attr interface{}, fieldName string) bool {
v := reflect.ValueOf(attr)
if v.Kind() == reflect.Struct {
if f := v.FieldByName(fieldName); f.IsValid() && f.Kind() == reflect.Bool { // nosemgrep
return f.Bool()
}
}
return false
}

// isRequired checks if an attribute is required
func (e *FrameworkFieldExtractor) isRequired(attr interface{}) bool {
return e.getFieldBool(attr, "Required")
}

// isOptional checks if an attribute is optional
func (e *FrameworkFieldExtractor) isOptional(attr interface{}) bool {
return e.getFieldBool(attr, "Optional")
}

// isComputed checks if an attribute is computed
func (e *FrameworkFieldExtractor) isComputed(attr interface{}) bool {
return e.getFieldBool(attr, "Computed")
}

// SDKFieldExtractor extracts fields from SDK schema
type SDKFieldExtractor struct{}

// NewSDKFieldExtractor creates a new extractor for SDK schemas
func NewSDKFieldExtractor() *SDKFieldExtractor {
return &SDKFieldExtractor{}
}

var _ SchemaFieldExtractor = &SDKFieldExtractor{}

// ExtractFields implements SchemaFieldExtractor for SDK schemas
func (e *SDKFieldExtractor) ExtractFields(schema interface{}) ([]TemplateField, error) {
resource, ok := schema.(*sdkschema.Resource)
if !ok {
return nil, fmt.Errorf("schema is not a *schema.Resource: %T", schema)
}

fields := e.processSchema(resource.Schema)

// Check if we extracted zero fields, which indicates an invalid or empty schema
if len(fields) == 0 {
return nil, fmt.Errorf("no fields could be extracted from SDK schema, schema may be empty or nil")
}

// Sort fields to ensure consistent order
fields = sortTemplateFields(fields)

return fields, nil
}

// processSchema extracts fields from an SDK schema
func (e *SDKFieldExtractor) processSchema(schema map[string]*sdkschema.Schema) []TemplateField {
fields := make([]TemplateField, 0, len(schema))

for name, sch := range schema {
// Skip computed-only fields (that aren't required or optional)
if sch.Computed && !sch.Optional && !sch.Required {
continue
}

field := TemplateField{
Name: name,
Required: sch.Required,
Optional: sch.Optional,
Computed: sch.Computed,
FieldType: FieldTypeUnknown, // Default to unknown
}

// Handle different schema types
switch sch.Type {
case sdkschema.TypeBool:
field.FieldType = FieldTypeBool
case sdkschema.TypeInt, sdkschema.TypeFloat:
field.FieldType = FieldTypeNumber
case sdkschema.TypeMap:
field.IsMap = true
field.FieldType = FieldTypeMap
case sdkschema.TypeList, sdkschema.TypeSet:
field.IsCollection = true
field.FieldType = FieldTypeCollection

// Check if Elem is a resource (nested block)
if res, ok := sch.Elem.(*sdkschema.Resource); ok {
field.IsObject = true
field.FieldType = FieldTypeObject
// Recursively process nested schema
field.NestedFields = e.processSchema(res.Schema)
}
}

fields = append(fields, field)
}

return fields
}

// sortTemplateFields returns a sorted copy of the fields slice to ensure deterministic rendering
func sortTemplateFields(fields []TemplateField) []TemplateField {
// Make a copy to avoid modifying the original
result := make([]TemplateField, len(fields))
copy(result, fields)

// Sort fields by name
sort.Slice(result, func(i, j int) bool {
return result[i].Name < result[j].Name
})

// Sort nested fields recursively
for i := range result {
if len(result[i].NestedFields) > 0 {
result[i].NestedFields = sortTemplateFields(result[i].NestedFields)
}
}

return result
}
Loading

0 comments on commit 8da6570

Please sign in to comment.