Documentation
¶
Index ¶
- Variables
- func Externs() map[string][]byte
- func GetDuration(val cty.Value) (time.Duration, error)
- func GetTime(val cty.Value) (time.Time, error)
- func GetTimeFunctions() map[string]function.Function
- func NewDurationCapsule(d time.Duration) cty.Value
- func NewTimeCapsule(t time.Time) cty.Value
- type Duration
- type Timestamp
Constants ¶
This section is empty.
Variables ¶
var AbsDurationFunc = function.New(&function.Spec{ Description: "A duration's magnitude, discarding its sign.", Params: []function.Parameter{ { Name: "d", Type: DurationCapsuleType, Description: "The duration.", }, }, Type: function.StaticReturnType(DurationCapsuleType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { d, err := GetDuration(args[0]) if err != nil { return cty.NilVal, err } if d < 0 { d = -d } return NewDurationCapsule(d), nil }, })
AbsDurationFunc returns the absolute value of a duration.
var AddDaysFunc = function.New(&function.Spec{ Description: "A time shifted by whole days. Negative n moves backwards.", Params: []function.Parameter{ { Name: "t", Type: TimeCapsuleType, Description: "The time to shift.", }, { Name: "n", Type: cty.Number, Description: "Days to add; truncated to a whole number.", }, }, Type: function.StaticReturnType(TimeCapsuleType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { t, err := GetTime(args[0]) if err != nil { return cty.NilVal, err } n, _ := args[1].AsBigFloat().Int64() return NewTimeCapsule(t.AddDate(0, 0, int(n))), nil }, })
AddDaysFunc adds n calendar days to a time (calls time.Time.AddDate).
var AddMonthsFunc = function.New(&function.Spec{ Description: "A time shifted by whole calendar months. Negative n moves backwards; a day-of-month that the target month lacks rolls forward, as Go's AddDate does.", Params: []function.Parameter{ { Name: "t", Type: TimeCapsuleType, Description: "The time to shift.", }, { Name: "n", Type: cty.Number, Description: "Months to add; truncated to a whole number.", }, }, Type: function.StaticReturnType(TimeCapsuleType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { t, err := GetTime(args[0]) if err != nil { return cty.NilVal, err } n, _ := args[1].AsBigFloat().Int64() return NewTimeCapsule(t.AddDate(0, int(n), 0)), nil }, })
AddMonthsFunc adds n calendar months to a time (calls time.Time.AddDate).
var AddYearsFunc = function.New(&function.Spec{ Description: "A time shifted by whole calendar years. Negative n moves backwards.", Params: []function.Parameter{ { Name: "t", Type: TimeCapsuleType, Description: "The time to shift.", }, { Name: "n", Type: cty.Number, Description: "Years to add; truncated to a whole number.", }, }, Type: function.StaticReturnType(TimeCapsuleType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { t, err := GetTime(args[0]) if err != nil { return cty.NilVal, err } n, _ := args[1].AsBigFloat().Int64() return NewTimeCapsule(t.AddDate(int(n), 0, 0)), nil }, })
AddYearsFunc adds n calendar years to a time (calls time.Time.AddDate).
var DurationAddFunc = function.New(&function.Spec{ Description: "The sum of two durations.", Params: []function.Parameter{ { Name: "d1", Type: DurationCapsuleType, Description: "The first duration.", }, { Name: "d2", Type: DurationCapsuleType, Description: "The duration to add.", }, }, Type: function.StaticReturnType(DurationCapsuleType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { d1, _ := GetDuration(args[0]) d2, _ := GetDuration(args[1]) return NewDurationCapsule(d1 + d2), nil }, })
DurationAddFunc adds two durations: d1 + d2
var DurationCapsuleType = cty.CapsuleWithOps("duration", reflect.TypeOf(Duration{}), &cty.CapsuleOps{ Equals: func(a, b any) cty.Value { da := a.(*Duration) db := b.(*Duration) return cty.BoolVal(da.Duration == db.Duration) }, RawEquals: func(a, b any) bool { da := a.(*Duration) db := b.(*Duration) return da.Duration == db.Duration }, GoString: func(val any) string { return fmt.Sprintf("duration(%q)", val.(*Duration).Duration.String()) }, TypeGoString: func(_ reflect.Type) string { return "duration" }, })
DurationCapsuleType is a cty capsule type wrapping Duration. Supports equality (==, !=) via Equals/RawEquals. Note: ordering operators (<, >, etc.) are not available for capsule types in go-cty — use get(d, unit) (via rich-cty-types) to extract a numeric value and compare that instead.
var DurationDivFunc = function.New(&function.Spec{ Description: "A duration divided by a divisor. Dividing by zero is an error.", Params: []function.Parameter{ { Name: "d", Type: DurationCapsuleType, Description: "The duration to divide.", }, { Name: "n", Type: cty.Number, Description: "The divisor; may be fractional.", }, }, Type: function.StaticReturnType(DurationCapsuleType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { d, _ := GetDuration(args[0]) n, _ := args[1].AsBigFloat().Float64() if n == 0 { return cty.NilVal, fmt.Errorf("duration::div: division by zero") } return NewDurationCapsule(time.Duration(float64(d) / n)), nil }, })
DurationDivFunc divides a duration by a scalar: d / n (returns duration)
var DurationFunc = function.New(&function.Spec{ Description: `A duration, from a string ("1h30m" or ISO 8601 "PT1H30M") or from a number and a unit. The first argument's type therefore decides how many arguments there are, which is why the externs declare this as two forms.`, Params: []function.Parameter{ { Name: "val", Type: cty.DynamicPseudoType, Description: "A duration string, or the number of `unit`s.", }, }, VarParam: &function.Parameter{ Name: "unit", Type: cty.String, Description: `One of "h", "m", "s", "ms", "us", "ns". Required when val is a number; not allowed when it is a string.`, }, Type: func(args []cty.Value) (cty.Type, error) { switch len(args) { case 1: t := args[0].Type() if t != cty.String && t != cty.DynamicPseudoType { return cty.NilType, fmt.Errorf("duration() 1-arg form requires a string, got %s", t.FriendlyName()) } return DurationCapsuleType, nil case 2: t0, t1 := args[0].Type(), args[1].Type() if t0 != cty.Number && t0 != cty.DynamicPseudoType { return cty.NilType, fmt.Errorf("duration() 2-arg form requires a number as first argument, got %s", t0.FriendlyName()) } if t1 != cty.String && t1 != cty.DynamicPseudoType { return cty.NilType, fmt.Errorf("duration() 2-arg form requires a string unit as second argument, got %s", t1.FriendlyName()) } return DurationCapsuleType, nil default: return cty.NilType, fmt.Errorf("duration() requires 1 or 2 arguments, got %d", len(args)) } }, Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { if len(args) == 1 { return parseDurationString(args[0].AsString()) } n, _ := args[0].AsBigFloat().Float64() return durationFromNumber(n, args[1].AsString()) }, })
DurationFunc creates a duration from a string or from a number and unit. Called as duration("5m"), duration("PT5M"), or duration(5, "m").
var DurationGtFunc = function.New(&function.Spec{ Description: "Whether d1 is longer than d2. Signed: 1s is greater than -1h.", Params: []function.Parameter{ { Name: "d1", Type: DurationCapsuleType, Description: "The duration to test.", }, { Name: "d2", Type: DurationCapsuleType, Description: "The duration to compare against.", }, }, Type: function.StaticReturnType(cty.Bool), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { d1, err := GetDuration(args[0]) if err != nil { return cty.NilVal, err } d2, err := GetDuration(args[1]) if err != nil { return cty.NilVal, err } return cty.BoolVal(d1 > d2), nil }, })
DurationGtFunc returns true if d1 > d2.
var DurationLtFunc = function.New(&function.Spec{ Description: "Whether d1 is shorter than d2. Signed: -1h is less than 1s.", Params: []function.Parameter{ { Name: "d1", Type: DurationCapsuleType, Description: "The duration to test.", }, { Name: "d2", Type: DurationCapsuleType, Description: "The duration to compare against.", }, }, Type: function.StaticReturnType(cty.Bool), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { d1, err := GetDuration(args[0]) if err != nil { return cty.NilVal, err } d2, err := GetDuration(args[1]) if err != nil { return cty.NilVal, err } return cty.BoolVal(d1 < d2), nil }, })
DurationLtFunc returns true if d1 < d2.
var DurationMulFunc = function.New(&function.Spec{ Description: "A duration scaled by a factor.", Params: []function.Parameter{ { Name: "d", Type: DurationCapsuleType, Description: "The duration to scale.", }, { Name: "n", Type: cty.Number, Description: "The factor; may be fractional.", }, }, Type: function.StaticReturnType(DurationCapsuleType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { d, _ := GetDuration(args[0]) n, _ := args[1].AsBigFloat().Float64() return NewDurationCapsule(time.Duration(float64(d) * n)), nil }, })
DurationMulFunc multiplies a duration by a scalar: d * n
var DurationRoundFunc = function.New(&function.Spec{ Description: "A duration rounded to the nearest multiple of m, halves away from zero.", Params: []function.Parameter{ { Name: "d", Type: DurationCapsuleType, Description: "The duration to round.", }, { Name: "m", Type: DurationCapsuleType, Description: "The multiple to round to — itself a duration, e.g. duration(\"1m\").", }, }, Type: function.StaticReturnType(DurationCapsuleType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { d, _ := GetDuration(args[0]) m, _ := GetDuration(args[1]) return NewDurationCapsule(d.Round(m)), nil }, })
DurationRoundFunc rounds d to the nearest multiple of m: d.Round(m)
var DurationSubFunc = function.New(&function.Spec{ Description: "The difference between two durations. Negative if d2 is the longer.", Params: []function.Parameter{ { Name: "d1", Type: DurationCapsuleType, Description: "The duration to subtract from.", }, { Name: "d2", Type: DurationCapsuleType, Description: "The duration to subtract.", }, }, Type: function.StaticReturnType(DurationCapsuleType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { d1, _ := GetDuration(args[0]) d2, _ := GetDuration(args[1]) return NewDurationCapsule(d1 - d2), nil }, })
DurationSubFunc subtracts durations: d1 - d2
var DurationTruncateFunc = function.New(&function.Spec{ Description: "A duration rounded down to a multiple of m.", Params: []function.Parameter{ { Name: "d", Type: DurationCapsuleType, Description: "The duration to truncate.", }, { Name: "m", Type: DurationCapsuleType, Description: "The multiple to truncate to — itself a duration, e.g. duration(\"1m\").", }, }, Type: function.StaticReturnType(DurationCapsuleType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { d, _ := GetDuration(args[0]) m, _ := GetDuration(args[1]) return NewDurationCapsule(d.Truncate(m)), nil }, })
DurationTruncateFunc truncates d to a multiple of m: d.Truncate(m)
var FormatDurationFunc = function.New(&function.Spec{ Description: "Render a duration.", Params: []function.Parameter{ { Name: "d", Type: DurationCapsuleType, Description: "The duration to render.", }, }, VarParam: &function.Parameter{ Name: "fmt", Type: cty.String, Description: `"go" ("1h30m0s") or "iso" ("PT1H30M"); defaults to "go".`, }, Type: boundedArity("duration::format", 2, cty.String), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { d, err := GetDuration(args[0]) if err != nil { return cty.NilVal, err } format := "go" if len(args) > 1 { format = args[1].AsString() } switch format { case "go", "": return cty.StringVal(d.String()), nil case "iso": return cty.StringVal(durationToISO8601(d)), nil default: return cty.NilVal, fmt.Errorf("duration::format: unknown format %q; valid values are \"go\" and \"iso\"", format) } }, })
FormatDurationFunc formats a duration as a string. Called as duration::format(d) for Go format (default) or duration::format(d, "iso") for ISO 8601.
var FormatTimeFunc = function.New(&function.Spec{ Description: "Render a time with a Go reference layout.", Params: []function.Parameter{ { Name: "format", Type: cty.String, Description: "A Go reference layout (\"2006-01-02\"), or an @alias such as @rfc3339.", }, { Name: "t", Type: TimeCapsuleType, Description: "The time to render.", }, }, Type: function.StaticReturnType(cty.String), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { layout, err := resolveFormat(args[0].AsString()) if err != nil { return cty.NilVal, err } t, err := GetTime(args[1]) if err != nil { return cty.NilVal, err } return cty.StringVal(t.Format(layout)), nil }, })
FormatTimeFunc formats a time value using Go's reference-time format or a @name alias. Called as time::format("2006-01-02", t) or time::format("@rfc3339", t).
var FromUnixFunc = function.New(&function.Spec{ Description: "A time from a Unix epoch count. Always returns a UTC time.", Params: []function.Parameter{ { Name: "n", Type: cty.Number, Description: "The epoch count; fractional seconds are honored.", }, }, VarParam: &function.Parameter{ Name: "unit", Type: cty.String, Description: `One of "s", "ms", "us", "ns"; defaults to "s".`, }, Type: boundedArity("time::from_unix", 2, TimeCapsuleType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { unit := "s" if len(args) > 1 { unit = args[1].AsString() } n, _ := args[0].AsBigFloat().Float64() switch unit { case "s": secs := int64(n) nanos := int64((n - float64(secs)) * 1e9) return NewTimeCapsule(time.Unix(secs, nanos).UTC()), nil case "ms": return NewTimeCapsule(time.UnixMilli(int64(n)).UTC()), nil case "us": return NewTimeCapsule(time.UnixMicro(int64(n)).UTC()), nil case "ns": return NewTimeCapsule(time.Unix(0, int64(n)).UTC()), nil default: return cty.NilVal, fmt.Errorf("time::from_unix: unknown unit %q; valid units: s, ms, us, ns", unit) } }, })
FromUnixFunc creates a time from a Unix epoch value. Called as time::from_unix(n) for seconds (possibly fractional), or time::from_unix(n, unit) where unit is "s", "ms", "us", or "ns". Always returns UTC.
var InTimezoneFunc = function.New(&function.Spec{ Description: "The same instant, displayed in another zone.", Params: []function.Parameter{ { Name: "t", Type: TimeCapsuleType, Description: "The time to convert.", }, { Name: "tz", Type: cty.String, Description: "IANA zone name, e.g. \"America/New_York\".", }, }, Type: function.StaticReturnType(TimeCapsuleType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { t, err := GetTime(args[0]) if err != nil { return cty.NilVal, err } loc, err := time.LoadLocation(args[1].AsString()) if err != nil { return cty.NilVal, fmt.Errorf("time::in_zone: invalid timezone %q: %s", args[1].AsString(), err) } return NewTimeCapsule(t.In(loc)), nil }, })
InTimezoneFunc re-expresses a time in a different IANA timezone. The instant is unchanged; only the displayed timezone changes.
var NextZoneSerialFunc = function.New(&function.Spec{ Description: "The next DNS zone serial: whichever is greater of s + 1 and today's date in the conventional YYYYMMDDNN form, so that the serial always advances.", Params: []function.Parameter{ { Name: "s", Type: cty.DynamicPseudoType, Description: "The current serial, as a number or a string.", }, }, VarParam: &function.Parameter{ Name: "t", Type: TimeCapsuleType, Description: "The date to advance to; defaults to now().", }, Type: func(args []cty.Value) (cty.Type, error) { if len(args) > 2 { return cty.NilType, fmt.Errorf("dns::next_zone_serial() takes 1 or 2 arguments") } t0 := args[0].Type() if t0 != cty.Number && t0 != cty.String && t0 != cty.DynamicPseudoType { return cty.NilType, fmt.Errorf("dns::next_zone_serial: serial must be a number or string, got %s", t0.FriendlyName()) } return cty.Number, nil }, Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { s, err := parseSerialArg(args[0], "dns::next_zone_serial") if err != nil { return cty.NilVal, err } var t time.Time if len(args) == 2 { t, err = GetTime(args[1]) if err != nil { return cty.NilVal, err } } else { t = time.Now() } year, month, day := t.Date() x := int64(year)*1_000_000 + int64(month)*10_000 + int64(day)*100 return cty.NumberIntVal(max(s+1, x)), nil }, })
NextZoneSerialFunc computes the next DNS zone serial number in YYYYMMDDNN format.
Called as dns::next_zone_serial(s) or dns::next_zone_serial(s, t).
s: current serial (number or string) t: optional time capsule; defaults to time::now()
Computes x = first serial of the day for t (YYYYMMDD * 100), then returns max(s+1, x).
var NowFunc = function.New(&function.Spec{ Description: "The current time. Without a zone, in the machine's local zone.", VarParam: &function.Parameter{ Name: "tz", Type: cty.String, Description: `IANA zone name, e.g. "America/New_York".`, }, Type: boundedArity("time::now", 1, TimeCapsuleType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { if len(args) == 0 { return NewTimeCapsule(time.Now()), nil } tzName := args[0].AsString() loc, err := time.LoadLocation(tzName) if err != nil { return cty.NilVal, fmt.Errorf("invalid timezone %q: %s", tzName, err) } return NewTimeCapsule(time.Now().In(loc)), nil }, })
NowFunc returns the current time, optionally in the given IANA timezone. Called as time::now() or time::now("America/New_York").
The optional tz is a VarParam because cty has no other way to spell an optional argument — see the externs, which declare the signature this cannot.
var ParseTimeFunc = function.New(&function.Spec{ Description: "Parse a timestamp. With one argument it reads RFC 3339; with two, the first is a Go reference layout or an @alias; a third interprets the parsed wall-clock in that zone. The meaning of the first argument therefore depends on how many there are, which is why the externs declare this as three forms.", VarParam: &function.Parameter{ Name: "args", Type: cty.String, Description: "The timestamp; or a format and then the timestamp; or a format, the timestamp, and an IANA zone.", }, Type: func(args []cty.Value) (cty.Type, error) { if len(args) < 1 || len(args) > 3 { return cty.NilType, fmt.Errorf("time::parse() takes 1 to 3 arguments") } return TimeCapsuleType, nil }, Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { switch len(args) { case 1: s := args[0].AsString() t, err := time.Parse(time.RFC3339Nano, s) if err != nil { return cty.NilVal, fmt.Errorf("time::parse: invalid RFC 3339 timestamp %q: %s", s, err) } return NewTimeCapsule(t), nil case 2: layout, err := resolveFormat(args[0].AsString()) if err != nil { return cty.NilVal, err } t, err := time.Parse(layout, args[1].AsString()) if err != nil { return cty.NilVal, fmt.Errorf("time::parse: cannot parse %q with format %q: %s", args[1].AsString(), args[0].AsString(), err) } return NewTimeCapsule(t), nil case 3: layout, err := resolveFormat(args[0].AsString()) if err != nil { return cty.NilVal, err } loc, err := time.LoadLocation(args[2].AsString()) if err != nil { return cty.NilVal, fmt.Errorf("time::parse: invalid timezone %q: %s", args[2].AsString(), err) } t, err := time.ParseInLocation(layout, args[1].AsString(), loc) if err != nil { return cty.NilVal, fmt.Errorf("time::parse: cannot parse %q with format %q: %s", args[1].AsString(), args[0].AsString(), err) } return NewTimeCapsule(t), nil default: return cty.NilVal, fmt.Errorf("time::parse() takes 1 to 3 arguments") } }, })
ParseTimeFunc parses a timestamp string into a time value.
Forms:
time::parse(s) — RFC 3339 (timezone required) time::parse(format, s) — parse s using Go layout (or @name alias) time::parse(format, s, tz) — same, but interpret s in the given IANA timezone
var ParseZoneSerialFunc = function.New(&function.Spec{ Description: "The date a DNS zone serial in YYYYMMDDNN form encodes, as a UTC midnight time. The NN revision suffix is discarded; an out-of-range month or day is snapped to the nearest valid date.", Params: []function.Parameter{ { Name: "s", Type: cty.DynamicPseudoType, Description: "The serial to decode, as a number or a string.", }, }, Type: func(args []cty.Value) (cty.Type, error) { t := args[0].Type() if t != cty.Number && t != cty.String && t != cty.DynamicPseudoType { return cty.NilType, fmt.Errorf("dns::parse_zone_serial: serial must be a number or string, got %s", t.FriendlyName()) } return TimeCapsuleType, nil }, Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { s, err := parseSerialArg(args[0], "dns::parse_zone_serial") if err != nil { return cty.NilVal, err } datepart := s / 100 year := int(datepart / 10_000) month := time.Month((datepart / 100) % 100) day := int(datepart % 100) if month < 1 { month = 1 } if month > 12 { month = 12 day = 31 } if day < 1 { day = 1 } if last := time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day(); day > last { day = last } return NewTimeCapsule(time.Date(year, month, day, 0, 0, 0, 0, time.UTC)), nil }, })
ParseZoneSerialFunc converts a DNS zone serial back to an approximate time value. The serial format is YYYYMMDDNN; the NN sequence number is ignored. For out-of-range date components, the nearest valid date is used: month > 12 → December 31; day > days in month → last day of month.
var SinceFunc = function.New(&function.Spec{ Description: "How long ago a time was: the duration from it until now. Negative if it is in the future.", Params: []function.Parameter{ { Name: "t", Type: TimeCapsuleType, Description: "The time to measure from.", }, }, Type: function.StaticReturnType(DurationCapsuleType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { t, err := GetTime(args[0]) if err != nil { return cty.NilVal, err } return NewDurationCapsule(time.Since(t)), nil }, })
SinceFunc returns the duration elapsed since the given time (equivalent to time::sub(time::now(), t)).
var StrftimeFunc = function.New(&function.Spec{ Description: "Render a time with a C-style strftime format. Unlike formattime, @aliases are not resolved here.", Params: []function.Parameter{ { Name: "format", Type: cty.String, Description: "A strftime-style layout, e.g. \"%Y-%m-%d\".", }, { Name: "t", Type: TimeCapsuleType, Description: "The time to render.", }, }, Type: function.StaticReturnType(cty.String), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { t, err := GetTime(args[1]) if err != nil { return cty.NilVal, err } return cty.StringVal(timefmt.Format(t, args[0].AsString())), nil }, })
StrftimeFunc formats a time using a strftime-style format string (via itchyny/timefmt-go). Called as time::strftime("%Y-%m-%d", t).
var StrptimeFunc = function.New(&function.Spec{ Description: "Parse a timestamp with a C-style strftime format. Unlike parsetime and formattime, @aliases are not resolved here.", Params: []function.Parameter{ { Name: "format", Type: cty.String, Description: "strftime-style layout, e.g. \"%Y-%m-%d\".", }, { Name: "s", Type: cty.String, Description: "The timestamp to parse.", }, }, VarParam: &function.Parameter{ Name: "tz", Type: cty.String, Description: "IANA zone; the parsed wall-clock is read as being in it. Optional.", }, Type: func(args []cty.Value) (cty.Type, error) { if len(args) > 3 { return cty.NilType, fmt.Errorf("time::strptime() takes 2 or 3 arguments") } return TimeCapsuleType, nil }, Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { t, err := timefmt.Parse(args[1].AsString(), args[0].AsString()) if err != nil { return cty.NilVal, fmt.Errorf("time::strptime: cannot parse %q with format %q: %s", args[1].AsString(), args[0].AsString(), err) } if len(args) == 3 { loc, err := time.LoadLocation(args[2].AsString()) if err != nil { return cty.NilVal, fmt.Errorf("time::strptime: invalid timezone %q: %s", args[2].AsString(), err) } t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), loc) } return NewTimeCapsule(t), nil }, })
StrptimeFunc parses a time string using a strftime-style format (via itchyny/timefmt-go). Called as time::strptime("%Y-%m-%d", "2024-01-15") or time::strptime("%Y-%m-%d", "2024-01-15", "UTC").
var TimeAddFunc = function.New(&function.Spec{ Description: "Add a duration to a time. Either argument may be given as a string: a timestamp as RFC 3339, a duration in Go syntax (\"1h30m\") or ISO 8601 (\"PT1H30M\").", Params: []function.Parameter{ { Name: "ts", Type: cty.DynamicPseudoType, Description: "The time to add to: a time, or an RFC 3339 string.", }, { Name: "dur", Type: cty.DynamicPseudoType, Description: "The duration to add: a duration, or a string in Go or ISO 8601 syntax.", }, }, Type: func(args []cty.Value) (cty.Type, error) { t0, t1 := args[0].Type(), args[1].Type() if t0 == cty.DynamicPseudoType || t1 == cty.DynamicPseudoType { return cty.DynamicPseudoType, nil } validTS := t0 == TimeCapsuleType || t0 == cty.String validDur := t1 == DurationCapsuleType || t1 == cty.String if validTS && validDur { return TimeCapsuleType, nil } return cty.NilType, fmt.Errorf("time::add: unsupported argument types %s and %s", t0.FriendlyName(), t1.FriendlyName()) }, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { // The time: a capsule, or an RFC 3339 (Nano) string. var t time.Time switch args[0].Type() { case cty.String: var err error t, err = time.Parse(time.RFC3339Nano, args[0].AsString()) if err != nil { return cty.NilVal, fmt.Errorf("time::add: invalid timestamp %q: %s", args[0].AsString(), err) } case TimeCapsuleType: var err error t, err = GetTime(args[0]) if err != nil { return cty.NilVal, err } default: return cty.NilVal, fmt.Errorf("time::add: first argument must be a time or string, got %s", args[0].Type().FriendlyName()) } // The duration: a capsule, or a string in Go or ISO 8601 syntax. var d time.Duration switch args[1].Type() { case cty.String: v, err := parseDurationString(args[1].AsString()) if err != nil { return cty.NilVal, err } d, err = GetDuration(v) if err != nil { return cty.NilVal, err } case DurationCapsuleType: var err error d, err = GetDuration(args[1]) if err != nil { return cty.NilVal, err } default: return cty.NilVal, fmt.Errorf("time::add: second argument must be a duration or string, got %s", args[1].Type().FriendlyName()) } return NewTimeCapsule(t.Add(d)), nil }, })
TimeAddFunc adds a duration to a time, and always returns a time.
Signatures:
time::add(time, duration) → time time::add(time, string) → time (string parsed as a duration) time::add(string, duration) → time (string parsed as RFC 3339 Nano) time::add(string, string) → time
It carries no stdlib-compatibility burden: cty's own stdlib.TimeAddFunc is the string-in/string-out `timeadd`, and a host that wants that behavior registers it directly. Shedding that duty is what lets every form here return a time — so the return type is static, and a string is parsed the same way whichever form it lands in. It used to be neither: (string, string) returned a *string* and accepted only Go duration syntax, so time::add("2024-01-01T00:00:00Z", "PT5M") failed while time::add(time::parse("2024-01-01T00:00:00Z"), "PT5M") succeeded.
var TimeAfterFunc = function.New(&function.Spec{ Description: "Whether t1 is later than t2.", Params: []function.Parameter{ { Name: "t1", Type: TimeCapsuleType, Description: "The time to test.", }, { Name: "t2", Type: TimeCapsuleType, Description: "The time to compare against.", }, }, Type: function.StaticReturnType(cty.Bool), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { t1, err := GetTime(args[0]) if err != nil { return cty.NilVal, err } t2, err := GetTime(args[1]) if err != nil { return cty.NilVal, err } return cty.BoolVal(t1.After(t2)), nil }, })
TimeAfterFunc returns true if t1 is after t2.
var TimeBeforeFunc = function.New(&function.Spec{ Description: "Whether t1 is earlier than t2.", Params: []function.Parameter{ { Name: "t1", Type: TimeCapsuleType, Description: "The time to test.", }, { Name: "t2", Type: TimeCapsuleType, Description: "The time to compare against.", }, }, Type: function.StaticReturnType(cty.Bool), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { t1, err := GetTime(args[0]) if err != nil { return cty.NilVal, err } t2, err := GetTime(args[1]) if err != nil { return cty.NilVal, err } return cty.BoolVal(t1.Before(t2)), nil }, })
TimeBeforeFunc returns true if t1 is before t2.
var TimeCapsuleType = cty.CapsuleWithOps("time", reflect.TypeOf(Timestamp{}), &cty.CapsuleOps{ Equals: func(a, b any) cty.Value { ta := a.(*Timestamp) tb := b.(*Timestamp) return cty.BoolVal(ta.Equal(tb.Time)) }, RawEquals: func(a, b any) bool { ta := a.(*Timestamp) tb := b.(*Timestamp) return ta.Equal(tb.Time) }, GoString: func(val any) string { return fmt.Sprintf("time(%q)", val.(*Timestamp).Format(time.RFC3339Nano)) }, TypeGoString: func(_ reflect.Type) string { return "time" }, })
TimeCapsuleType is a cty capsule type wrapping Timestamp. Supports equality (==, !=) via Equals/RawEquals. Note: ordering operators (<, >, etc.) are not available for capsule types in go-cty — use time::sub() and compare the resulting duration instead.
var TimeSubFunc = function.New(&function.Spec{ Description: "Subtract from a time: another time, giving the duration between them (negative if t2 is later), or a duration, giving the earlier time. The return type therefore depends on the arguments, which is why the externs declare this as two forms.", Params: []function.Parameter{ { Name: "t1", Type: TimeCapsuleType, Description: "The time to subtract from.", }, { Name: "t2", Type: cty.DynamicPseudoType, Description: "A time (giving the duration between) or a duration (giving the earlier time).", }, }, Type: func(args []cty.Value) (cty.Type, error) { t1 := args[1].Type() if t1 == cty.DynamicPseudoType { return cty.DynamicPseudoType, nil } switch t1 { case TimeCapsuleType: return DurationCapsuleType, nil case DurationCapsuleType: return TimeCapsuleType, nil default: return cty.NilType, fmt.Errorf("time::sub: second argument must be a time or duration, got %s", t1.FriendlyName()) } }, Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { t1, err := GetTime(args[0]) if err != nil { return cty.NilVal, err } switch args[1].Type() { case TimeCapsuleType: t2, err := GetTime(args[1]) if err != nil { return cty.NilVal, err } return NewDurationCapsule(t1.Sub(t2)), nil case DurationCapsuleType: d, err := GetDuration(args[1]) if err != nil { return cty.NilVal, err } return NewTimeCapsule(t1.Add(-d)), nil default: return cty.NilVal, fmt.Errorf("time::sub: second argument must be a time or duration, got %s", args[1].Type().FriendlyName()) } }, })
TimeSubFunc subtracts a time or duration from a time.
Signatures:
time::sub(time, time) → duration (elapsed from t2 to t1; negative if t1 < t2) time::sub(time, duration) → time (time minus duration)
var TimezoneFunc = function.New(&function.Spec{ Description: "A time's zone name. Without an argument, the machine's local zone.", VarParam: &function.Parameter{ Name: "t", Type: TimeCapsuleType, Description: "The time whose zone to report; omit for the local zone.", }, Type: boundedArity("time::zone", 1, cty.String), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { if len(args) == 0 { return cty.StringVal(time.Local.String()), nil } t, err := GetTime(args[0]) if err != nil { return cty.NilVal, err } return cty.StringVal(t.Location().String()), nil }, })
TimezoneFunc returns the timezone name. Called as time::zone() for the local system timezone, or time::zone(t) for the timezone stored in a time value.
var UnixFunc = function.New(&function.Spec{ Description: "A time as a Unix epoch count. Seconds are fractional; the other units are whole.", Params: []function.Parameter{ { Name: "t", Type: TimeCapsuleType, Description: "The time to convert.", }, }, VarParam: &function.Parameter{ Name: "unit", Type: cty.String, Description: `One of "s", "ms", "us", "ns"; defaults to "s".`, }, Type: boundedArity("time::to_unix", 2, cty.Number), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { t, err := GetTime(args[0]) if err != nil { return cty.NilVal, err } unit := "s" if len(args) > 1 { unit = args[1].AsString() } switch unit { case "s": return cty.NumberFloatVal(float64(t.UnixNano()) / 1e9), nil case "ms": return cty.NumberIntVal(t.UnixMilli()), nil case "us": return cty.NumberIntVal(t.UnixMicro()), nil case "ns": return cty.NumberIntVal(t.UnixNano()), nil default: return cty.NilVal, fmt.Errorf("time::to_unix: unknown unit %q; valid units: s, ms, us, ns", unit) } }, })
UnixFunc returns the Unix epoch value for a time. Called as time::to_unix(t) for fractional seconds, or time::to_unix(t, unit) where unit is "s" (float), "ms", "us", or "ns" (integers).
var UntilFunc = function.New(&function.Spec{ Description: "How far off a time is: the duration from now until it. Negative if it has passed.", Params: []function.Parameter{ { Name: "t", Type: TimeCapsuleType, Description: "The time to measure to.", }, }, Type: function.StaticReturnType(DurationCapsuleType), Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) { t, err := GetTime(args[0]) if err != nil { return cty.NilVal, err } return NewDurationCapsule(time.Until(t)), nil }, })
UntilFunc returns the duration until the given time (equivalent to time::sub(t, time::now())).
Functions ¶
func Externs ¶ added in v0.3.0
Externs returns the functy `//functy:extern` declarations for the functions GetTimeFunctions provides whose real signature their cty metadata cannot express, keyed by a filename to report in diagnostics.
cty can only make a function's *trailing* parameters optional, so an optional argument has to be faked with a variadic — which erases its name, its type, and its default: `time::from_unix(n, unit = "s")` reflects as `from_unix(n, ...args)`. And a cty function has one signature, so a function whose argument shapes differ per call (`time::parse(s)` vs `time::parse(format, s)`) or whose parameters are a union (`dns::parse_zone_serial` takes a number *or* a string) cannot be described at all. These declarations say what each really accepts, so that help(), generated documentation, and editor tooling can show it.
There is one file per namespace, because a functy source declares at most one: `time`, `duration`, `dns`, and a global file for the un-namespaced `duration()` constructor. A host registers them all:
for name, src := range timecty.Externs() {
parser.RegisterExterns(src, name)
}
The bytes are opaque to this package: it does not import functy, and nothing here parses them. The functions with no declaration are deliberately undeclared — their cty metadata is complete, so an extern would only be a second place for the same facts to drift.
func GetDuration ¶
GetDuration extracts a time.Duration from a cty capsule value. Returns an error if the value is not a DurationCapsuleType.
func GetTime ¶
GetTime extracts a time.Time from a cty capsule value. Returns an error if the value is not a TimeCapsuleType.
func GetTimeFunctions ¶
GetTimeFunctions returns this package's cty functions, keyed by the name they are callable under.
The names are namespaced — `time::add`, `duration::truncate`, `dns::next_zone_serial`. HCL parses `a::b(x)` natively and resolves it as a single flat map key, so a namespace is a naming convention rather than a containment relationship: these are ordinary map entries whose keys happen to contain `::`.
Two rules, both HashiCorp's for provider-defined functions, and both worth stating because they are why the names look the way they do:
- The leaf name does not repeat the namespace. `time::add`, not `time::timeadd`.
- Namespaced functions use underscores. HCL's *built-in* functions run words together (`formatdate`, `jsonencode`) for historical reasons; once a namespace does the grouping, the leaf name gets longer and more descriptive, and run-on stops being readable — `next_zone_serial` over `nextzoneserial`.
`duration` keeps a bare, un-namespaced name: it is the type constructor, and reads as one — `duration("1h30m")`, like `tostring(x)`. A bare name and a namespace of the same name coexist without conflict, since they are simply different map keys.
There is deliberately no `timeadd`. That name belongs to cty's own stdlib.TimeAddFunc, the string-in/string-out function HCL has always had; a host that wants it registers it directly. `time::add` carries no compatibility burden as a result, which is why every one of its forms returns a time.
func NewDurationCapsule ¶
NewDurationCapsule wraps a time.Duration in a cty capsule value.
Types ¶
type Duration ¶ added in v0.2.0
Duration wraps a time.Duration so it can implement the rich-cty-types Stringable and Gettable interfaces.
type Timestamp ¶ added in v0.2.0
Timestamp wraps a time.Time so it can implement the rich-cty-types Stringable and Gettable interfaces. Embedding forwards time.Time's methods (Format, Year, Equal, etc.) so callers can use them directly.