Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

function/parse_duration: Add duration parsing function #350

Merged
merged 8 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/unreleased/FEATURES-20250123-191529.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: FEATURES
body: 'functions/duration_parse: Added a new `duration_parse` function that parses [Go duration strings](https://pkg.go.dev/time#ParseDuration).'
time: 2025-01-23T19:15:29.754845+01:00
custom:
Issue: "350"
54 changes: 54 additions & 0 deletions docs/functions/duration_parse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
page_title: "duration_parse function - terraform-provider-time"
subcategory: ""
description: |-
Parse a Go duration string https://pkg.go.dev/time#ParseDuration into an object
---

# function: duration_parse

Given a [Go duration string](https://pkg.go.dev/time#ParseDuration), will parse and return an object representation of that duration.

## Example Usage

```terraform
# Configuration using provider functions must include required_providers configuration.
terraform {
required_providers {
time = {
source = "hashicorp/time"
# Setting the provider version is a strongly recommended practice
# version = "..."
}
}
# Provider functions require Terraform 1.8 and later.
required_version = ">= 1.8.0"
}

output "example_output" {
value = provider::time::duration_parse("1h")
}
```

## Signature

<!-- signature generated by tfplugindocs -->
```text
duration_parse(duration string) object
```

## Arguments

<!-- arguments generated by tfplugindocs -->
1. `duration` (String) Go time package duration string to parse


## Return Type

The `object` returned from `duration_parse` has the following attributes:
- `hours` (Number) The duration as a floating point number of hours.
- `minutes` (Number) The duration as a floating point number of minutes.
- `seconds` (Number) The duration as a floating point number of seconds.
- `milliseconds` (Number) The duration as an integer number of milliseconds.
- `microseconds` (Number) The duration as an integer number of microseconds.
- `nanoseconds` (Number) The duration as an integer number of nanoseconds.
16 changes: 16 additions & 0 deletions examples/functions/duration_parse/function.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Configuration using provider functions must include required_providers configuration.
terraform {
required_providers {
time = {
source = "hashicorp/time"
# Setting the provider version is a strongly recommended practice
# version = "..."
}
}
# Provider functions require Terraform 1.8 and later.
required_version = ">= 1.8.0"
}

output "example_output" {
value = provider::time::duration_parse("1h")
}
91 changes: 91 additions & 0 deletions internal/provider/function_duration_parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"context"
"fmt"
"time"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

var durationParseReturnAttrTypes = map[string]attr.Type{
"hours": types.Float64Type,
"minutes": types.Float64Type,
"seconds": types.Float64Type,
"milliseconds": types.Int64Type,
"microseconds": types.Int64Type,
"nanoseconds": types.Int64Type,
}

var _ function.Function = &DurationParseFunction{}

type DurationParseFunction struct{}

func NewDurationParseFunction() function.Function {
return &DurationParseFunction{}
}

func (f *DurationParseFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) {
resp.Name = "duration_parse"
}

func (f *DurationParseFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Summary: "Parse a [Go duration string](https://pkg.go.dev/time#ParseDuration) into an object",
Description: "Given a [Go duration string](https://pkg.go.dev/time#ParseDuration), will parse and return an object representation of that duration.",

Parameters: []function.Parameter{
function.StringParameter{
Name: "duration",
Description: "Go time package duration string to parse",
},
},
Return: function.ObjectReturn{
AttributeTypes: durationParseReturnAttrTypes,
},
}
}

func (f *DurationParseFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var input string

resp.Error = req.Arguments.Get(ctx, &input)
if resp.Error != nil {
return
}

duration, err := time.ParseDuration(input)
if err != nil {
// Intentionally not including the Go parse error in the return diagnostic, as the message is based on a Go-specific
// reference time that may be unfamiliar to practitioners
tflog.Error(ctx, fmt.Sprintf("failed to parse duration string, underlying time.Duration error: %s", err.Error()))

resp.Error = function.NewArgumentFuncError(0, fmt.Sprintf("Error parsing duration string: %q is not a valid duration string", input))
return
}

durationObj, diags := types.ObjectValue(
durationParseReturnAttrTypes,
map[string]attr.Value{
"hours": types.Float64Value(duration.Hours()),
"minutes": types.Float64Value(duration.Minutes()),
"seconds": types.Float64Value(duration.Seconds()),
"milliseconds": types.Int64Value(duration.Milliseconds()),
"microseconds": types.Int64Value(duration.Microseconds()),
"nanoseconds": types.Int64Value(duration.Nanoseconds()),
},
)

resp.Error = function.FuncErrorFromDiags(ctx, diags)
if resp.Error != nil {
return
}

resp.Error = resp.Result.Set(ctx, &durationObj)
}
125 changes: 125 additions & 0 deletions internal/provider/function_duration_parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/plancheck"
"github.com/hashicorp/terraform-plugin-testing/tfversion"
)

func TestDurationParse_valid(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_8_0),
},
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
Config: `
output "test" {
value = provider::time::duration_parse("1h")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectKnownOutputValue("test", knownvalue.ObjectExact(
map[string]knownvalue.Check{
"hours": knownvalue.Float64Exact(1),
"minutes": knownvalue.Float64Exact(60),
"seconds": knownvalue.Float64Exact(3600),
"milliseconds": knownvalue.Int64Exact(3600000),
"microseconds": knownvalue.Int64Exact(3600000000),
"nanoseconds": knownvalue.Int64Exact(3600000000000),
},
)),
},
},
},
{
Config: `
output "test" {
value = provider::time::duration_parse("60m")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
{
Config: `
output "test" {
value = provider::time::duration_parse("3600s")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
{
Config: `
output "test" {
value = provider::time::duration_parse("3600000ms")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
{
Config: `
output "test" {
value = provider::time::duration_parse("3600000000us")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
{
Config: `
output "test" {
value = provider::time::duration_parse("3600000000000ns")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
},
})
}

func TestDurationParse_invalid(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_8_0),
},
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
Config: `
output "test" {
value = provider::time::duration_parse("abcdef")
}
`,
ExpectError: regexp.MustCompile(`"abcdef" is not a valid duration string.`),
},
},
})
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func (p *timeProvider) Schema(context.Context, provider.SchemaRequest, *provider

func (p *timeProvider) Functions(ctx context.Context) []func() function.Function {
return []func() function.Function{
NewDurationParseFunction,
NewRFC3339ParseFunction,
}
}
37 changes: 37 additions & 0 deletions templates/functions/duration_parse.md.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}"
subcategory: ""
description: |-
{{ .Summary | plainmarkdown | trimspace | prefixlines " " }}
---

# {{.Type}}: {{.Name}}

{{ .Description | trimspace }}

{{ if .HasExample -}}
## Example Usage

{{tffile .ExampleFile }}
{{- end }}

## Signature

{{ .FunctionSignatureMarkdown }}

## Arguments

{{ .FunctionArgumentsMarkdown }}
{{ if .HasVariadic -}}
{{ .FunctionVariadicArgumentMarkdown }}
{{- end }}

## Return Type

The `object` returned from `duration_parse` has the following attributes:
- `hours` (Number) The duration as a floating point number of hours.
- `minutes` (Number) The duration as a floating point number of minutes.
- `seconds` (Number) The duration as a floating point number of seconds.
- `milliseconds` (Number) The duration as an integer number of milliseconds.
- `microseconds` (Number) The duration as an integer number of microseconds.
- `nanoseconds` (Number) The duration as an integer number of nanoseconds.