Skip to content

Commit

Permalink
function/parse_duration: Add duration parsing function (#350)
Browse files Browse the repository at this point in the history
* function/parse_duration: Add duration parsing function

* Renamed parse_duration to duration_parse

* Added link to Go duration strings in duration_parse function summary and description

* Use static assertions in duration_parse tests instead

* Added CHANGELOG entry

* Removed unused import

* Updated time.ParseDuration

* Output of make generate
  • Loading branch information
ricardbejarano authored Feb 13, 2025
1 parent 6911c5d commit f73bd87
Show file tree
Hide file tree
Showing 7 changed files with 329 additions and 0 deletions.
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.

1 comment on commit f73bd87

@dongho-jung
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 🚀 🚀

Please sign in to comment.