package validate import ( "context" "fmt" tfdiag "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" ccdiag "github.com/hashicorp/terraform-provider-awscc/internal/diag" ) type RequiredAttributesFunc func(names []string) tfdiag.Diagnostics // Required returns a RequiredAttributesFunc that validates that all required attributes are specfied. func Required(required ...string) RequiredAttributesFunc { return func(names []string) tfdiag.Diagnostics { var diags tfdiag.Diagnostics for _, r := range required { var specified bool for _, n := range names { if r == n { specified = true break } } if !specified { diags.Append(tfdiag.NewErrorDiagnostic( "Attribute not specified", fmt.Sprintf("Required attribute (%s) not specified", r), )) } } return diags } } // AllOfRequired returns a RequiredAttributesFunc that validates that all of the specified validators pass. // "To validate against allOf, the given data must be valid against all of the given subschemas." func AllOfRequired(fs ...RequiredAttributesFunc) RequiredAttributesFunc { return func(names []string) tfdiag.Diagnostics { var output tfdiag.Diagnostics for _, f := range fs { output.Append(f(names)...) } return output } } // AnyOfRequired returns a RequiredAttributesFunc that validates that any of the specified validators pass. // "To validate against anyOf, the given data must be valid against any (one or more) of the given subschemas." func AnyOfRequired(fs ...RequiredAttributesFunc) RequiredAttributesFunc { return func(names []string) tfdiag.Diagnostics { var output tfdiag.Diagnostics for _, f := range fs { diags := f(names) if diags.HasError() { output = append(output, diags...) } else { return nil } } return output } } // OneOfRequired returns a RequiredAttributesFunc that validates that exactly one of of the specified validators pass. // "To validate against oneOf, the given data must be valid against exactly one of the given subschemas." func OneOfRequired(fs ...RequiredAttributesFunc) RequiredAttributesFunc { return func(names []string) tfdiag.Diagnostics { var output tfdiag.Diagnostics var n int for _, f := range fs { diags := f(names) if diags.HasError() { output = append(output, diags...) } else { n++ } } switch n { case 0: case 1: return nil default: output.Append(tfdiag.NewErrorDiagnostic( "Conflicting attributes", fmt.Sprintf("%d groups of required attributes match", n), )) } return output } } // requiredAttributesValidator validates that required Attributes are specified. type requiredAttributesValidator struct { tfsdk.AttributeValidator fs []RequiredAttributesFunc } // Description describes the validation in plain text formatting. func (validator requiredAttributesValidator) Description(_ context.Context) string { return "required Attributes are specified" } // MarkdownDescription describes the validation in Markdown formatting. func (validator requiredAttributesValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } // Validate performs the validation. func (validator requiredAttributesValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { // The attribute is either: // * Object (SingleNestedAttribute) // * List (ListNestedAttribute) // * Set (SetNestedAttribute) isMap := false isSlice := false switch v := request.AttributeConfig.(type) { case types.Object: if v.Null || v.Unknown { return } isMap = true case types.List: if v.Null || v.Unknown { return } isSlice = true case types.Set: if v.Null || v.Unknown { return } isSlice = true default: response.Diagnostics.Append(ccdiag.NewIncorrectValueTypeAttributeError( request.AttributePath, v, )) return } val, err := request.AttributeConfig.ToTerraformValue(ctx) if err != nil { response.Diagnostics.Append(ccdiag.NewUnableToObtainValueAttributeError( request.AttributePath, err, )) return } var diags tfdiag.Diagnostics if isMap { var v map[string]tftypes.Value if err := val.As(&v); err != nil { response.Diagnostics.Append(ccdiag.NewUnableToConvertValueTypeAttributeError( request.AttributePath, err, )) return } // Ensure that the object is fully known. for _, val := range v { if !val.IsFullyKnown() { return } } diags = evaluateRequiredAttributesFuncs(specifiedAttributes(v), validator.fs...) } else if isSlice { var v []tftypes.Value if err := val.As(&v); err != nil { response.Diagnostics.Append(ccdiag.NewUnableToConvertValueTypeAttributeError( request.AttributePath, err, )) return } // Ensure that the array is fully known. for _, val := range v { if !val.IsFullyKnown() { return } } for i, val := range v { // Each array element must be an Object. var vals map[string]tftypes.Value if err := val.As(&vals); err != nil { response.Diagnostics.Append(ccdiag.NewUnableToConvertValueTypeAttributeError( request.AttributePath.WithElementKeyInt(i), err, )) return } diags = evaluateRequiredAttributesFuncs(specifiedAttributes(vals), validator.fs...) // Required attributes must be specified for every element. if diags.HasError() { break } } } response.Diagnostics = append(response.Diagnostics, diags...) if diags.HasError() { return } } // AttributeRequired returns a new required Attributes validator. func RequiredAttributes(fs ...RequiredAttributesFunc) tfsdk.AttributeValidator { return requiredAttributesValidator{ fs: fs, } } // requiredAttributesResourceConfigValidator validates that resource schema-level required Attributes are specified. type resourceConfigRequiredAttributesValidator struct { tfsdk.ResourceConfigValidator fs []RequiredAttributesFunc } // Description describes the validation in plain text formatting. func (validator resourceConfigRequiredAttributesValidator) Description(_ context.Context) string { return "required Attributes are specified" } // MarkdownDescription describes the validation in Markdown formatting. func (validator resourceConfigRequiredAttributesValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } // Validate performs the validation. func (validator resourceConfigRequiredAttributesValidator) Validate(ctx context.Context, request tfsdk.ValidateResourceConfigRequest, response *tfsdk.ValidateResourceConfigResponse) { val := request.Config.Raw if val.IsNull() || !val.IsFullyKnown() { return } if typ := val.Type(); !typ.Is(tftypes.Object{}) { response.Diagnostics.Append(ccdiag.NewIncorrectValueTypeResourceConfigError(typ)) return } var vals map[string]tftypes.Value if err := val.As(&vals); err != nil { response.Diagnostics.Append(ccdiag.NewUnableToConvertValueTypeResourceConfigError(err)) return } diags := evaluateRequiredAttributesFuncs(specifiedAttributes(vals), validator.fs...) response.Diagnostics = append(response.Diagnostics, diags...) if diags.HasError() { return } } // ResourceConfigRequiredAttributes returns a new resource schema-level required Attributes validator. func ResourceConfigRequiredAttributes(fs ...RequiredAttributesFunc) tfsdk.ResourceConfigValidator { return resourceConfigRequiredAttributesValidator{ fs: fs, } } func evaluateRequiredAttributesFuncs(names []string, fs ...RequiredAttributesFunc) tfdiag.Diagnostics { var diags tfdiag.Diagnostics for _, f := range fs { diags = append(diags, f(names)...) } return diags } // specifiedAttributes returns the names of the attributes that are set in an object. // The object is fully known. func specifiedAttributes(vals map[string]tftypes.Value) []string { as := make([]string, 0) for a, val := range vals { if !val.IsNull() { as = append(as, a) } } return as }