Skip to content

Commit 95675de

Browse files
authored
feat: signature defaults (#581)
Thanks to @cmilesdev for the first pass in #579!
1 parent becd4f3 commit 95675de

File tree

2 files changed

+214
-4
lines changed

2 files changed

+214
-4
lines changed

signature_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package kong_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/alecthomas/assert/v2"
8+
9+
"github.com/alecthomas/kong"
10+
)
11+
12+
// Command with value receiver.
13+
type signatureCmd struct {
14+
Arg string `arg:"" optional:""`
15+
}
16+
17+
func (signatureCmd) Signature() string {
18+
return `cmd:"" name:"sig" help:"Signature help" aliases:"s,sg"`
19+
}
20+
21+
func TestSignatureCommand(t *testing.T) {
22+
var cli struct {
23+
Cmd signatureCmd
24+
}
25+
p := mustNew(t, &cli)
26+
27+
// Should be reachable by the signature-provided name.
28+
_, err := p.Parse([]string{"sig", "value"})
29+
assert.NoError(t, err)
30+
assert.Equal(t, "value", cli.Cmd.Arg)
31+
32+
// Should be reachable by aliases.
33+
_, err = p.Parse([]string{"s", "other"})
34+
assert.NoError(t, err)
35+
assert.Equal(t, "other", cli.Cmd.Arg)
36+
37+
_, err = p.Parse([]string{"sg", "third"})
38+
assert.NoError(t, err)
39+
assert.Equal(t, "third", cli.Cmd.Arg)
40+
}
41+
42+
func TestSignatureCommandHelp(t *testing.T) {
43+
var cli struct {
44+
Cmd signatureCmd
45+
}
46+
buf := &strings.Builder{}
47+
p := mustNew(t, &cli, kong.Writers(buf, buf), kong.Exit(func(int) {}))
48+
_, _ = p.Parse([]string{"--help"})
49+
assert.Contains(t, buf.String(), "Signature help")
50+
}
51+
52+
// Command with pointer receiver.
53+
type signaturePtrCmd struct {
54+
Flag string `default:"def"`
55+
}
56+
57+
func (*signaturePtrCmd) Signature() string {
58+
return `cmd:"" name:"ptrcmd" help:"Pointer receiver help"`
59+
}
60+
61+
func TestSignaturePointerReceiver(t *testing.T) {
62+
var cli struct {
63+
Cmd *signaturePtrCmd
64+
}
65+
p := mustNew(t, &cli)
66+
_, err := p.Parse([]string{"ptrcmd"})
67+
assert.NoError(t, err)
68+
assert.Equal(t, "def", cli.Cmd.Flag)
69+
}
70+
71+
func TestSignaturePointerReceiverHelp(t *testing.T) {
72+
var cli struct {
73+
Cmd *signaturePtrCmd
74+
}
75+
buf := &strings.Builder{}
76+
p := mustNew(t, &cli, kong.Writers(buf, buf), kong.Exit(func(int) {}))
77+
_, _ = p.Parse([]string{"--help"})
78+
assert.Contains(t, buf.String(), "Pointer receiver help")
79+
}
80+
81+
func TestSignatureFieldTagOverrides(t *testing.T) {
82+
var cli struct {
83+
// Field tag overrides the name from the signature, but help comes from signature.
84+
Cmd signatureCmd `cmd:"" name:"override"`
85+
}
86+
p := mustNew(t, &cli)
87+
88+
// The overridden name should work.
89+
_, err := p.Parse([]string{"override", "val"})
90+
assert.NoError(t, err)
91+
assert.Equal(t, "val", cli.Cmd.Arg)
92+
93+
// The signature name should NOT work because the field tag overrode it.
94+
_, err = p.Parse([]string{"sig"})
95+
assert.Error(t, err)
96+
}
97+
98+
func TestSignatureFieldTagOverridesHelp(t *testing.T) {
99+
var cli struct {
100+
Cmd signatureCmd `cmd:"" name:"override"`
101+
}
102+
buf := &strings.Builder{}
103+
p := mustNew(t, &cli, kong.Writers(buf, buf), kong.Exit(func(int) {}))
104+
_, _ = p.Parse([]string{"--help"})
105+
// Help from signature should still be present since field tag didn't override it.
106+
assert.Contains(t, buf.String(), "Signature help")
107+
}
108+
109+
// Non-struct type implementing Signature as a flag.
110+
type signatureFlag string
111+
112+
func (signatureFlag) Signature() string {
113+
return `help:"Flag from signature" default:"sigdefault"`
114+
}
115+
116+
func TestSignatureNonStructFlag(t *testing.T) {
117+
var cli struct {
118+
Flag signatureFlag
119+
}
120+
p := mustNew(t, &cli)
121+
_, err := p.Parse(nil)
122+
assert.NoError(t, err)
123+
assert.Equal(t, signatureFlag("sigdefault"), cli.Flag)
124+
}
125+
126+
func TestSignatureNonStructFlagFieldOverrides(t *testing.T) {
127+
var cli struct {
128+
Flag signatureFlag `default:"fieldval"`
129+
}
130+
p := mustNew(t, &cli)
131+
_, err := p.Parse(nil)
132+
assert.NoError(t, err)
133+
// Field tag should override the signature default.
134+
assert.Equal(t, signatureFlag("fieldval"), cli.Flag)
135+
}
136+
137+
// Empty signature should be ignored.
138+
type emptySignatureCmd struct{}
139+
140+
func (emptySignatureCmd) Signature() string { return "" }
141+
142+
func TestSignatureEmptyIgnored(t *testing.T) {
143+
var cli struct {
144+
Cmd emptySignatureCmd `cmd:""`
145+
}
146+
p := mustNew(t, &cli)
147+
_, err := p.Parse([]string{"cmd"})
148+
assert.NoError(t, err)
149+
}
150+
151+
// Whitespace-only signature should also be ignored.
152+
type whitespaceSignatureCmd struct{}
153+
154+
func (whitespaceSignatureCmd) Signature() string { return " " }
155+
156+
func TestSignatureWhitespaceIgnored(t *testing.T) {
157+
var cli struct {
158+
Cmd whitespaceSignatureCmd `cmd:""`
159+
}
160+
p := mustNew(t, &cli)
161+
_, err := p.Parse([]string{"cmd"})
162+
assert.NoError(t, err)
163+
}

tag.go

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,13 @@ func parseTagItems(tagString string, chr tagChars) (map[string][]string, error)
166166
return d, nil
167167
}
168168

169-
func getTagInfo(ft reflect.StructField) (string, tagChars) {
170-
s, ok := ft.Tag.Lookup("kong")
169+
func getTagInfo(tag reflect.StructTag) (string, tagChars) {
170+
s, ok := tag.Lookup("kong")
171171
if ok {
172172
return s, kongChars
173173
}
174174

175-
return string(ft.Tag), bareChars
175+
return string(tag), bareChars
176176
}
177177

178178
func newEmptyTag() *Tag {
@@ -204,10 +204,26 @@ func parseTag(parent reflect.Value, ft reflect.StructField) (*Tag, error) {
204204
t.Ignored = true
205205
return t, nil
206206
}
207-
items, err := parseTagItems(getTagInfo(ft))
207+
items := map[string][]string{}
208+
// First use a [Signature] if present
209+
signatureTag, ok := maybeGetSignature(ft.Type)
210+
if ok {
211+
signatureItems, err := parseTagItems(getTagInfo(signatureTag))
212+
if err != nil {
213+
return nil, err
214+
}
215+
items = signatureItems
216+
}
217+
// Next overlay the field's tags.
218+
fieldItems, err := parseTagItems(getTagInfo(ft.Tag))
208219
if err != nil {
209220
return nil, err
210221
}
222+
for key, value := range fieldItems {
223+
// Prepend field tag values
224+
items[key] = append(value, items[key]...)
225+
}
226+
211227
t := &Tag{
212228
items: items,
213229
}
@@ -384,3 +400,34 @@ func (t *Tag) GetSep(k string, dflt rune) (rune, error) {
384400
}
385401
return r, nil
386402
}
403+
404+
// Signature allows flags, args and commands to supply a default set of tags,
405+
// that can be overridden by the field itself.
406+
type Signature interface {
407+
// Signature returns default tags for the flag, arg or command.
408+
//
409+
// eg. `name:"migrate" help:"Run migrations" aliases:"mig,mg"`.
410+
Signature() string
411+
}
412+
413+
var signatureOverrideType = reflect.TypeOf((*Signature)(nil)).Elem()
414+
415+
func maybeGetSignature(t reflect.Type) (reflect.StructTag, bool) {
416+
ut := t
417+
if ut.Kind() == reflect.Pointer {
418+
ut = ut.Elem()
419+
}
420+
ptr := reflect.New(ut)
421+
var sig string
422+
for _, v := range []reflect.Value{ptr, ptr.Elem()} {
423+
if v.Type().Implements(signatureOverrideType) {
424+
sig = v.Interface().(Signature).Signature() //nolint:forcetypeassert
425+
break
426+
}
427+
}
428+
sig = strings.TrimSpace(sig)
429+
if sig == "" {
430+
return "", false
431+
}
432+
return reflect.StructTag(sig), true
433+
}

0 commit comments

Comments
 (0)