Repository: prometheus/common
Pull request #270 (2021-02-08 16:20:54)

Add MarshalText and UnmarshalText for Duration
This is mostly needed for https://github.com/alecthomas/kong.
Furthermore it should be useful for others too, as it implements stdlib interfaces.

roidelapluie: model.Duration is an internal format, generally kong should use time.Duration. What would be the usecase?
metalmatze: time.Duration doesn't support anything above hours. Therefore it would need a custom wrapper to support `1w`, `1y` and more. 
We want to add flags that support these time ranges too.
As there's no direct dependency on anything and this change implements two stdlib interfaces I don't see this hurting. 
roidelapluie: I am not sure that I'd like to advise large projects like kong to rely on prometheus common given all the direct and indirect dependencies we have. We have complains like https://github.com/prometheus/common/issues/255 already, and I do not want to amplify these. I agree that the code change is small, but it is also using exported interfaces only, such as this could as well be done downstream in kong with the same amount of code:

`type promDuration model.Duration`

Would that be an option?
roidelapluie: Okay, can you add a comment about which interface you are implementing?

Patch:
diff --git a/model/time.go b/model/time.go
index c40e640..77b82b2 100644
--- a/model/time.go
+++ b/model/time.go
@@ -254,6 +254,18 @@ func (d Duration) String() string {
 	return r
 }
 
+// MarshalText implements the encoding.TextMarshaler interface.
+func (d *Duration) MarshalText() ([]byte, error) {
+	return []byte(d.String()), nil
+}
+
+// UnmarshalText implements the encoding.TextUnmarshaler interface.
+func (d *Duration) UnmarshalText(text []byte) error {
+	var err error
+	*d, err = ParseDuration(string(text))
+	return err
+}
+
 // MarshalYAML implements the yaml.Marshaler interface.
 func (d Duration) MarshalYAML() (interface{}, error) {
 	return d.String(), nil


Test patch:
diff --git a/model/time_test.go b/model/time_test.go
index d9c6789..14fb9eb 100644
--- a/model/time_test.go
+++ b/model/time_test.go
@@ -157,6 +157,79 @@ func TestParseDuration(t *testing.T) {
 	}
 }
 
+func TestDuration_UnmarshalText(t *testing.T) {
+	var cases = []struct {
+		in  string
+		out time.Duration
+
+		expectedString string
+	}{
+		{
+			in:             "0",
+			out:            0,
+			expectedString: "0s",
+		}, {
+			in:             "0w",
+			out:            0,
+			expectedString: "0s",
+		}, {
+			in:  "0s",
+			out: 0,
+		}, {
+			in:  "324ms",
+			out: 324 * time.Millisecond,
+		}, {
+			in:  "3s",
+			out: 3 * time.Second,
+		}, {
+			in:  "5m",
+			out: 5 * time.Minute,
+		}, {
+			in:  "1h",
+			out: time.Hour,
+		}, {
+			in:  "4d",
+			out: 4 * 24 * time.Hour,
+		}, {
+			in:  "4d1h",
+			out: 4*24*time.Hour + time.Hour,
+		}, {
+			in:             "14d",
+			out:            14 * 24 * time.Hour,
+			expectedString: "2w",
+		}, {
+			in:  "3w",
+			out: 3 * 7 * 24 * time.Hour,
+		}, {
+			in:             "3w2d1h",
+			out:            3*7*24*time.Hour + 2*24*time.Hour + time.Hour,
+			expectedString: "23d1h",
+		}, {
+			in:  "10y",
+			out: 10 * 365 * 24 * time.Hour,
+		},
+	}
+
+	for _, c := range cases {
+		var d Duration
+		err := d.UnmarshalText([]byte(c.in))
+		if err != nil {
+			t.Errorf("Unexpected error on input %q", c.in)
+		}
+		if time.Duration(d) != c.out {
+			t.Errorf("Expected %v but got %v", c.out, d)
+		}
+		expectedString := c.expectedString
+		if c.expectedString == "" {
+			expectedString = c.in
+		}
+		text, _ := d.MarshalText() // MarshalText returns hardcoded nil
+		if string(text) != expectedString {
+			t.Errorf("Expected duration string %q but got %q", c.in, d.String())
+		}
+	}
+}
+
 func TestParseBadDuration(t *testing.T) {
 	var cases = []string{
 		"1",