Skip to content

Commit

Permalink
function/parse_duration: Add duration parsing function
Browse files Browse the repository at this point in the history
  • Loading branch information
ricardbejarano committed Sep 4, 2024
1 parent e284774 commit 95b6f62
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 0 deletions.
54 changes: 54 additions & 0 deletions docs/functions/parse_duration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
page_title: "parse_duration function - terraform-provider-time"
subcategory: ""
description: |-
Parse a duration string into an object
---

# function: parse_duration

Given a duration string, 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::parse_duration("1h")
}
```

## Signature

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

## Arguments

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


## Return Type

The `object` returned from `parse_duration` 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/parse_duration/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::parse_duration("1h")
}
91 changes: 91 additions & 0 deletions internal/provider/function_parse_duration.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 parseDurationReturnAttrTypes = 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 = &ParseDurationFunction{}

type ParseDurationFunction struct{}

func NewParseDurationFunction() function.Function {
return &ParseDurationFunction{}
}

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

func (f *ParseDurationFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Summary: "Parse a duration string into an object",
Description: "Given a duration string, 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: parseDurationReturnAttrTypes,
},
}
}

func (f *ParseDurationFunction) 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(
parseDurationReturnAttrTypes,
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)
}
126 changes: 126 additions & 0 deletions internal/provider/function_parse_duration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"regexp"
"testing"
"time"

"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 TestParseDuration_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::parse_duration("1h")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectKnownOutputValue("test", knownvalue.ObjectExact(
map[string]knownvalue.Check{
"hours": knownvalue.Float64Exact(time.Hour.Hours()),
"minutes": knownvalue.Float64Exact(time.Hour.Minutes()),
"seconds": knownvalue.Float64Exact(time.Hour.Seconds()),
"milliseconds": knownvalue.Int64Exact(time.Hour.Milliseconds()),
"microseconds": knownvalue.Int64Exact(time.Hour.Microseconds()),
"nanoseconds": knownvalue.Int64Exact(time.Hour.Nanoseconds()),
},
)),
},
},
},
{
Config: `
output "test" {
value = provider::time::parse_duration("60m")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
{
Config: `
output "test" {
value = provider::time::parse_duration("3600s")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
{
Config: `
output "test" {
value = provider::time::parse_duration("3600000ms")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
{
Config: `
output "test" {
value = provider::time::parse_duration("3600000000us")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
{
Config: `
output "test" {
value = provider::time::parse_duration("3600000000000ns")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
},
})
}

func TestParseDuration_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::parse_duration("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{
NewParseDurationFunction,
NewRFC3339ParseFunction,
}
}
37 changes: 37 additions & 0 deletions templates/functions/parse_duration.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 `parse_duration` 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.

0 comments on commit 95b6f62

Please sign in to comment.