slog: marshal/unmarshal support for Levels
Add UnmarshalJSON to match MarshalJSON.
Add MarshalText and UnmarshalText. These enable using flag.TextVar
for levels.
Change-Id: I79fe4ed96ea100562dc03d8cc07e0848816751f9
Reviewed-on: https://go-review.googlesource.com/c/exp/+/463256
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Russ Cox <rsc@golang.org>
Reviewed-by: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/slog/level.go b/slog/level.go
index 871c1ea..f7c0a53 100644
--- a/slog/level.go
+++ b/slog/level.go
@@ -5,8 +5,10 @@
package slog
import (
+ "errors"
"fmt"
"strconv"
+ "strings"
"sync/atomic"
)
@@ -75,6 +77,8 @@
}
}
+// MarshalJSON implements [encoding/json.Marshaler]
+// by quoting the output of [Level.String].
func (l Level) MarshalJSON() ([]byte, error) {
// AppendQuote is sufficient for JSON-encoding all Level strings.
// They don't contain any runes that would produce invalid JSON
@@ -82,6 +86,66 @@
return strconv.AppendQuote(nil, l.String()), nil
}
+// UnmarshalJSON implements [encoding/json.Unmarshaler]
+// It accepts any string produced by [Level.MarshalJSON],
+// ignoring case.
+// It also accepts numeric offsets that would result in a different string on
+// output. For example, "Error-8" would marshal as "INFO".
+func (l *Level) UnmarshalJSON(data []byte) error {
+ s, err := strconv.Unquote(string(data))
+ if err != nil {
+ return err
+ }
+ return l.parse(s)
+}
+
+// MarshalText implements [encoding.TextMarshaler]
+// by calling [Level.String].
+func (l Level) MarshalText() ([]byte, error) {
+ return []byte(l.String()), nil
+}
+
+// UnmarshalText implements [encoding.TextUnmarshaler].
+// It accepts any string produced by [Level.MarshalText],
+// ignoring case.
+// It also accepts numeric offsets that would result in a different string on
+// output. For example, "Error-8" would marshal as "INFO".
+func (l *Level) UnmarshalText(data []byte) error {
+ return l.parse(string(data))
+}
+
+func (l *Level) parse(s string) (err error) {
+ defer func() {
+ if err != nil {
+ err = fmt.Errorf("slog: level string %q: %w", s, err)
+ }
+ }()
+
+ name := s
+ offset := 0
+ if i := strings.IndexAny(s, "+-"); i >= 0 {
+ name = s[:i]
+ offset, err = strconv.Atoi(s[i:])
+ if err != nil {
+ return err
+ }
+ }
+ switch strings.ToUpper(name) {
+ case "DEBUG":
+ *l = LevelDebug
+ case "INFO":
+ *l = LevelInfo
+ case "WARN":
+ *l = LevelWarn
+ case "ERROR":
+ *l = LevelError
+ default:
+ return errors.New("unknown name")
+ }
+ *l += Level(offset)
+ return nil
+}
+
// Level returns the receiver.
// It implements Leveler.
func (l Level) Level() Level { return l }
diff --git a/slog/level_test.go b/slog/level_test.go
index 833ef3e..38be813 100644
--- a/slog/level_test.go
+++ b/slog/level_test.go
@@ -5,6 +5,8 @@
package slog
import (
+ "flag"
+ "strings"
"testing"
)
@@ -47,3 +49,90 @@
}
}
+
+func TestMarshalJSON(t *testing.T) {
+ want := LevelWarn - 3
+ data, err := want.MarshalJSON()
+ if err != nil {
+ t.Fatal(err)
+ }
+ var got Level
+ if err := got.UnmarshalJSON(data); err != nil {
+ t.Fatal(err)
+ }
+ if got != want {
+ t.Errorf("got %s, want %s", got, want)
+ }
+}
+
+func TestMarshalText(t *testing.T) {
+ want := LevelWarn - 3
+ data, err := want.MarshalText()
+ if err != nil {
+ t.Fatal(err)
+ }
+ var got Level
+ if err := got.UnmarshalText(data); err != nil {
+ t.Fatal(err)
+ }
+ if got != want {
+ t.Errorf("got %s, want %s", got, want)
+ }
+}
+
+func TestLevelParse(t *testing.T) {
+ for _, test := range []struct {
+ in string
+ want Level
+ }{
+ {"DEBUG", LevelDebug},
+ {"INFO", LevelInfo},
+ {"WARN", LevelWarn},
+ {"ERROR", LevelError},
+ {"debug", LevelDebug},
+ {"iNfo", LevelInfo},
+ {"INFO+87", LevelInfo + 87},
+ {"Error-18", LevelError - 18},
+ {"Error-8", LevelInfo},
+ } {
+ var got Level
+ if err := got.parse(test.in); err != nil {
+ t.Fatalf("%q: %v", test.in, err)
+ }
+ if got != test.want {
+ t.Errorf("%q: got %s, want %s", test.in, got, test.want)
+ }
+ }
+}
+
+func TestLevelParseError(t *testing.T) {
+ for _, test := range []struct {
+ in string
+ want string // error string should contain this
+ }{
+ {"", "unknown name"},
+ {"dbg", "unknown name"},
+ {"INFO+", "invalid syntax"},
+ {"INFO-", "invalid syntax"},
+ {"ERROR+23x", "invalid syntax"},
+ } {
+ var l Level
+ err := l.parse(test.in)
+ if err == nil || !strings.Contains(err.Error(), test.want) {
+ t.Errorf("%q: got %v, want string containing %q", test.in, err, test.want)
+ }
+ }
+}
+
+func TestLevelFlag(t *testing.T) {
+ fs := flag.NewFlagSet("test", flag.ContinueOnError)
+ lf := LevelInfo
+ fs.TextVar(&lf, "level", lf, "set level")
+ err := fs.Parse([]string{"-level", "WARN+3"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if g, w := lf, LevelWarn+3; g != w {
+ t.Errorf("got %v, want %v", g, w)
+ }
+}