mirror of
https://github.com/hanzoai/types.git
synced 2026-10-01 00:45:52 +00:00
types: the fleet's shared value layer
A value belongs here when two or more services must agree on it exactly and neither should inherit the other's machinery to do so. The package imports the standard library only; go list -deps is 63, all stdlib. Seeded with the metered-series window. Cloud and ai must resolve ?range/?start/ ?end to the same [start, end) and the same bucket interval, and cloud reached that 30-line rule through a package that carries 1925 dependencies. Interval is a closed type. The bucket function is not bindable in the engines that serve these series, so the interval reaches a query by interpolation; ParseWindow yields Hour or Day and nothing else can be constructed through it. ParseWindow is total: it returns a window or an error, never a zero window a caller could read as a valid range. It also carries the resolved label, which every call site previously re-derived. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
*.test
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Hanzo AI Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,9 @@
|
||||
// Package types holds the values Hanzo services share.
|
||||
//
|
||||
// It imports the standard library only, so a service can depend on a value
|
||||
// without inheriting the machinery of whoever else uses it.
|
||||
//
|
||||
// A value earns a place here when two or more services must agree on it
|
||||
// exactly. Fetching, storing and recording are effects, and an effect belongs
|
||||
// with the service that owns it, next to the schema or transport it acts on.
|
||||
package types
|
||||
@@ -0,0 +1,105 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Interval is the width of one bucket in a metered time series.
|
||||
//
|
||||
// The set is closed at Hour and Day. A bucket function is not a bindable
|
||||
// parameter in the engines that serve these series, so the interval reaches a
|
||||
// query by interpolation; keeping the set closed means a request cannot put
|
||||
// anything else there.
|
||||
type Interval string
|
||||
|
||||
const (
|
||||
Hour Interval = "hour"
|
||||
Day Interval = "day"
|
||||
)
|
||||
|
||||
// Step is how much time one bucket covers.
|
||||
func (i Interval) Step() time.Duration {
|
||||
if i == Day {
|
||||
return 24 * time.Hour
|
||||
}
|
||||
return time.Hour
|
||||
}
|
||||
|
||||
// Window is a half-open time range [Start, End) and the interval its series
|
||||
// buckets to.
|
||||
//
|
||||
// Label is the range as the caller asked for it, defaulted when the caller
|
||||
// asked for nothing, so a response can say which window it answers without the
|
||||
// caller re-deriving it.
|
||||
type Window struct {
|
||||
Label string
|
||||
Start time.Time
|
||||
End time.Time
|
||||
Interval Interval
|
||||
}
|
||||
|
||||
// ParseWindow resolves a range request into an absolute window.
|
||||
//
|
||||
// "24h", "7d" and "30d", with their aliases, are relative to now. "custom"
|
||||
// reads start and end, each RFC3339 or unix seconds; end defaults to now and
|
||||
// must follow start. An unrecognised label is an error rather than a silent
|
||||
// default, so a typo cannot quietly change the window a caller is shown.
|
||||
//
|
||||
// now is a parameter so the window is a function of the arguments alone.
|
||||
func ParseWindow(label, start, end string, now time.Time) (Window, error) {
|
||||
now = now.UTC()
|
||||
|
||||
w := Window{Label: strings.TrimSpace(label)}
|
||||
if w.Label == "" {
|
||||
w.Label = "24h"
|
||||
}
|
||||
|
||||
switch strings.ToLower(strings.TrimSpace(label)) {
|
||||
case "", "24h", "1d", "day", "today":
|
||||
w.Start, w.End, w.Interval = now.Add(-24*time.Hour), now, Hour
|
||||
case "7d", "week":
|
||||
w.Start, w.End, w.Interval = now.Add(-7*24*time.Hour), now, Day
|
||||
case "30d", "month":
|
||||
w.Start, w.End, w.Interval = now.Add(-30*24*time.Hour), now, Day
|
||||
case "custom":
|
||||
from, err := parseTime(start)
|
||||
if err != nil {
|
||||
return Window{}, fmt.Errorf("custom range requires a valid start: %w", err)
|
||||
}
|
||||
to := now
|
||||
if strings.TrimSpace(end) != "" {
|
||||
if to, err = parseTime(end); err != nil {
|
||||
return Window{}, fmt.Errorf("custom range has an invalid end: %w", err)
|
||||
}
|
||||
}
|
||||
if !to.After(from) {
|
||||
return Window{}, fmt.Errorf("custom range end must be after start")
|
||||
}
|
||||
w.Start, w.End, w.Interval = from.UTC(), to.UTC(), Hour
|
||||
if w.End.Sub(w.Start) > 48*time.Hour {
|
||||
w.Interval = Day
|
||||
}
|
||||
default:
|
||||
return Window{}, fmt.Errorf("unknown range %q", label)
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// parseTime accepts RFC3339 or a unix-seconds string.
|
||||
func parseTime(s string) (time.Time, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return time.Time{}, fmt.Errorf("empty time")
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||
return t.UTC(), nil
|
||||
}
|
||||
if secs, err := strconv.ParseInt(s, 10, 64); err == nil && secs > 0 {
|
||||
return time.Unix(secs, 0).UTC(), nil
|
||||
}
|
||||
return time.Time{}, fmt.Errorf("not RFC3339 or unix seconds: %q", s)
|
||||
}
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var now = time.Date(2026, 7, 26, 15, 4, 5, 0, time.UTC)
|
||||
|
||||
func TestParseWindowRelativeLabels(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
label string
|
||||
back time.Duration
|
||||
want Interval
|
||||
}{
|
||||
{"", 24 * time.Hour, Hour},
|
||||
{"24h", 24 * time.Hour, Hour},
|
||||
{"1d", 24 * time.Hour, Hour},
|
||||
{"day", 24 * time.Hour, Hour},
|
||||
{"today", 24 * time.Hour, Hour},
|
||||
{"7d", 7 * 24 * time.Hour, Day},
|
||||
{"week", 7 * 24 * time.Hour, Day},
|
||||
{"30d", 30 * 24 * time.Hour, Day},
|
||||
{"month", 30 * 24 * time.Hour, Day},
|
||||
{" 7D ", 7 * 24 * time.Hour, Day},
|
||||
} {
|
||||
t.Run(strings.TrimSpace(tc.label), func(t *testing.T) {
|
||||
w, err := ParseWindow(tc.label, "", "", now)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseWindow(%q) errored: %v", tc.label, err)
|
||||
}
|
||||
if !w.End.Equal(now) {
|
||||
t.Errorf("End = %v, want %v", w.End, now)
|
||||
}
|
||||
if !w.Start.Equal(now.Add(-tc.back)) {
|
||||
t.Errorf("Start = %v, want %v", w.Start, now.Add(-tc.back))
|
||||
}
|
||||
if w.Interval != tc.want {
|
||||
t.Errorf("Interval = %q, want %q", w.Interval, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The label a response echoes is the caller's, defaulted when absent. Every
|
||||
// caller used to re-derive this after the call; the window now carries it.
|
||||
func TestParseWindowLabelIsDefaultedNotCanonicalised(t *testing.T) {
|
||||
for _, tc := range []struct{ in, want string }{
|
||||
{"", "24h"},
|
||||
{" ", "24h"},
|
||||
{"7d", "7d"},
|
||||
{" 30d ", "30d"},
|
||||
{"7D", "7D"},
|
||||
{"custom", "custom"},
|
||||
} {
|
||||
in := tc.in
|
||||
start := ""
|
||||
if strings.TrimSpace(in) == "custom" {
|
||||
start = "2026-07-01T00:00:00Z"
|
||||
}
|
||||
w, err := ParseWindow(in, start, "", now)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseWindow(%q) errored: %v", in, err)
|
||||
}
|
||||
if w.Label != tc.want {
|
||||
t.Errorf("ParseWindow(%q).Label = %q, want %q", in, w.Label, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseWindowCustom(t *testing.T) {
|
||||
t.Run("rfc3339 under 48h buckets hourly", func(t *testing.T) {
|
||||
w, err := ParseWindow("custom", "2026-07-25T00:00:00Z", "2026-07-26T00:00:00Z", now)
|
||||
if err != nil {
|
||||
t.Fatalf("errored: %v", err)
|
||||
}
|
||||
if w.Interval != Hour {
|
||||
t.Errorf("Interval = %q, want %q", w.Interval, Hour)
|
||||
}
|
||||
if !w.Start.Equal(time.Date(2026, 7, 25, 0, 0, 0, 0, time.UTC)) {
|
||||
t.Errorf("Start = %v", w.Start)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("over 48h buckets daily", func(t *testing.T) {
|
||||
w, err := ParseWindow("custom", "2026-07-01T00:00:00Z", "2026-07-26T00:00:00Z", now)
|
||||
if err != nil {
|
||||
t.Fatalf("errored: %v", err)
|
||||
}
|
||||
if w.Interval != Day {
|
||||
t.Errorf("Interval = %q, want %q", w.Interval, Day)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("exactly 48h stays hourly", func(t *testing.T) {
|
||||
w, err := ParseWindow("custom", "2026-07-24T00:00:00Z", "2026-07-26T00:00:00Z", now)
|
||||
if err != nil {
|
||||
t.Fatalf("errored: %v", err)
|
||||
}
|
||||
if w.Interval != Hour {
|
||||
t.Errorf("Interval = %q, want %q", w.Interval, Hour)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unix seconds accepted", func(t *testing.T) {
|
||||
w, err := ParseWindow("custom", "1753401600", "1753488000", now)
|
||||
if err != nil {
|
||||
t.Fatalf("errored: %v", err)
|
||||
}
|
||||
if w.Start.Location() != time.UTC || w.End.Location() != time.UTC {
|
||||
t.Errorf("bounds must be UTC, got %v/%v", w.Start.Location(), w.End.Location())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("absent end defaults to now", func(t *testing.T) {
|
||||
w, err := ParseWindow("custom", "2026-07-25T00:00:00Z", "", now)
|
||||
if err != nil {
|
||||
t.Fatalf("errored: %v", err)
|
||||
}
|
||||
if !w.End.Equal(now) {
|
||||
t.Errorf("End = %v, want %v", w.End, now)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// A rejected window must not hand back a zero Window a caller could mistake for
|
||||
// a valid range.
|
||||
func TestParseWindowRejects(t *testing.T) {
|
||||
for _, tc := range []struct{ name, label, start, end, wantErr string }{
|
||||
{"unknown label", "yesterday", "", "", `unknown range "yesterday"`},
|
||||
{"unknown label keeps caller casing", "LastWeek", "", "", `unknown range "LastWeek"`},
|
||||
{"custom without start", "custom", "", "", "custom range requires a valid start: empty time"},
|
||||
{"custom bad start", "custom", "nonsense", "", `custom range requires a valid start: not RFC3339 or unix seconds: "nonsense"`},
|
||||
{"custom bad end", "custom", "2026-07-01T00:00:00Z", "nope", `custom range has an invalid end: not RFC3339 or unix seconds: "nope"`},
|
||||
{"custom end before start", "custom", "2026-07-26T00:00:00Z", "2026-07-01T00:00:00Z", "custom range end must be after start"},
|
||||
{"custom end equals start", "custom", "2026-07-01T00:00:00Z", "2026-07-01T00:00:00Z", "custom range end must be after start"},
|
||||
{"custom non-positive unix start", "custom", "0", "", `custom range requires a valid start: not RFC3339 or unix seconds: "0"`},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
w, err := ParseWindow(tc.label, tc.start, tc.end, now)
|
||||
if err == nil {
|
||||
t.Fatalf("want error, got window %+v", w)
|
||||
}
|
||||
if err.Error() != tc.wantErr {
|
||||
t.Errorf("error = %q, want %q", err.Error(), tc.wantErr)
|
||||
}
|
||||
if w != (Window{}) {
|
||||
t.Errorf("rejected window must be zero, got %+v", w)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntervalStep(t *testing.T) {
|
||||
if got := Hour.Step(); got != time.Hour {
|
||||
t.Errorf("Hour.Step() = %v, want %v", got, time.Hour)
|
||||
}
|
||||
if got := Day.Step(); got != 24*time.Hour {
|
||||
t.Errorf("Day.Step() = %v, want %v", got, 24*time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
// ParseWindow only ever yields Hour or Day, so a caller interpolating the
|
||||
// interval into a query has no third case to guard against.
|
||||
func TestParseWindowOnlyYieldsTheClosedSet(t *testing.T) {
|
||||
labels := []string{"", "24h", "1d", "day", "today", "7d", "week", "30d", "month", "custom"}
|
||||
for _, l := range labels {
|
||||
start := ""
|
||||
if l == "custom" {
|
||||
start = "2026-07-01T00:00:00Z"
|
||||
}
|
||||
w, err := ParseWindow(l, start, "", now)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseWindow(%q) errored: %v", l, err)
|
||||
}
|
||||
if w.Interval != Hour && w.Interval != Day {
|
||||
t.Fatalf("ParseWindow(%q) yielded interval %q outside {hour, day}", l, w.Interval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// resolveLegacy is the implementation this package replaced, kept verbatim as
|
||||
// the oracle the parity test compares against.
|
||||
func resolveLegacy(rangeLabel, startStr, endStr string, now time.Time) (start, end time.Time, interval string, err error) {
|
||||
parseParam := func(s string) (time.Time, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return time.Time{}, fmt.Errorf("empty time")
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||
return t.UTC(), nil
|
||||
}
|
||||
if secs, err := strconv.ParseInt(s, 10, 64); err == nil && secs > 0 {
|
||||
return time.Unix(secs, 0).UTC(), nil
|
||||
}
|
||||
return time.Time{}, fmt.Errorf("not RFC3339 or unix seconds: %q", s)
|
||||
}
|
||||
|
||||
now = now.UTC()
|
||||
switch strings.ToLower(strings.TrimSpace(rangeLabel)) {
|
||||
case "", "24h", "1d", "day", "today":
|
||||
return now.Add(-24 * time.Hour), now, "hour", nil
|
||||
case "7d", "week":
|
||||
return now.Add(-7 * 24 * time.Hour), now, "day", nil
|
||||
case "30d", "month":
|
||||
return now.Add(-30 * 24 * time.Hour), now, "day", nil
|
||||
case "custom":
|
||||
start, err = parseParam(startStr)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, "", fmt.Errorf("custom range requires a valid start: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(endStr) == "" {
|
||||
end = now
|
||||
} else if end, err = parseParam(endStr); err != nil {
|
||||
return time.Time{}, time.Time{}, "", fmt.Errorf("custom range has an invalid end: %w", err)
|
||||
}
|
||||
if !end.After(start) {
|
||||
return time.Time{}, time.Time{}, "", fmt.Errorf("custom range end must be after start")
|
||||
}
|
||||
interval = "hour"
|
||||
if end.Sub(start) > 48*time.Hour {
|
||||
interval = "day"
|
||||
}
|
||||
return start.UTC(), end.UTC(), interval, nil
|
||||
default:
|
||||
return time.Time{}, time.Time{}, "", fmt.Errorf("unknown range %q", rangeLabel)
|
||||
}
|
||||
}
|
||||
|
||||
// Bounds, interval and error text must match the replaced implementation
|
||||
// exactly: these values reach a live dashboard and a 400 body.
|
||||
func TestParseWindowMatchesTheReplacedImplementation(t *testing.T) {
|
||||
labels := []string{
|
||||
"", " ", "24h", "1d", "day", "today", "7d", "week", "30d", "month",
|
||||
"24H", " 7d ", "custom", "CUSTOM", "yesterday", "1h", "all",
|
||||
}
|
||||
bounds := []string{
|
||||
"", " ", "2026-07-01T00:00:00Z", "2026-07-25T12:00:00Z", "2026-07-26T00:00:00Z",
|
||||
"1753401600", "0", "-5", "nonsense", "2026-07-01",
|
||||
}
|
||||
|
||||
cases := 0
|
||||
for _, l := range labels {
|
||||
for _, s := range bounds {
|
||||
for _, e := range bounds {
|
||||
cases++
|
||||
w, gotErr := ParseWindow(l, s, e, now)
|
||||
wantStart, wantEnd, wantInterval, wantErr := resolveLegacy(l, s, e, now)
|
||||
|
||||
switch {
|
||||
case gotErr == nil && wantErr != nil:
|
||||
t.Fatalf("ParseWindow(%q,%q,%q) succeeded; legacy errored %v", l, s, e, wantErr)
|
||||
case gotErr != nil && wantErr == nil:
|
||||
t.Fatalf("ParseWindow(%q,%q,%q) errored %v; legacy succeeded", l, s, e, gotErr)
|
||||
case gotErr != nil:
|
||||
if gotErr.Error() != wantErr.Error() {
|
||||
t.Fatalf("ParseWindow(%q,%q,%q) error = %q, legacy = %q", l, s, e, gotErr, wantErr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !w.Start.Equal(wantStart) {
|
||||
t.Fatalf("ParseWindow(%q,%q,%q) Start = %v, legacy = %v", l, s, e, w.Start, wantStart)
|
||||
}
|
||||
if !w.End.Equal(wantEnd) {
|
||||
t.Fatalf("ParseWindow(%q,%q,%q) End = %v, legacy = %v", l, s, e, w.End, wantEnd)
|
||||
}
|
||||
if string(w.Interval) != wantInterval {
|
||||
t.Fatalf("ParseWindow(%q,%q,%q) Interval = %q, legacy = %q", l, s, e, w.Interval, wantInterval)
|
||||
}
|
||||
if got, want := w.Interval.Step(), legacyStep(wantInterval); got != want {
|
||||
t.Fatalf("Step(%q) = %v, legacy = %v", w.Interval, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Logf("compared %d label/start/end combinations against the replaced implementation", cases)
|
||||
}
|
||||
|
||||
// legacyStep is the bucket-width helper this package replaced.
|
||||
func legacyStep(interval string) time.Duration {
|
||||
if strings.EqualFold(interval, "day") {
|
||||
return 24 * time.Hour
|
||||
}
|
||||
return time.Hour
|
||||
}
|
||||
Reference in New Issue
Block a user