package config import ( "fmt" "strconv" "strings" "time" "gopkg.in/yaml.v3" ) type ByteSize int64 type Duration time.Duration func (size *ByteSize) UnmarshalYAML(value *yaml.Node) error { if value.Kind != yaml.ScalarNode || value.Tag != "!!str" { return fmt.Errorf("size must be a string with B, KB, MB, or GB suffix") } var raw string if err := value.Decode(&raw); err != nil { return err } parsed, err := ParseByteSize(raw) if err != nil { return err } *size = parsed return nil } func (size ByteSize) String() string { value := int64(size) if value == 0 { return "0B" } units := []struct { suffix string multiplier int64 }{ {suffix: "GB", multiplier: 1024 * 1024 * 1024}, {suffix: "MB", multiplier: 1024 * 1024}, {suffix: "KB", multiplier: 1024}, {suffix: "B", multiplier: 1}, } for _, unit := range units { if value%unit.multiplier == 0 { return strconv.FormatInt(value/unit.multiplier, 10) + unit.suffix } } return strconv.FormatInt(value, 10) + "B" } func ParseByteSize(raw string) (ByteSize, error) { value := strings.TrimSpace(raw) if value == "" { return 0, fmt.Errorf("size is required") } units := []struct { suffix string multiplier int64 }{ {suffix: "GB", multiplier: 1024 * 1024 * 1024}, {suffix: "MB", multiplier: 1024 * 1024}, {suffix: "KB", multiplier: 1024}, {suffix: "B", multiplier: 1}, } for _, unit := range units { number, ok := strings.CutSuffix(value, unit.suffix) if !ok { continue } if strings.TrimSpace(number) != number || number == "" { return 0, fmt.Errorf("size must be an integer followed by B, KB, MB, or GB") } parsed, err := strconv.ParseInt(number, 10, 64) if err != nil { return 0, fmt.Errorf("size must be an integer followed by B, KB, MB, or GB") } if parsed < 0 { return 0, fmt.Errorf("size must be non-negative") } const maxInt64 = int64(1<<63 - 1) if parsed > 0 && parsed > maxInt64/unit.multiplier { return 0, fmt.Errorf("size is too large") } return ByteSize(parsed * unit.multiplier), nil } return 0, fmt.Errorf("size must use B, KB, MB, or GB suffix") } func (duration *Duration) UnmarshalYAML(value *yaml.Node) error { if value.Kind != yaml.ScalarNode || value.Tag != "!!str" { return fmt.Errorf("duration must be a string duration") } var raw string if err := value.Decode(&raw); err != nil { return err } parsed, err := time.ParseDuration(raw) if err != nil { return fmt.Errorf("duration must be a valid duration: %w", err) } *duration = Duration(parsed) return nil } func (duration Duration) String() string { return time.Duration(duration).String() } func (duration Duration) AsDuration() time.Duration { return time.Duration(duration) }