Skip to content

Commit 4ff212d

Browse files
authored
A generic mechanism for supporting new types (#23)
1 parent 9de0c49 commit 4ff212d

File tree

9 files changed

+147
-242
lines changed

9 files changed

+147
-242
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
name: Test
22

33
on:
4+
workflow_dispatch:
45
push:
56
branches: [ master ]
67
pull_request:
@@ -16,7 +17,7 @@ jobs:
1617
- name: Set up Go
1718
uses: actions/setup-go@v4
1819
with:
19-
go-version: '1.16'
20+
go-version: '1.19'
2021

2122
- name: Test
2223
run: go test -v ./...

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ import "github.com/itzg/go-flagsfiller"
2929
- Beyond the standard types supported by flag.FlagSet also includes support for:
3030
- `[]string` where repetition of the argument appends to the slice and/or an argument value can contain a comma-separated list of values. For example: `--arg one --arg two,three`
3131
- `map[string]string` where each entry is a `key=value` and/or repetition of the arguments adds to the map or multiple entries can be comma-separated in a single argument value. For example: `--arg k1=v1 --arg k2=v2,k3=v3`
32+
- `time.Time` parse via time.Parse(), with tag `layout` specify the layout string, default is "2006-01-02 15:04:05"
33+
- `net.IP` parse via net.ParseIP()
34+
- `net.IPNet` parse via net.ParseCIDR()
35+
- `net.HardwareAddr` parse via net.ParseMAC()
3236
- Optionally set flag values from environment variables. Similar to flag names, environment variable names are derived automatically from the field names
37+
- New types could be supported via user code, via `RegisterSimpleType(ConvertFunc)`, check [time.go](time.go) and [net.go](net.go) to see how it works
3338

3439
## Quick example
3540

flagset.go

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,8 @@ func (f *FlagSetFiller) Fill(flagSet *flag.FlagSet, from interface{}) error {
5555
}
5656
}
5757

58-
// this is a list of supported struct, like time.Time, that walkFields() won't walk into,
59-
// the key is the is string returned by the getTypeName(<struct_type>),
60-
// each supported struct need to be added in this map in init()
61-
var supportedStructList = make(map[string]struct{})
62-
6358
func isSupportedStruct(name string) bool {
64-
_, ok := supportedStructList[name]
59+
_, ok := extendedTypes[name]
6560
return ok
6661
}
6762

@@ -181,20 +176,13 @@ func (f *FlagSetFiller) processField(flagSet *flag.FlagSet, fieldRef interface{}
181176
renamed = f.options.renameLongName(name)
182177
}
183178
typeName := getTypeName(t)
184-
switch {
185-
//check the typeName
186-
case typeName == "net.IP":
187-
f.processIP(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases)
188-
case typeName == "net.IPNet":
189-
f.processIPNet(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases)
190-
case typeName == "net.HardwareAddr":
191-
f.processMAC(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases)
192-
193-
case typeName == "time.Time":
194-
layoutStr, _ := tag.Lookup("layout")
195-
f.processTime(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases, layoutStr)
196-
//end of check typeName
197179

180+
// go through all supported structs
181+
if handler, ok := extendedTypes[typeName]; ok {
182+
err = handler(tag, fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases)
183+
}
184+
185+
switch {
198186
case t.Kind() == reflect.String:
199187
f.processString(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases)
200188

general.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package flagsfiller
2+
3+
/*
4+
The code in this file could be opened up in future if more complex implementation is needed
5+
*/
6+
7+
import (
8+
"flag"
9+
"fmt"
10+
"reflect"
11+
"strings"
12+
)
13+
14+
// this is a list of addtional supported types(include struct), like time.Time, that walkFields() won't walk into,
15+
// the key is the is string returned by the getTypeName(<type>),
16+
// each supported type need to be added in this map in init()
17+
var extendedTypes = make(map[string]handlerFunc)
18+
19+
type handlerFunc func(tag reflect.StructTag, fieldRef interface{},
20+
hasDefaultTag bool, tagDefault string,
21+
flagSet *flag.FlagSet, renamed string,
22+
usage string, aliases string) error
23+
24+
type flagVal[T any] interface {
25+
flag.Value
26+
StrConverter(string) (T, error)
27+
SetRef(*T)
28+
}
29+
30+
func processGeneral[T any](fieldRef interface{}, val flagVal[T],
31+
hasDefaultTag bool, tagDefault string,
32+
flagSet *flag.FlagSet, renamed string,
33+
usage string, aliases string) (err error) {
34+
35+
casted := fieldRef.(*T)
36+
if hasDefaultTag {
37+
*casted, err = val.StrConverter(tagDefault)
38+
if err != nil {
39+
return fmt.Errorf("failed to parse default into %T: %w", *new(T), err)
40+
}
41+
}
42+
val.SetRef(casted)
43+
flagSet.Var(val, renamed, usage)
44+
if aliases != "" {
45+
for _, alias := range strings.Split(aliases, ",") {
46+
flagSet.Var(val, alias, usage)
47+
}
48+
}
49+
return nil
50+
51+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/itzg/go-flagsfiller
22

3-
go 1.16
3+
go 1.19
44

55
require (
66
github.com/iancoleman/strcase v0.2.0

mac.go

Lines changed: 0 additions & 57 deletions
This file was deleted.

net.go

Lines changed: 15 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,33 @@
11
package flagsfiller
22

33
import (
4-
"flag"
54
"fmt"
65
"net"
7-
"strings"
6+
"reflect"
87
)
98

109
func init() {
11-
supportedStructList["net.IPNet"] = struct{}{}
10+
RegisterSimpleType(ipConverter)
11+
RegisterSimpleType(ipnetConverter)
12+
RegisterSimpleType(macConverter)
1213
}
1314

14-
type ipValue struct {
15-
addr *net.IP
16-
}
17-
18-
func (v *ipValue) String() string {
19-
if v.addr == nil {
20-
return fmt.Sprint(nil)
21-
}
22-
return v.addr.String()
23-
}
24-
25-
func (v *ipValue) Set(s string) error {
26-
*v.addr = net.ParseIP(s)
27-
if *v.addr == nil {
28-
return fmt.Errorf("invalid ip addr %v", s)
29-
}
30-
return nil
31-
}
32-
33-
func (f *FlagSetFiller) processIP(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string, aliases string) (err error) {
34-
casted, ok := fieldRef.(*net.IP)
35-
if !ok {
36-
return f.processCustom(
37-
fieldRef,
38-
func(s string) (interface{}, error) {
39-
value := net.ParseIP(s)
40-
if value == nil {
41-
return nil, fmt.Errorf("invalid IP address %s", s)
42-
}
43-
return value, nil
44-
},
45-
hasDefaultTag,
46-
tagDefault,
47-
flagSet,
48-
renamed,
49-
usage,
50-
aliases,
51-
)
52-
}
53-
54-
if hasDefaultTag {
55-
*casted = net.ParseIP(tagDefault)
56-
if *casted == nil {
57-
return fmt.Errorf("failed to parse default into net.IP: %s", tagDefault)
58-
}
15+
func ipConverter(s string, tag reflect.StructTag) (net.IP, error) {
16+
addr := net.ParseIP(s)
17+
if addr == nil {
18+
return nil, fmt.Errorf("%s is not a valid IP address", s)
5919
}
60-
flagSet.Var(&ipValue{casted}, renamed, usage)
61-
if aliases != "" {
62-
for _, alias := range strings.Split(aliases, ",") {
63-
flagSet.Var(&ipValue{casted}, alias, usage)
64-
}
65-
}
66-
return nil
67-
}
68-
69-
type ipnetValue struct {
70-
prefix *net.IPNet
71-
}
72-
73-
func (v *ipnetValue) String() string {
74-
if v.prefix == nil {
75-
return fmt.Sprint(nil)
76-
}
77-
return v.prefix.String()
20+
return addr, nil
7821
}
7922

80-
func (v *ipnetValue) Set(s string) error {
81-
_, pr, err := net.ParseCIDR(s)
23+
func ipnetConverter(s string, tag reflect.StructTag) (net.IPNet, error) {
24+
_, prefix, err := net.ParseCIDR(s)
8225
if err != nil {
83-
return fmt.Errorf("invalid ip prefix %v", s)
26+
return net.IPNet{}, err
8427
}
85-
*v.prefix = *pr
86-
return nil
28+
return *prefix, nil
8729
}
8830

89-
func (f *FlagSetFiller) processIPNet(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string, aliases string) (err error) {
90-
casted, ok := fieldRef.(*net.IPNet)
91-
if !ok {
92-
return f.processCustom(
93-
fieldRef,
94-
func(s string) (interface{}, error) {
95-
_, value, err := net.ParseCIDR(s)
96-
if err != nil {
97-
return nil, fmt.Errorf("invalid IP prefix %s, %w", s, err)
98-
}
99-
return *value, nil
100-
},
101-
hasDefaultTag,
102-
tagDefault,
103-
flagSet,
104-
renamed,
105-
usage,
106-
aliases,
107-
)
108-
}
109-
110-
if hasDefaultTag {
111-
_, casted, err = net.ParseCIDR(tagDefault)
112-
if err != nil {
113-
return fmt.Errorf("failed to parse default into net.IPNet: %s, %w", tagDefault, err)
114-
}
115-
}
116-
flagSet.Var(&ipnetValue{casted}, renamed, usage)
117-
if aliases != "" {
118-
for _, alias := range strings.Split(aliases, ",") {
119-
flagSet.Var(&ipnetValue{casted}, alias, usage)
120-
}
121-
}
122-
return nil
31+
func macConverter(s string, tag reflect.StructTag) (net.HardwareAddr, error) {
32+
return net.ParseMAC(s)
12333
}

simple.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package flagsfiller
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"reflect"
7+
)
8+
9+
// RegisterSimpleType register a new type,
10+
// should be called in init(),
11+
// see time.go and net.go for implementation examples
12+
func RegisterSimpleType[T any](c ConvertFunc[T]) {
13+
base := simpleType[T]{converter: c}
14+
extendedTypes[getTypeName(reflect.TypeOf(*new(T)))] = base.Process
15+
}
16+
17+
// ConvertFunc is a function convert string s into a specific type T, the tag is the struct field tag, as addtional input.
18+
// see time.go and net.go for implementation examples
19+
type ConvertFunc[T any] func(s string, tag reflect.StructTag) (T, error)
20+
21+
type simpleType[T any] struct {
22+
val *T
23+
tags reflect.StructTag
24+
converter ConvertFunc[T]
25+
}
26+
27+
func newSimpleType[T any](c ConvertFunc[T], tag reflect.StructTag) simpleType[T] {
28+
return simpleType[T]{val: new(T), converter: c, tags: tag}
29+
}
30+
31+
func (v *simpleType[T]) String() string {
32+
if v.val == nil {
33+
return fmt.Sprint(nil)
34+
}
35+
return fmt.Sprintf("%v", *v.val)
36+
}
37+
38+
func (v *simpleType[T]) StrConverter(s string) (T, error) {
39+
return v.converter(s, v.tags)
40+
}
41+
42+
func (v *simpleType[T]) Set(s string) error {
43+
var err error
44+
*v.val, err = v.converter(s, v.tags)
45+
if err != nil {
46+
return fmt.Errorf("failed to parse %s into %T, %w", s, *(new(T)), err)
47+
}
48+
return nil
49+
}
50+
51+
func (v *simpleType[T]) SetRef(t *T) {
52+
v.val = t
53+
}
54+
55+
func (v *simpleType[T]) Process(tag reflect.StructTag, fieldRef interface{},
56+
hasDefaultTag bool, tagDefault string,
57+
flagSet *flag.FlagSet, renamed string,
58+
usage string, aliases string) error {
59+
val := newSimpleType(v.converter, tag)
60+
return processGeneral[T](fieldRef, &val, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases)
61+
}

0 commit comments

Comments
 (0)