-
Notifications
You must be signed in to change notification settings - Fork 9.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
685 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package types | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/hashicorp/terraform-plugin-framework/attr" | ||
"github.com/hashicorp/terraform-plugin-framework/attr/xattr" | ||
"github.com/hashicorp/terraform-plugin-framework/diag" | ||
"github.com/hashicorp/terraform-plugin-framework/path" | ||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes" | ||
"github.com/hashicorp/terraform-plugin-go/tftypes" | ||
) | ||
|
||
type TimestampType struct { | ||
basetypes.StringType | ||
} | ||
|
||
var ( | ||
_ basetypes.StringTypable = TimestampType{} | ||
_ xattr.TypeWithValidate = TimestampType{} | ||
) | ||
|
||
func (typ TimestampType) ValueFromString(_ context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) { | ||
if in.IsUnknown() { | ||
return NewTimestampUnknown(), nil | ||
} | ||
|
||
if in.IsNull() { | ||
return NewTimestampNull(), nil | ||
} | ||
|
||
s := in.ValueString() | ||
|
||
var diags diag.Diagnostics | ||
t, err := time.Parse(time.RFC3339, s) | ||
if err != nil { | ||
diags.AddError( | ||
"Timestamp Type Validation Error", | ||
fmt.Sprintf("Value %q cannot be parsed as a Timestamp.", s), | ||
) | ||
return nil, diags | ||
} | ||
|
||
return newTimestampValue(s, t), nil | ||
} | ||
|
||
func (typ TimestampType) ValueFromTerraform(_ context.Context, in tftypes.Value) (attr.Value, error) { | ||
if !in.IsKnown() { | ||
return NewTimestampUnknown(), nil | ||
} | ||
|
||
if in.IsNull() { | ||
return NewTimestampNull(), nil | ||
} | ||
|
||
var s string | ||
err := in.As(&s) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
t, err := time.Parse(time.RFC3339, s) | ||
if err != nil { | ||
return NewTimestampUnknown(), nil //nolint: nilerr // Must not return validation errors | ||
} | ||
|
||
return newTimestampValue(s, t), nil | ||
} | ||
|
||
func (typ TimestampType) ValueType(context.Context) attr.Value { | ||
return TimestampValue{} | ||
} | ||
|
||
func (typ TimestampType) Equal(o attr.Type) bool { | ||
other, ok := o.(TimestampType) | ||
if !ok { | ||
return false | ||
} | ||
|
||
return typ.StringType.Equal(other.StringType) | ||
} | ||
|
||
// String returns a human-friendly description of the TimestampType. | ||
func (typ TimestampType) String() string { | ||
return "types.TimestampType" | ||
} | ||
|
||
func (typ TimestampType) Validate(ctx context.Context, in tftypes.Value, path path.Path) diag.Diagnostics { | ||
var diags diag.Diagnostics | ||
|
||
if !in.IsKnown() || in.IsNull() { | ||
return diags | ||
} | ||
|
||
var s string | ||
err := in.As(&s) | ||
if err != nil { | ||
diags.AddAttributeError( | ||
path, | ||
"Invalid Terraform Value", | ||
"An unexpected error occurred while attempting to convert a Terraform value to a string. "+ | ||
"This is generally an issue with the provider schema implementation. "+ | ||
"Please report the following to the provider developer:\n\n"+ | ||
"Path: "+path.String()+"\n"+ | ||
"Error: "+err.Error(), | ||
) | ||
return diags | ||
} | ||
|
||
_, err = time.Parse(time.RFC3339, s) | ||
if err != nil { | ||
diags.AddAttributeError( | ||
path, | ||
"Invalid Timestamp Value", | ||
fmt.Sprintf("Value %q cannot be parsed as an RFC 3339 Timestamp.\n\n"+ | ||
"Path: %s\n"+ | ||
"Error: %s", s, path, err), | ||
) | ||
return diags | ||
} | ||
|
||
return diags | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
package types_test | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
"time" | ||
|
||
"github.com/hashicorp/terraform-plugin-framework/attr" | ||
"github.com/hashicorp/terraform-plugin-framework/path" | ||
"github.com/hashicorp/terraform-plugin-go/tftypes" | ||
fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" | ||
) | ||
|
||
func TestTimestampTypeValueFromTerraform(t *testing.T) { | ||
t.Parallel() | ||
|
||
tests := map[string]struct { | ||
val tftypes.Value | ||
expected attr.Value | ||
}{ | ||
"null value": { | ||
val: tftypes.NewValue(tftypes.String, nil), | ||
expected: fwtypes.NewTimestampNull(), | ||
}, | ||
"unknown value": { | ||
val: tftypes.NewValue(tftypes.String, tftypes.UnknownValue), | ||
expected: fwtypes.NewTimestampUnknown(), | ||
}, | ||
"valid timestamp UTC": { | ||
val: tftypes.NewValue(tftypes.String, "2023-06-07T15:11:34Z"), | ||
expected: fwtypes.NewTimestampValue(time.Date(2023, time.June, 7, 15, 11, 34, 0, time.UTC)), | ||
}, | ||
"valid timestamp zone": { | ||
val: tftypes.NewValue(tftypes.String, "2023-06-07T15:11:34-06:00"), | ||
expected: fwtypes.NewTimestampValue(time.Date(2023, time.June, 7, 15, 11, 34, 0, locationFromString(t, "America/Regina"))), // No DST | ||
}, | ||
"invalid value": { | ||
val: tftypes.NewValue(tftypes.String, "not ok"), | ||
expected: fwtypes.NewTimestampUnknown(), | ||
}, | ||
"invalid no zone": { | ||
val: tftypes.NewValue(tftypes.String, "2023-06-07T15:11:34"), | ||
expected: fwtypes.NewTimestampUnknown(), | ||
}, | ||
"invalid date only": { | ||
val: tftypes.NewValue(tftypes.String, "2023-06-07Z"), | ||
expected: fwtypes.NewTimestampUnknown(), | ||
}, | ||
} | ||
|
||
for name, test := range tests { | ||
name, test := name, test | ||
t.Run(name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
ctx := context.Background() | ||
val, err := fwtypes.TimestampType{}.ValueFromTerraform(ctx, test.val) | ||
|
||
if err != nil { | ||
t.Fatalf("got unexpected error: %s", err) | ||
} | ||
|
||
if !test.expected.Equal(val) { | ||
t.Errorf("unexpected diff\nwanted: %s\ngot: %s", test.expected, val) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestTimestampTypeValidate(t *testing.T) { | ||
t.Parallel() | ||
|
||
type testCase struct { | ||
val tftypes.Value | ||
expectError bool | ||
} | ||
tests := map[string]testCase{ | ||
"not a string": { | ||
val: tftypes.NewValue(tftypes.Bool, true), | ||
expectError: true, | ||
}, | ||
"unknown string": { | ||
val: tftypes.NewValue(tftypes.String, tftypes.UnknownValue), | ||
}, | ||
"null string": { | ||
val: tftypes.NewValue(tftypes.String, nil), | ||
}, | ||
"valid timestamp UTC": { | ||
val: tftypes.NewValue(tftypes.String, "2023-06-07T15:11:34Z"), | ||
}, | ||
"valid timestamp zone": { | ||
val: tftypes.NewValue(tftypes.String, "2023-06-07T15:11:34-06:00"), | ||
}, | ||
"invalid string": { | ||
val: tftypes.NewValue(tftypes.String, "not ok"), | ||
expectError: true, | ||
}, | ||
"invalid no zone": { | ||
val: tftypes.NewValue(tftypes.String, "2023-06-07T15:11:34"), | ||
expectError: true, | ||
}, | ||
"invalid date only": { | ||
val: tftypes.NewValue(tftypes.String, "2023-06-07Z"), | ||
expectError: true, | ||
}, | ||
} | ||
|
||
for name, test := range tests { | ||
name, test := name, test | ||
t.Run(name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
ctx := context.Background() | ||
|
||
diags := fwtypes.TimestampType{}.Validate(ctx, test.val, path.Root("test")) | ||
|
||
if !diags.HasError() && test.expectError { | ||
t.Fatal("expected error, got no error") | ||
} | ||
|
||
if diags.HasError() && !test.expectError { | ||
t.Fatalf("got unexpected error: %#v", diags) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func locationFromString(t *testing.T, s string) *time.Location { | ||
location, err := time.LoadLocation(s) | ||
if err != nil { | ||
t.Fatalf("loading time.Location %q: %s", s, err) | ||
} | ||
|
||
return location | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package types | ||
|
||
import ( | ||
"context" | ||
"time" | ||
|
||
"github.com/hashicorp/terraform-plugin-framework/attr" | ||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes" | ||
) | ||
|
||
func NewTimestampNull() TimestampValue { | ||
return TimestampValue{ | ||
StringValue: types.StringNull(), | ||
} | ||
} | ||
|
||
func NewTimestampUnknown() TimestampValue { | ||
return TimestampValue{ | ||
StringValue: types.StringUnknown(), | ||
} | ||
} | ||
|
||
func NewTimestampValue(t time.Time) TimestampValue { | ||
return newTimestampValue(t.Format(time.RFC3339), t) | ||
} | ||
|
||
func NewTimestampValueString(s string) (TimestampValue, error) { | ||
t, err := time.Parse(time.RFC3339, s) | ||
if err != nil { | ||
return TimestampValue{}, err | ||
} | ||
return newTimestampValue(s, t), nil | ||
} | ||
|
||
func newTimestampValue(s string, t time.Time) TimestampValue { | ||
return TimestampValue{ | ||
StringValue: types.StringValue(s), | ||
value: t, | ||
} | ||
} | ||
|
||
var ( | ||
_ basetypes.StringValuable = TimestampValue{} | ||
) | ||
|
||
type TimestampValue struct { | ||
basetypes.StringValue | ||
|
||
// value contains the parsed value, if not Null or Unknown. | ||
value time.Time | ||
} | ||
|
||
func (val TimestampValue) Type(_ context.Context) attr.Type { | ||
return TimestampType{} | ||
} | ||
|
||
func (val TimestampValue) Equal(other attr.Value) bool { | ||
o, ok := other.(TimestampValue) | ||
|
||
if !ok { | ||
return false | ||
} | ||
|
||
if val.StringValue.IsUnknown() { | ||
return o.StringValue.IsUnknown() | ||
} | ||
|
||
if val.StringValue.IsNull() { | ||
return o.StringValue.IsNull() | ||
} | ||
|
||
return val.value.Equal(o.value) | ||
} | ||
|
||
// ValueTimestamp returns the known time.Time value. If Timestamp is null or unknown, returns 0. | ||
// To get the value as a string, use ValueString. | ||
func (val TimestampValue) ValueTimestamp() time.Time { | ||
return val.value | ||
} |
Oops, something went wrong.