diff --git a/configtype/duration.go b/configtype/duration.go index 838c9d15fe..dc8789ce05 100644 --- a/configtype/duration.go +++ b/configtype/duration.go @@ -2,164 +2,29 @@ package configtype import ( "encoding/json" - "fmt" - "regexp" - "strconv" "time" "github.com/invopop/jsonschema" ) -var ( - numberRegexp = regexp.MustCompile(`^[0-9]+$`) - - baseDurationSegmentPattern = `[-+]?([0-9]*(\.[0-9]*)?[a-z]+)+` // copied from time.ParseDuration - baseDurationPattern = fmt.Sprintf(`^%s$`, baseDurationSegmentPattern) - baseDurationRegexp = regexp.MustCompile(baseDurationPattern) - - humanDurationSignsPattern = `ago|from\s+now` - - humanDurationUnitsPattern = `nanoseconds?|ns|microseconds?|us|µs|μs|milliseconds?|ms|seconds?|s|minutes?|m|hours?|h|days?|d|months?|M|years?|Y` - humanDurationUnitsRegex = regexp.MustCompile(fmt.Sprintf(`^%s$`, humanDurationUnitsPattern)) - - humanDurationSegmentPattern = fmt.Sprintf(`(([0-9]+\s+(%[1]s)|%[2]s))`, humanDurationUnitsPattern, baseDurationSegmentPattern) - - humanDurationPattern = fmt.Sprintf(`^%[1]s(\s+%[1]s)*$`, humanDurationSegmentPattern) - - humanRelativeDurationPattern = fmt.Sprintf(`^%[1]s(\s+%[1]s)*\s+(%[2]s)$`, humanDurationSegmentPattern, humanDurationSignsPattern) - humanRelativeDurationRegexp = regexp.MustCompile(humanRelativeDurationPattern) - - whitespaceRegexp = regexp.MustCompile(`\s+`) -) - // Duration is a wrapper around time.Duration that should be used in config // when a duration type is required. We wrap the time.Duration type so that // the spec can be extended in the future to support other types of durations // (e.g. a duration that is specified in days). type Duration struct { - input string - - relative bool - sign int duration time.Duration - days int - months int - years int } func NewDuration(d time.Duration) Duration { return Duration{ - input: d.String(), - sign: 1, duration: d, } } -func ParseDuration(s string) (Duration, error) { - var d Duration - d.input = s - - var inValue bool - var value int64 - - var inSign bool - - parts := whitespaceRegexp.Split(s, -1) - - var err error - - for _, part := range parts { - switch { - case inSign: - if part != "now" { - return Duration{}, fmt.Errorf("invalid duration format: invalid sign specifier: %q", part) - } - - d.sign = 1 - inSign = false - case inValue: - if !humanDurationUnitsRegex.MatchString(part) { - return Duration{}, fmt.Errorf("invalid duration format: invalid unit specifier: %q", part) - } - - err = d.addUnit(part, value) - if err != nil { - return Duration{}, fmt.Errorf("invalid duration format: %w", err) - } - - value = 0 - inValue = false - case part == "ago": - if d.sign != 0 { - return Duration{}, fmt.Errorf("invalid duration format: more than one sign specifier") - } - - d.sign = -1 - case part == "from": - if d.sign != 0 { - return Duration{}, fmt.Errorf("invalid duration format: more than one sign specifier") - } - - inSign = true - case numberRegexp.MatchString(part): - value, err = strconv.ParseInt(part, 10, 64) - if err != nil { - return Duration{}, fmt.Errorf("invalid duration format: invalid value specifier: %q", part) - } - - inValue = true - case baseDurationRegexp.MatchString(part): - duration, err := time.ParseDuration(part) - if err != nil { - return Duration{}, fmt.Errorf("invalid duration format: invalid value specifier: %q", part) - } - - d.duration += duration - default: - return Duration{}, fmt.Errorf("invalid duration format: invalid value: %q", part) - } - } - - d.relative = d.sign != 0 - - if !d.relative { - d.sign = 1 - } - - return d, nil -} - -func (d *Duration) addUnit(unit string, number int64) error { - switch unit { - case "nanosecond", "nanoseconds", "ns": - d.duration += time.Nanosecond * time.Duration(number) - case "microsecond", "microseconds", "us", "μs", "µs": - d.duration += time.Microsecond * time.Duration(number) - case "millisecond", "milliseconds": - d.duration += time.Millisecond * time.Duration(number) - case "second", "seconds": - d.duration += time.Second * time.Duration(number) - case "minute", "minutes": - d.duration += time.Minute * time.Duration(number) - case "hour", "hours": - d.duration += time.Hour * time.Duration(number) - case "day", "days": - d.days += int(number) - case "month", "months": - d.months += int(number) - case "year", "years": - d.years += int(number) - default: - return fmt.Errorf("invalid unit: %q", unit) - } - - return nil -} - func (Duration) JSONSchema() *jsonschema.Schema { return &jsonschema.Schema{ Type: "string", - Pattern: patternCases(baseDurationPattern, humanDurationPattern, humanRelativeDurationPattern), + Pattern: `^[-+]?([0-9]*(\.[0-9]*)?[a-z]+)+$`, // copied from time.ParseDuration Title: "CloudQuery configtype.Duration", } } @@ -169,33 +34,22 @@ func (d *Duration) UnmarshalJSON(b []byte) error { if err := json.Unmarshal(b, &s); err != nil { return err } - - duration, err := ParseDuration(s) + duration, err := time.ParseDuration(s) if err != nil { return err } - - *d = duration + *d = Duration{duration: duration} return nil } -func (d Duration) MarshalJSON() ([]byte, error) { - return json.Marshal(d.String()) +func (d *Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(d.duration.String()) } -func (d Duration) Duration() time.Duration { - duration := d.duration - duration += time.Duration(d.days) * 24 * time.Hour - duration += time.Duration(d.months) * 30 * 24 * time.Hour - duration += time.Duration(d.years) * 365 * 24 * time.Hour - duration *= time.Duration(d.sign) - return duration +func (d *Duration) Duration() time.Duration { + return d.duration } func (d Duration) Equal(other Duration) bool { - return d == other -} - -func (d Duration) String() string { - return d.input + return d.duration == other.duration } diff --git a/configtype/duration_test.go b/configtype/duration_test.go index b5c47bca9c..fadd3d1104 100644 --- a/configtype/duration_test.go +++ b/configtype/duration_test.go @@ -21,12 +21,6 @@ func TestDuration(t *testing.T) { {"1ns", 1 * time.Nanosecond}, {"20s", 20 * time.Second}, {"-50m30s", -50*time.Minute - 30*time.Second}, - {"25 minute", 25 * time.Minute}, - {"50 minutes", 50 * time.Minute}, - {"10 years ago", -10 * 365 * 24 * time.Hour}, - {"1 month from now", 30 * 24 * time.Hour}, - {"1 month from now", 30 * 24 * time.Hour}, - {"1 year 2 month 3 days 4 hours 5 minutes 6 seconds from now", (365+60+3)*24*time.Hour + 4*time.Hour + 5*time.Minute + 6*time.Second}, } for _, tc := range cases { var d configtype.Duration @@ -40,32 +34,6 @@ func TestDuration(t *testing.T) { } } -func TestDuration_JSONMarshal(t *testing.T) { - cases := []struct { - give string - want string - }{ - {"1ns", "1ns"}, - {"20s", "20s"}, - {"-50m30s", "-50m30s"}, - {"25 minutes", "25 minutes"}, - {"50 minutes", "50 minutes"}, - {"10 years ago", "10 years ago"}, - {"1 month from now", "1 month from now"}, - {"1 month from now", "1 month from now"}, - } - for _, tc := range cases { - var d configtype.Duration - err := json.Unmarshal([]byte(`"`+tc.give+`"`), &d) - if err != nil { - t.Fatalf("error calling Unmarshal(%q): %v", tc.give, err) - } - if d.String() != tc.want { - t.Errorf("String(%q) = %q, want %v", tc.give, d.String(), tc.want) - } - } -} - func TestComparability(t *testing.T) { cases := []struct { give configtype.Duration diff --git a/configtype/time.go b/configtype/time.go index b0c1db208d..0d3b94ac0e 100644 --- a/configtype/time.go +++ b/configtype/time.go @@ -4,18 +4,41 @@ import ( "encoding/json" "fmt" "regexp" + "strconv" "time" "github.com/invopop/jsonschema" ) +var ( + numberRegexp = regexp.MustCompile(`^[0-9]+$`) + + baseDurationSegmentPattern = `[-+]?([0-9]*(\.[0-9]*)?[a-z]+)+` // copied from time.ParseDuration + baseDurationPattern = fmt.Sprintf(`^%s$`, baseDurationSegmentPattern) + baseDurationRegexp = regexp.MustCompile(baseDurationPattern) + + humanDurationSignsPattern = `ago|from\s+now` + + humanDurationUnitsPattern = `nanoseconds?|ns|microseconds?|us|µs|μs|milliseconds?|ms|seconds?|s|minutes?|m|hours?|h|days?|d|months?|M|years?|Y` + humanDurationUnitsRegex = regexp.MustCompile(fmt.Sprintf(`^%s$`, humanDurationUnitsPattern)) + + humanDurationSegmentPattern = fmt.Sprintf(`(([0-9]+\s+(%[1]s)|%[2]s))`, humanDurationUnitsPattern, baseDurationSegmentPattern) + + humanDurationPattern = fmt.Sprintf(`^%[1]s(\s+%[1]s)*$`, humanDurationSegmentPattern) + + humanRelativeDurationPattern = fmt.Sprintf(`^%[1]s(\s+%[1]s)*\s+(%[2]s)$`, humanDurationSegmentPattern, humanDurationSignsPattern) + humanRelativeDurationRegexp = regexp.MustCompile(humanRelativeDurationPattern) + + whitespaceRegexp = regexp.MustCompile(`\s+`) +) + // Time is a wrapper around time.Time that should be used in config // when a time type is required. We wrap the time.Time type so that // the spec can be extended in the future to support other types of times type Time struct { input string time time.Time - duration *Duration + duration *timeDuration } func ParseTime(s string) (Time, error) { @@ -25,15 +48,15 @@ func ParseTime(s string) (Time, error) { var err error switch { case timeNowRegexp.MatchString(s): - t.duration = new(Duration) - *t.duration = NewDuration(0) + t.duration = new(timeDuration) + *t.duration = newTimeDuration(0) case timeRFC3339Regexp.MatchString(s): t.time, err = time.Parse(time.RFC3339, s) case dateRegexp.MatchString(s): t.time, err = time.Parse(time.DateOnly, s) case baseDurationRegexp.MatchString(s), humanRelativeDurationRegexp.MatchString(s): - t.duration = new(Duration) - *t.duration, err = ParseDuration(s) + t.duration = new(timeDuration) + *t.duration, err = parseTimeDuration(s) default: return t, fmt.Errorf("invalid time format: %s", s) } @@ -109,3 +132,132 @@ func (t Time) IsZero() bool { func (t Time) String() string { return t.input } + +type timeDuration struct { + input string + + relative bool + sign int + duration time.Duration + days int + months int + years int +} + +func newTimeDuration(d time.Duration) timeDuration { + return timeDuration{ + input: d.String(), + sign: 1, + duration: d, + } +} + +func parseTimeDuration(s string) (timeDuration, error) { + var d timeDuration + d.input = s + + var inValue bool + var value int64 + + var inSign bool + + parts := whitespaceRegexp.Split(s, -1) + + var err error + + for _, part := range parts { + switch { + case inSign: + if part != "now" { + return timeDuration{}, fmt.Errorf("invalid duration format: invalid sign specifier: %q", part) + } + + d.sign = 1 + inSign = false + case inValue: + if !humanDurationUnitsRegex.MatchString(part) { + return timeDuration{}, fmt.Errorf("invalid duration format: invalid unit specifier: %q", part) + } + + err = d.addUnit(part, value) + if err != nil { + return timeDuration{}, fmt.Errorf("invalid duration format: %w", err) + } + + value = 0 + inValue = false + case part == "ago": + if d.sign != 0 { + return timeDuration{}, fmt.Errorf("invalid duration format: more than one sign specifier") + } + + d.sign = -1 + case part == "from": + if d.sign != 0 { + return timeDuration{}, fmt.Errorf("invalid duration format: more than one sign specifier") + } + + inSign = true + case numberRegexp.MatchString(part): + value, err = strconv.ParseInt(part, 10, 64) + if err != nil { + return timeDuration{}, fmt.Errorf("invalid duration format: invalid value specifier: %q", part) + } + + inValue = true + case baseDurationRegexp.MatchString(part): + duration, err := time.ParseDuration(part) + if err != nil { + return timeDuration{}, fmt.Errorf("invalid duration format: invalid value specifier: %q", part) + } + + d.duration += duration + default: + return timeDuration{}, fmt.Errorf("invalid duration format: invalid value: %q", part) + } + } + + d.relative = d.sign != 0 + + if !d.relative { + d.sign = 1 + } + + return d, nil +} + +func (d *timeDuration) addUnit(unit string, number int64) error { + switch unit { + case "nanosecond", "nanoseconds", "ns": + d.duration += time.Nanosecond * time.Duration(number) + case "microsecond", "microseconds", "us", "μs", "µs": + d.duration += time.Microsecond * time.Duration(number) + case "millisecond", "milliseconds": + d.duration += time.Millisecond * time.Duration(number) + case "second", "seconds": + d.duration += time.Second * time.Duration(number) + case "minute", "minutes": + d.duration += time.Minute * time.Duration(number) + case "hour", "hours": + d.duration += time.Hour * time.Duration(number) + case "day", "days": + d.days += int(number) + case "month", "months": + d.months += int(number) + case "year", "years": + d.years += int(number) + default: + return fmt.Errorf("invalid unit: %q", unit) + } + + return nil +} + +func (d timeDuration) Duration() time.Duration { + duration := d.duration + duration += time.Duration(d.days) * 24 * time.Hour + duration += time.Duration(d.months) * 30 * 24 * time.Hour + duration += time.Duration(d.years) * 365 * 24 * time.Hour + duration *= time.Duration(d.sign) + return duration +}