- Any other criticisms or comments are welcome!
Missed this
You may already know this, but if re-use is a concern, you could also implement Attribute Validation for a set using the validator.Set
interface.
You could even make it generic with the comparable
type constraint from Go 1.20 if you really want to get crazy (not official guidance, just speculating). This is a “napkin math” comparable example I whipped up:
Validator
func AttributeUniqueValidator[T comparable](attrName string, getCompareKey func(attr.Value) T) validator.Set {
return attributeConflictValidator[T]{
attrName: attrName,
getCompareKey: getCompareKey,
}
}
var _ validator.Set = attributeConflictValidator[string]{}
type attributeConflictValidator[T comparable] struct {
attrName string
getCompareKey func(attr.Value) T
}
func (v attributeConflictValidator[T]) Description(ctx context.Context) string {
return ""
}
func (v attributeConflictValidator[T]) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}
func (v attributeConflictValidator[T]) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
_, ok := req.ConfigValue.ElementType(ctx).(basetypes.ObjectTypable)
if !ok {
resp.Diagnostics.AddAttributeError(
req.Path,
"Invalid Validator for Element Type",
"While performing schema-based validation, an unexpected error occurred. "+
"The attribute declares a Object values validator, however its values do not implement types.ObjectType or the types.ObjectTypable interface for custom Object types. "+
"Use the appropriate values validator that matches the element type. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("Path: %s\n", req.Path.String())+
fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx)),
)
return
}
nameMap := make(map[T]bool)
for _, element := range req.ConfigValue.Elements() {
objectPath := req.Path.AtSetValue(element)
objectValuable, ok := element.(basetypes.ObjectValuable)
if !ok {
resp.Diagnostics.AddAttributeError(
req.Path,
"Invalid Validator for Element Value",
"While performing schema-based validation, an unexpected error occurred. "+
"The attribute declares a Object values validator, however its values do not implement types.ObjectType or the types.ObjectTypable interface for custom Object types. "+
"This is likely an issue with terraform-plugin-framework and should be reported to the provider developers.\n\n"+
fmt.Sprintf("Path: %s\n", req.Path.String())+
fmt.Sprintf("Element Type: %T\n", req.ConfigValue.ElementType(ctx))+
fmt.Sprintf("Element Value Type: %T\n", element),
)
return
}
objectValue, diags := objectValuable.ToObjectValue(ctx)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
for name, attr := range objectValue.Attributes() {
if name != v.attrName || attr.IsUnknown() {
continue
}
compareKey := v.getCompareKey(attr)
if nameMap[compareKey] {
resp.Diagnostics.AddAttributeError(
objectPath,
fmt.Sprintf("%s collision", v.attrName),
fmt.Sprintf("Two objects cannot use the same %s", v.attrName),
)
} else {
nameMap[compareKey] = true
}
break
}
}
}
Schema
func (r *thingResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"configure_set": schema.SetNestedAttribute{
Required: true,
Validators: []validator.Set{
AttributeUniqueValidator("name", func(v attr.Value) string {
strValue := v.(types.String)
return strValue.ValueString()
}),
AttributeUniqueValidator("number", func(v attr.Value) int64 {
strValue := v.(types.Int64)
return strValue.ValueInt64()
}),
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
},
"number": schema.Int64Attribute{
Required: true,
},
},
},
},
},
}
}
Sample validation errors
│ Error: number collision
│
│ with examplecloud_thing.this,
│ on resource.tf line 9, in resource "examplecloud_thing" "this":
│ 9: configure_set = [
│ 10: {
│ 11: name = "abc",
│ 12: number = 123
│ 13: },
│ 14: {
│ 15: name = "def",
│ 16: number = 123
│ 17: }
│ 18: ]
│
│ Two objects cannot use the same number
│ Error: name collision
│
│ with examplecloud_thing.this,
│ on resource.tf line 9, in resource "examplecloud_thing" "this":
│ 9: configure_set = [
│ 10: {
│ 11: name = "abc",
│ 12: number = 123
│ 13: },
│ 14: {
│ 15: name = "abc",
│ 16: number = 456
│ 17: }
│ 18: ]
│
│ Two objects cannot use the same name
That can probably be cleaned up immensely, but just some random thoughts since you asked and hoping it’ll spur up a better idea