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)
+	}
+}