Skip to content

Commit 3d26679

Browse files
committed
feat: add TextFlag
* Added `TextFlag` which supports setting values for types that satisfies the `encoding.TextMarshaller` and `encoding.TextUnmarshaller` which is very handy when you want to set log levels or string-like types that satifies the interfaces. Fixes: urfave#2051 Signed-off-by: Tobias Dahlberg <[email protected]>
1 parent efab65a commit 3d26679

File tree

4 files changed

+268
-0
lines changed

4 files changed

+268
-0
lines changed

flag_text.go

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package cli
2+
3+
import (
4+
"encoding"
5+
)
6+
7+
type TextMarshalUnmarshaler interface {
8+
encoding.TextMarshaler
9+
encoding.TextUnmarshaler
10+
}
11+
12+
// TextFlag enables you to set types that satisfies [TextMarshalUnmarshaler] using flags such as log levels.
13+
type TextFlag = FlagBase[TextMarshalUnmarshaler, NoConfig, TextValue]
14+
15+
type TextValue struct {
16+
Value TextMarshalUnmarshaler
17+
}
18+
19+
func (f TextValue) String() string {
20+
text, err := f.Value.MarshalText()
21+
if err != nil {
22+
return ""
23+
}
24+
25+
return string(text)
26+
}
27+
28+
func (f TextValue) Set(s string) error {
29+
return f.Value.UnmarshalText([]byte(s))
30+
}
31+
32+
func (f TextValue) Get() any {
33+
return f.Value
34+
}
35+
36+
func (f TextValue) Create(v TextMarshalUnmarshaler, p *TextMarshalUnmarshaler, _ NoConfig) Value {
37+
pp := *p
38+
if v != nil {
39+
if b, err := v.MarshalText(); err == nil {
40+
_ = pp.UnmarshalText(b)
41+
}
42+
}
43+
44+
return &TextValue{
45+
Value: pp,
46+
}
47+
}
48+
49+
func (f TextValue) ToString(v TextMarshalUnmarshaler) string {
50+
text, _ := v.MarshalText()
51+
52+
return string(text)
53+
}

flag_text_test.go

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package cli_test
2+
3+
import (
4+
"context"
5+
"encoding"
6+
"errors"
7+
"io"
8+
"log/slog"
9+
"slices"
10+
"testing"
11+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
15+
"github.com/urfave/cli/v3"
16+
)
17+
18+
type badMarshaller struct{}
19+
20+
func (badMarshaller) UnmarshalText(_ []byte) error {
21+
return nil
22+
}
23+
24+
func (badMarshaller) MarshalText() ([]byte, error) {
25+
return nil, errors.New("bad")
26+
}
27+
28+
func ptr[T any](v T) *T {
29+
return &v
30+
}
31+
32+
func TestTextFlag(t *testing.T) {
33+
tests := []struct {
34+
name string
35+
flag *cli.TextFlag
36+
args []string
37+
want string
38+
wantErr bool
39+
}{
40+
{
41+
name: "empty",
42+
flag: &cli.TextFlag{
43+
Name: "log-level",
44+
Value: &slog.LevelVar{},
45+
Destination: ptr[cli.TextMarshalUnmarshaler](&slog.LevelVar{}),
46+
},
47+
want: "INFO",
48+
},
49+
{
50+
name: "info",
51+
flag: &cli.TextFlag{
52+
Name: "log-level",
53+
Value: &slog.LevelVar{},
54+
Destination: ptr[cli.TextMarshalUnmarshaler](&slog.LevelVar{}),
55+
Validator: func(v cli.TextMarshalUnmarshaler) error {
56+
text, err := v.MarshalText()
57+
if err != nil {
58+
return err
59+
}
60+
61+
if !slices.Equal(text, []byte("INFO")) {
62+
return errors.New("expected \"INFO\"")
63+
}
64+
65+
return nil
66+
},
67+
},
68+
args: []string{"--log-level", "info"},
69+
want: "INFO",
70+
},
71+
{
72+
name: "debug",
73+
flag: &cli.TextFlag{
74+
Name: "log-level",
75+
Value: &slog.LevelVar{},
76+
Destination: ptr[cli.TextMarshalUnmarshaler](&slog.LevelVar{}),
77+
},
78+
args: []string{"--log-level", "debug"},
79+
want: "DEBUG",
80+
},
81+
{
82+
name: "invalid",
83+
flag: &cli.TextFlag{
84+
Name: "log-level",
85+
Value: &slog.LevelVar{},
86+
Destination: ptr[cli.TextMarshalUnmarshaler](&slog.LevelVar{}),
87+
},
88+
args: []string{"--log-level", "invalid"},
89+
want: "INFO",
90+
wantErr: true,
91+
},
92+
{
93+
name: "bad_marshaller",
94+
flag: &cli.TextFlag{
95+
Name: "text",
96+
Value: &badMarshaller{},
97+
Destination: ptr[cli.TextMarshalUnmarshaler](&badMarshaller{}),
98+
},
99+
args: []string{"--text", "foo"},
100+
wantErr: true,
101+
},
102+
{
103+
name: "default",
104+
flag: &cli.TextFlag{
105+
Name: "log-level",
106+
Value: func() *slog.LevelVar {
107+
var l slog.LevelVar
108+
109+
l.Set(slog.LevelWarn)
110+
111+
return &l
112+
}(),
113+
Destination: ptr[cli.TextMarshalUnmarshaler](&slog.LevelVar{}),
114+
},
115+
args: []string{},
116+
want: "WARN",
117+
},
118+
{
119+
name: "override_default",
120+
flag: &cli.TextFlag{
121+
Name: "log-level",
122+
Value: func() *slog.LevelVar {
123+
var l slog.LevelVar
124+
125+
l.Set(slog.LevelWarn)
126+
127+
return &l
128+
}(),
129+
Destination: ptr[cli.TextMarshalUnmarshaler](&slog.LevelVar{}),
130+
},
131+
args: []string{"--log-level", "error"},
132+
want: "ERROR",
133+
},
134+
}
135+
136+
t.Parallel()
137+
138+
for _, tt := range tests {
139+
t.Run(tt.name, func(t *testing.T) {
140+
cmd := &cli.Command{
141+
Name: tt.name,
142+
Flags: []cli.Flag{tt.flag},
143+
Writer: io.Discard,
144+
ErrWriter: io.Discard,
145+
}
146+
147+
err := cmd.Run(context.TODO(), append([]string{"mock"}, tt.args...))
148+
149+
if err != nil && !tt.wantErr {
150+
require.NoError(t, err)
151+
152+
return
153+
} else if err != nil {
154+
return
155+
}
156+
157+
var got []byte
158+
159+
got, err = tt.flag.Get().(encoding.TextMarshaler).MarshalText()
160+
if tt.wantErr {
161+
require.Error(t, err)
162+
163+
return
164+
}
165+
166+
assert.Equal(t, tt.want, string(got))
167+
})
168+
}
169+
}

godoc-current.txt

+23
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,29 @@ type SuggestCommandFunc func(commands []*Command, provided string) string
10401040

10411041
type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string
10421042

1043+
type TextFlag = FlagBase[TextMarshalUnmarshaler, NoConfig, TextValue]
1044+
TextFlag enables you to set types that satisfies TextMarshalUnmarshaler
1045+
using flags such as log levels.
1046+
1047+
type TextMarshalUnmarshaler interface {
1048+
encoding.TextMarshaler
1049+
encoding.TextUnmarshaler
1050+
}
1051+
1052+
type TextValue struct {
1053+
Value TextMarshalUnmarshaler
1054+
}
1055+
1056+
func (f TextValue) Create(v TextMarshalUnmarshaler, p *TextMarshalUnmarshaler, _ NoConfig) Value
1057+
1058+
func (f TextValue) Get() any
1059+
1060+
func (f TextValue) Set(s string) error
1061+
1062+
func (f TextValue) String() string
1063+
1064+
func (f TextValue) ToString(v TextMarshalUnmarshaler) string
1065+
10431066
type TimestampArg = ArgumentBase[time.Time, TimestampConfig, timestampValue]
10441067

10451068
type TimestampConfig struct {

testdata/godoc-v3.x.txt

+23
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,29 @@ type SuggestCommandFunc func(commands []*Command, provided string) string
10401040

10411041
type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string
10421042

1043+
type TextFlag = FlagBase[TextMarshalUnmarshaler, NoConfig, TextValue]
1044+
TextFlag enables you to set types that satisfies TextMarshalUnmarshaler
1045+
using flags such as log levels.
1046+
1047+
type TextMarshalUnmarshaler interface {
1048+
encoding.TextMarshaler
1049+
encoding.TextUnmarshaler
1050+
}
1051+
1052+
type TextValue struct {
1053+
Value TextMarshalUnmarshaler
1054+
}
1055+
1056+
func (f TextValue) Create(v TextMarshalUnmarshaler, p *TextMarshalUnmarshaler, _ NoConfig) Value
1057+
1058+
func (f TextValue) Get() any
1059+
1060+
func (f TextValue) Set(s string) error
1061+
1062+
func (f TextValue) String() string
1063+
1064+
func (f TextValue) ToString(v TextMarshalUnmarshaler) string
1065+
10431066
type TimestampArg = ArgumentBase[time.Time, TimestampConfig, timestampValue]
10441067

10451068
type TimestampConfig struct {

0 commit comments

Comments
 (0)