From fcb5d9c586e3e8a6d784e03315622288904d3754 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Wed, 20 Sep 2023 13:59:53 -0400 Subject: [PATCH] add support for case-insensitive collision detection --- apstra/apstra_validator/attribute_conflict.go | 46 +++++++++-- .../attribute_conflict_test.go | 82 +++++++++++++++++-- 2 files changed, 112 insertions(+), 16 deletions(-) diff --git a/apstra/apstra_validator/attribute_conflict.go b/apstra/apstra_validator/attribute_conflict.go index 36f6ddb0..40df890f 100644 --- a/apstra/apstra_validator/attribute_conflict.go +++ b/apstra/apstra_validator/attribute_conflict.go @@ -32,7 +32,8 @@ var _ CollectionValidator = attributeConflictValidator{} // // If keyAttrs is empty, then values across all attributes are evaluated. type attributeConflictValidator struct { - keyAttrs []string + keyAttrs []string + caseInsensitive bool } func (o attributeConflictValidator) Description(_ context.Context) string { @@ -107,12 +108,6 @@ func (o attributeConflictValidator) ValidateSet(ctx context.Context, req validat } } -func UniqueValueCombinationsAt(attrNames ...string) CollectionValidator { - return attributeConflictValidator{ - keyAttrs: attrNames, - } -} - type attributeConflictValidateElementRequest struct { elementValue attr.Value elementPath path.Path @@ -170,7 +165,14 @@ func (o *attributeConflictValidator) validateElement(ctx context.Context, req at return // cannot validate when attribute is unknown } - keyValuesMap[attrName] = base64.StdEncoding.EncodeToString([]byte(attrValue.String())) + var valueToCompare string // a configured value we're checking for unique-ness + if o.caseInsensitive { + valueToCompare = strings.ToLower(attrValue.String()) + } else { + valueToCompare = attrValue.String() + } + + keyValuesMap[attrName] = base64.StdEncoding.EncodeToString([]byte(valueToCompare)) if len(keyValuesMap) < len(keyAttributeNames) { continue // keep going until we fill keyValuesMap } @@ -185,10 +187,23 @@ func (o *attributeConflictValidator) validateElement(ctx context.Context, req at } if req.foundKeyValueCombinations[sb.String()] { // seen this value before? + var detailedError string + switch len(o.keyAttrs) { + case 0: + detailedError = fmt.Sprintf("Two objects cannot use the same value "+ + "combination across all attributes (case sensitive: %t", o.caseInsensitive) + case 1: + detailedError = fmt.Sprintf("Two objects cannot use the same value for "+ + "'%s' (case sensitive: %t)", o.keyAttrs[0], o.caseInsensitive) + default: + detailedError = fmt.Sprintf("Two objects cannot use the same value "+ + "combination for these attributes: ['%s'] (case sensitive: %t)", + strings.Join(o.keyAttrs, "', '"), o.caseInsensitive) + } resp.Diagnostics.AddAttributeError( req.elementPath, fmt.Sprintf("%s collision", o.keyAttrs), - fmt.Sprintf("Two objects cannot use the same %s", o.keyAttrs), + detailedError, ) } else { req.foundKeyValueCombinations[sb.String()] = true // log the name for future collision checks @@ -196,3 +211,16 @@ func (o *attributeConflictValidator) validateElement(ctx context.Context, req at break // all of the the required attribute have been found; move on to the next set member } } + +func UniqueValueCombinationsAt(attrNames ...string) CollectionValidator { + return attributeConflictValidator{ + keyAttrs: attrNames, + } +} + +func UniqueInsensitiveValueCombinationsAt(attrNames ...string) CollectionValidator { + return attributeConflictValidator{ + keyAttrs: attrNames, + caseInsensitive: true, + } +} diff --git a/apstra/apstra_validator/attribute_conflict_test.go b/apstra/apstra_validator/attribute_conflict_test.go index 02354860..18fb1ee2 100644 --- a/apstra/apstra_validator/attribute_conflict_test.go +++ b/apstra/apstra_validator/attribute_conflict_test.go @@ -14,10 +14,11 @@ func TestAttributeConflictValidator(t *testing.T) { ctx := context.Background() type testCase struct { - keyAttrNames []string - attrTypes map[string]attr.Type - attrValues map[string]attr.Value - expectError bool + keyAttrNames []string + attrTypes map[string]attr.Type + attrValues map[string]attr.Value + expectError bool + caseInsensitive bool } attrValueSlice := func(in map[string]attr.Value) []attr.Value { @@ -391,6 +392,58 @@ func TestAttributeConflictValidator(t *testing.T) { }, expectError: true, }, + "one_key_no_collision_case_sensitive": { + keyAttrNames: []string{"key1"}, + attrTypes: map[string]attr.Type{ + "key1": types.StringType, + }, + attrValues: map[string]attr.Value{ + "one": types.ObjectValueMust( + map[string]attr.Type{ + "key1": types.StringType, + }, + map[string]attr.Value{ + "key1": types.StringValue("foo"), + }, + ), + "two": types.ObjectValueMust( + map[string]attr.Type{ + "key1": types.StringType, + }, + map[string]attr.Value{ + "key1": types.StringValue("FOO"), + }, + ), + }, + expectError: false, + caseInsensitive: false, + }, + "one_key_collision_case_insensitive": { + keyAttrNames: []string{"key1"}, + attrTypes: map[string]attr.Type{ + "key1": types.StringType, + }, + attrValues: map[string]attr.Value{ + "one": types.ObjectValueMust( + map[string]attr.Type{ + "key1": types.StringType, + }, + map[string]attr.Value{ + "key1": types.StringValue("foo"), + }, + ), + "two": types.ObjectValueMust( + map[string]attr.Type{ + "key1": types.StringType, + }, + map[string]attr.Value{ + "key1": types.StringValue("FOO"), + }, + ), + }, + expectError: true, + caseInsensitive: true, + }, } // test list validation @@ -404,7 +457,12 @@ func TestAttributeConflictValidator(t *testing.T) { ConfigValue: types.ListValueMust(types.ObjectType{AttrTypes: tCase.attrTypes}, attrValueSlice(tCase.attrValues)), } response := validator.ListResponse{} - v := UniqueValueCombinationsAt(tCase.keyAttrNames...) + var v CollectionValidator + if tCase.caseInsensitive { + v = UniqueInsensitiveValueCombinationsAt(tCase.keyAttrNames...) + } else { + v = UniqueValueCombinationsAt(tCase.keyAttrNames...) + } v.ValidateList(ctx, request, &response) if !response.Diagnostics.HasError() && tCase.expectError { @@ -436,7 +494,12 @@ func TestAttributeConflictValidator(t *testing.T) { ConfigValue: types.MapValueMust(types.ObjectType{AttrTypes: tCase.attrTypes}, tCase.attrValues), } response := validator.MapResponse{} - v := UniqueValueCombinationsAt(tCase.keyAttrNames...) + var v CollectionValidator + if tCase.caseInsensitive { + v = UniqueInsensitiveValueCombinationsAt(tCase.keyAttrNames...) + } else { + v = UniqueValueCombinationsAt(tCase.keyAttrNames...) + } v.ValidateMap(ctx, request, &response) if !response.Diagnostics.HasError() && tCase.expectError { @@ -468,7 +531,12 @@ func TestAttributeConflictValidator(t *testing.T) { ConfigValue: types.SetValueMust(types.ObjectType{AttrTypes: tCase.attrTypes}, attrValueSlice(tCase.attrValues)), } response := validator.SetResponse{} - v := UniqueValueCombinationsAt(tCase.keyAttrNames...) + var v CollectionValidator + if tCase.caseInsensitive { + v = UniqueInsensitiveValueCombinationsAt(tCase.keyAttrNames...) + } else { + v = UniqueValueCombinationsAt(tCase.keyAttrNames...) + } v.ValidateSet(ctx, request, &response) if !response.Diagnostics.HasError() && tCase.expectError {