Skip to content

Commit

Permalink
Add new packages for datasourcetimeouts and resourcetimeouts that mak…
Browse files Browse the repository at this point in the history
…e use of type-specific schema and attributes
  • Loading branch information
bendbennett committed Dec 1, 2022
1 parent 8f55964 commit aa0bcd6
Show file tree
Hide file tree
Showing 8 changed files with 756 additions and 0 deletions.
32 changes: 32 additions & 0 deletions internal/validators/timeduration.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import (
"time"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
)

var _ tfsdk.AttributeValidator = timeDurationValidator{}
var _ validator.String = timeDurationValidator{}

// timeDurationValidator validates that a string Attribute's value is parseable as time.Duration.
type timeDurationValidator struct {
Expand All @@ -27,6 +29,8 @@ func (validator timeDurationValidator) MarkdownDescription(ctx context.Context)
}

// Validate performs the validation.
//
// Deprecated: Use ValidateString instead.
func (validator timeDurationValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) {
s := request.AttributeConfig.(types.String)

Expand All @@ -44,6 +48,24 @@ func (validator timeDurationValidator) Validate(ctx context.Context, request tfs
}
}

// ValidateString performs the validation.
func (validator timeDurationValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
s := req.ConfigValue

if s.IsUnknown() || s.IsNull() {
return
}

if _, err := time.ParseDuration(s.ValueString()); err != nil {
resp.Diagnostics.Append(diag.NewAttributeErrorDiagnostic(
req.Path,
"Invalid Attribute Value Time Duration",
fmt.Sprintf("%q %s", s.ValueString(), validator.Description(ctx))),
)
return
}
}

// TimeDuration returns an AttributeValidator which ensures that any configured
// attribute value:
//
Expand All @@ -53,3 +75,13 @@ func (validator timeDurationValidator) Validate(ctx context.Context, request tfs
func TimeDuration() tfsdk.AttributeValidator {
return timeDurationValidator{}
}

// TimeDurationString returns an AttributeValidator which ensures that any configured
// attribute value:
//
// - Is parseable as time duration.
//
// Null (unconfigured) and unknown (known after apply) values are skipped.
func TimeDurationString() validator.String {
return timeDurationValidator{}
}
51 changes: 51 additions & 0 deletions internal/validators/timeduration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"

Expand Down Expand Up @@ -62,3 +63,53 @@ func TestTimeDuration(t *testing.T) {
})
}
}

func TestTimeDurationString(t *testing.T) {
t.Parallel()

type testCase struct {
val types.String
expectedDiagnostics diag.Diagnostics
}

tests := map[string]testCase{
"unknown": {
val: types.StringUnknown(),
},
"null": {
val: types.StringNull(),
},
"valid": {
val: types.StringValue("20m"),
},
"invalid": {
val: types.StringValue("20x"),
expectedDiagnostics: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(
path.Root("test"),
"Invalid Attribute Value Time Duration",
`"20x" must be a string containing a sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".`,
),
},
},
}

for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
request := validator.StringRequest{
Path: path.Root("test"),
PathExpression: path.MatchRoot("test"),
ConfigValue: test.val,
}

response := validator.StringResponse{}

validators.TimeDurationString().ValidateString(context.Background(), request, &response)

if diff := cmp.Diff(response.Diagnostics, test.expectedDiagnostics); diff != "" {
t.Errorf("unexpected diagnostics difference: %s", diff)
}
})
}
}
101 changes: 101 additions & 0 deletions timeouts/datasource/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package datasourcetimeouts

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"

"github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators"
)

const (
attributeNameCreate = "create"
attributeNameRead = "read"
attributeNameUpdate = "update"
attributeNameDelete = "delete"
)

// Opts is used as an argument to Block and Attributes to indicate which attributes
// should be created.
type Opts struct {
Create bool
Read bool
Update bool
Delete bool
}

// Block returns a schema.Block containing attributes for each of the fields
// in Opts which are set to true. Each attribute is defined as types.StringType
// and optional. A validator is used to verify that the value assigned to an
// attribute can be parsed as time.Duration.
func Block(ctx context.Context, opts Opts) schema.Block {
return schema.SingleNestedBlock{
Attributes: attributesMap(opts),
}
}

// BlockAll returns a schema.Block containing attributes for each of create, read,
// update and delete. Each attribute is defined as types.StringType and optional.
// A validator is used to verify that the value assigned to an attribute can be
// parsed as time.Duration.
func BlockAll(ctx context.Context) schema.Block {
return Block(ctx, Opts{
Create: true,
Read: true,
Update: true,
Delete: true,
})
}

// Attributes returns a schema.SingleNestedAttribute
// which contains attributes for each of the fields in Opts which are set to true.
// Each attribute is defined as types.StringType and optional. A validator is used
// to verify that the value assigned to an attribute can be parsed as time.Duration.
func Attributes(ctx context.Context, opts Opts) schema.Attribute {
return schema.SingleNestedAttribute{
Optional: true,
Attributes: attributesMap(opts),
}
}

// AttributesAll returns a schema.SingleNestedAttribute
// which contains attributes for each of create, read, update and delete. Each
// attribute is defined as types.StringType and optional. A validator is used to
// verify that the value assigned to an attribute can be parsed as time.Duration.
func AttributesAll(ctx context.Context) schema.Attribute {
return Attributes(ctx, Opts{
Create: true,
Read: true,
Update: true,
Delete: true,
})
}

func attributesMap(opts Opts) map[string]schema.Attribute {
attributes := map[string]schema.Attribute{}
attribute := schema.StringAttribute{
Optional: true,
Validators: []validator.String{
validators.TimeDurationString(),
},
}

if opts.Create {
attributes[attributeNameCreate] = attribute
}

if opts.Read {
attributes[attributeNameRead] = attribute
}

if opts.Update {
attributes[attributeNameUpdate] = attribute
}

if opts.Delete {
attributes[attributeNameDelete] = attribute
}

return attributes
}
Loading

0 comments on commit aa0bcd6

Please sign in to comment.