fix: strict ClockTime parsing and ISODuration SQL

Validate constructors, strict HH:MM:SS parse, UTC ClockTimeFromTime,
ISODuration Value/Scan, Compare and TextMarshaler, codec.go split.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 18:01:08 -07:00
parent 49ac7dd22a
commit a1284075aa
7 changed files with 409 additions and 187 deletions
+13 -63
View File
@@ -1,20 +1,16 @@
package clocktime
import (
"encoding/json"
"fmt"
"io"
"time"
"github.com/sosodev/duration"
)
// ISODuration wraps a time.Duration to provide custom JSON and SQL
// serialization and full ISO8061 compatibility. Note: ISODuration is limited
// to the same range as time.Duration despite ISO8061 supporting larger durations.
// ISODuration wraps time.Duration with ISO 8601 string formatting.
type ISODuration time.Duration
// NewISODuration creates a new duration from numeric components.
// NewISODuration creates a duration from hour, minute, and second components.
func NewISODuration(hours, minutes, seconds int) ISODuration {
return ISODuration(
time.Hour*time.Duration(hours) +
@@ -23,77 +19,31 @@ func NewISODuration(hours, minutes, seconds int) ISODuration {
)
}
// DurationFromISOString parses an ISO8601 duration string (e.g., "PT1H30M45S")
// and returns an ISODuration.
// ParseISODuration parses an ISO 8601 duration string (for example PT1H30M).
func ParseISODuration(s string) (ISODuration, error) {
return DurationFromISOString(s)
}
// DurationFromISOString parses an ISO 8601 duration string.
func DurationFromISOString(s string) (ISODuration, error) {
d, err := duration.Parse(s)
if err != nil {
return 0, fmt.Errorf("error parsing ISO8061 duration: %w", err)
return 0, fmt.Errorf("error parsing ISO 8601 duration: %w", err)
}
return ISODuration(d.ToTimeDuration()), nil
}
// ISODurationFromDuration converts a time.Duration to an ISODuration.
// ISODurationFromDuration converts a time.Duration to ISODuration.
func ISODurationFromDuration(d time.Duration) ISODuration {
return ISODuration(d)
}
// String returns the ISO8601 string representation of the ISODuration.
// String returns the ISO 8601 representation.
func (d ISODuration) String() string {
td := time.Duration(d)
isoDur := duration.FromTimeDuration(td)
return isoDur.String()
return duration.FromTimeDuration(time.Duration(d)).String()
}
// Duration returns the time.Duration representation of the ISODuration.
// Duration returns the underlying time.Duration.
func (d ISODuration) Duration() time.Duration {
return time.Duration(d)
}
// MarshalJSON implements the json.Marshaler interface for ISODuration,
// serializing it as an ISO8601 duration string.
func (d ISODuration) MarshalJSON() ([]byte, error) {
return json.Marshal("\"" + d.String() + "\"")
}
// UnmarshalJSON implements the json.Unmarshaler interface for ISODuration,
// parsing an ISO8601 duration string.
func (d *ISODuration) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
parsedDur, err := DurationFromISOString(s)
if err != nil {
return err
}
*d = parsedDur
return nil
}
// MarshalGQL implements the graphql.Marshaler interface for ISODuration,
// serializing it as an ISO8601 duration string.
func (d ISODuration) MarshalGQL(w io.Writer) {
fmt.Fprint(w, "\""+d.String()+"\"")
}
// UnmarshalGQL implements the graphql.Unmarshaler interface for ISODuration,
// parsing an ISO8601 duration string. nil values are treated as zero duration.
func (d *ISODuration) UnmarshalGQL(value any) error {
if value == nil {
*d = ISODuration(0)
return nil
}
s, ok := value.(string)
if !ok {
return fmt.Errorf("ISODuration must be a string")
}
parsedDur, err := DurationFromISOString(s)
if err != nil {
return err
}
*d = parsedDur
return nil
}