diff --git a/helper/schema/schema.go b/helper/schema/schema.go index dd3f013529..90f5173630 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -187,8 +187,21 @@ type Schema struct { // // If the field Optional is set to true then MinItems is ignored and thus // effectively zero. + // + // If MaxItems is 1, you may optionally also set AsSingle in order to have + // Terraform v0.12 or later treat a TypeList or TypeSet as if it were a + // single value. It will remain a list or set in Terraform v0.10 and v0.11. + // Enabling this for an existing attribute after you've made at least one + // v0.12-compatible provider release is a breaking change. AsSingle is + // likely to misbehave when used with deeply-nested set structures due to + // the imprecision of set diffs, so be sure to test it thoroughly, + // including updates that change the set members at all levels. AsSingle + // exists primarily to be used in conjunction with ConfigMode when forcing + // a nested resource to be treated as an attribute, so it can be considered + // an attribute of object type rather than of list/set of object. MaxItems int MinItems int + AsSingle bool // PromoteSingle originally allowed for a single element to be assigned // where a primitive list was expected, but this no longer works from @@ -811,6 +824,15 @@ func (m schemaMap) internalValidate(topSchemaMap schemaMap, attrsOnly bool) erro } } + if v.AsSingle { + if v.MaxItems != 1 { + return fmt.Errorf("%s: MaxItems must be 1 when AsSingle is set", k) + } + if v.Type != TypeList && v.Type != TypeSet { + return fmt.Errorf("%s: AsSingle can be used only with TypeList and TypeSet schemas", k) + } + } + // Computed-only field if v.Computed && !v.Optional { if v.ValidateFunc != nil { diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go index 4f8d9dd61c..327a4e3439 100644 --- a/helper/schema/schema_test.go +++ b/helper/schema/schema_test.go @@ -3924,6 +3924,68 @@ func TestSchemaMap_InternalValidate(t *testing.T) { }, true, // in *schema.Resource with ConfigMode of attribute, so must also have ConfigMode of attribute }, + + "AsSingle okay": { + map[string]*Schema{ + "block": &Schema{ + Type: TypeList, + Optional: true, + MaxItems: 1, + AsSingle: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "sub": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{}, + }, + }, + }, + }, + }, + false, + }, + + "AsSingle without MaxItems": { + map[string]*Schema{ + "block": &Schema{ + Type: TypeList, + Optional: true, + AsSingle: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "sub": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{}, + }, + }, + }, + }, + }, + true, // MaxItems must be 1 when AsSingle is set + }, + + "AsSingle on primitive type": { + map[string]*Schema{ + "block": &Schema{ + Type: TypeString, + Optional: true, + MaxItems: 1, + AsSingle: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "sub": &Schema{ + Type: TypeList, + Optional: true, + Elem: &Resource{}, + }, + }, + }, + }, + }, + true, // Unexpected error occurred: block: MaxItems and MinItems are only supported on lists or sets + }, } for tn, tc := range cases {