Skip to content

Commit 7ad6f36

Browse files
committed
refactor(command): update visitor pattern
1 parent 3c93d36 commit 7ad6f36

File tree

17 files changed

+511
-307
lines changed

17 files changed

+511
-307
lines changed

cmd/go_flags_parse.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"strings"
1010

1111
"github.com/fatih/structtag"
12-
"github.com/pot-code/web-cli/internal/validate"
1312
)
1413

1514
type tagMeta struct {
@@ -38,10 +37,23 @@ func parseTag(tag reflect.StructTag) (*tagMeta, error) {
3837
tg.Default = d.Value()
3938
}
4039

41-
tg.Required = validate.IsRequired(tag)
40+
tg.Required = isRequired(tag)
4241
return tg, nil
4342
}
4443

44+
func isRequired(tag reflect.StructTag) bool {
45+
if tag == "" {
46+
return false
47+
}
48+
49+
vt := tag.Get("validate")
50+
if vt == "" {
51+
return false
52+
}
53+
54+
return strings.Contains(vt, "required")
55+
}
56+
4557
func parseConfigFile(config *GoFlagsConfig) (*configStructVisitor, error) {
4658
fset := token.NewFileSet()
4759
f, err := parser.ParseFile(fset, config.ConfigPath, nil, parser.ParseComments)

cmd/go_server.go renamed to cmd/go_web.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
type GoWebConfig struct {
1717
ProjectName string `arg:"0" alias:"project_name" validate:"required,var"`
1818
AuthorName string `flag:"author" alias:"a" usage:"author name for the app" validate:"required,var"`
19-
GoVersion string `flag:"version" alias:"v" usage:"go compiler version" validate:"required,version"`
19+
GoVersion string `flag:"version" alias:"v" usage:"go compiler version" validate:"version"`
2020
}
2121

2222
var GoWebCmd = command.NewCliCommand("web", "generate golang web project",
@@ -32,7 +32,7 @@ var GenWebProject = command.InlineHandler(func(c *cli.Context, cfg interface{})
3232
projectName := config.ProjectName
3333
authorName := config.AuthorName
3434

35-
if util.FileExists(projectName) {
35+
if util.Exists(projectName) {
3636
log.Infof("folder '%s' already exists", projectName)
3737
return nil
3838
}

cmd/react_init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
type ReactInitConfig struct {
11-
GenType string `flag:"type" alias:"t" usage:"project type" validate:"required,oneof=vanilla next"`
11+
GenType string `flag:"type" alias:"t" usage:"project type" validate:"oneof=vanilla next"`
1212
ProjectName string `arg:"0" alias:"project_name" validate:"required,var"`
1313
}
1414

cmd/root.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package cmd
22

33
import (
4-
log "github.com/sirupsen/logrus"
54
"github.com/urfave/cli/v2"
65
)
76

@@ -26,9 +25,9 @@ var RootCmd = &cli.App{
2625
},
2726
},
2827
Before: func(c *cli.Context) error {
29-
if c.Bool("debug") {
30-
log.SetLevel(log.DebugLevel)
31-
}
28+
// if c.Bool("debug") {
29+
// log.SetLevel(log.DebugLevel)
30+
// }
3231
return nil
3332
},
3433
}

internal/command/cli.go

Lines changed: 25 additions & 219 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
11
package command
22

33
import (
4-
"fmt"
5-
"reflect"
6-
"strconv"
7-
"strings"
8-
94
"github.com/go-playground/validator/v10"
105
"github.com/pkg/errors"
11-
"github.com/pot-code/web-cli/internal/util"
126
"github.com/pot-code/web-cli/internal/validate"
13-
log "github.com/sirupsen/logrus"
147
"github.com/urfave/cli/v2"
158
)
169

1710
type CliCommand struct {
1811
cmd *cli.Command
1912
cfg interface{}
20-
services []CommandHandler
13+
handlers []CommandHandler
2114
}
2215

2316
type CommandOption interface {
@@ -53,236 +46,49 @@ func NewCliCommand(name, usage string, defaultConfig interface{}, options ...Com
5346
Name: name,
5447
Usage: usage,
5548
}
56-
57-
visitor := NewExtractFlagsVisitor()
58-
IterateCliConfig(defaultConfig, visitor, nil)
59-
cmd.Flags = append(cmd.Flags, visitor.Flags...)
60-
6149
for _, option := range options {
6250
option.apply(cmd)
6351
}
6452
return &CliCommand{cmd, defaultConfig, []CommandHandler{}}
6553
}
6654

6755
func (cc *CliCommand) AddHandlers(h ...CommandHandler) *CliCommand {
68-
cc.services = append(cc.services, h...)
56+
cc.handlers = append(cc.handlers, h...)
6957
return cc
7058
}
7159

7260
func (cc *CliCommand) BuildCommand() *cli.Command {
73-
cc.cmd.Action = func(c *cli.Context) error {
74-
cfg := cc.cfg
61+
cfg := cc.cfg
7562

76-
if cfg != nil {
77-
IterateCliConfig(cfg, NewSetCliValueVisitor(), c)
78-
if err := validate.V.Struct(cfg); err != nil {
79-
if v, ok := err.(validator.ValidationErrors); ok {
80-
msg := v[0].Translate(validate.T)
81-
cli.ShowCommandHelp(c, c.Command.Name)
82-
return util.NewCommandError(c.Command.Name, errors.New(msg))
83-
}
84-
return err
85-
}
63+
efv := newExtractFlagsVisitor()
64+
walkConfig(cfg, efv)
65+
66+
cc.cmd.Flags = append(cc.cmd.Flags, efv.getFlags()...)
67+
cc.cmd.Before = func(c *cli.Context) error {
68+
scv := newSetConfigVisitor(c)
69+
walkConfig(cfg, scv)
70+
71+
if errs := scv.getErrors(); errs != nil {
72+
return errs[0]
8673
}
8774

88-
for _, s := range cc.services {
75+
if err := validate.V.Struct(cfg); err != nil {
76+
if v, ok := err.(validator.ValidationErrors); ok {
77+
msg := v[0].Translate(validate.T)
78+
cli.ShowCommandHelp(c, c.Command.Name)
79+
return errors.New(msg)
80+
}
81+
return err
82+
}
83+
return nil
84+
}
85+
cc.cmd.Action = func(c *cli.Context) error {
86+
for _, s := range cc.handlers {
8987
if err := s.Handle(c, cfg); err != nil {
9088
return err
9189
}
9290
}
93-
9491
return nil
9592
}
9693
return cc.cmd
9794
}
98-
99-
type ConfigStructVisitor interface {
100-
VisitStringType(reflect.StructField, reflect.Value, *cli.Context) error
101-
VisitBooleanType(reflect.StructField, reflect.Value, *cli.Context) error
102-
VisitIntType(reflect.StructField, reflect.Value, *cli.Context) error
103-
}
104-
105-
func IterateCliConfig(config interface{}, visitor ConfigStructVisitor, ctx *cli.Context) {
106-
if config == nil {
107-
return
108-
}
109-
110-
directType := reflect.TypeOf(config)
111-
if directType.Kind() != reflect.Ptr {
112-
panic("config must be of pointer type")
113-
}
114-
structType := directType.Elem()
115-
116-
configStruct := reflect.Indirect(reflect.ValueOf(config)) // reflection wrapper for the config struct
117-
for i := structType.NumField() - 1; i >= 0; i-- {
118-
fieldType := structType.Field(i)
119-
fieldValue := configStruct.Field(i)
120-
121-
if !fieldValue.CanSet() {
122-
log.WithFields(log.Fields{
123-
"caller": "ParseCliConfig",
124-
}).Warnf("config field [ %s ] can't be set, maybe it's not exported", fieldType.Name)
125-
continue
126-
}
127-
128-
switch fieldType.Type.Kind() {
129-
case reflect.String:
130-
visitor.VisitStringType(fieldType, fieldValue, ctx)
131-
case reflect.Bool:
132-
visitor.VisitBooleanType(fieldType, fieldValue, ctx)
133-
case reflect.Int:
134-
visitor.VisitIntType(fieldType, fieldValue, ctx)
135-
default:
136-
panic("not implemented")
137-
}
138-
}
139-
}
140-
141-
type ExtractFlagsVisitor struct {
142-
Flags []cli.Flag
143-
}
144-
145-
func NewExtractFlagsVisitor() *ExtractFlagsVisitor {
146-
return &ExtractFlagsVisitor{Flags: []cli.Flag{}}
147-
}
148-
149-
var _ ConfigStructVisitor = &ExtractFlagsVisitor{}
150-
151-
func (efv *ExtractFlagsVisitor) VisitStringType(tf reflect.StructField, vf reflect.Value, c *cli.Context) error {
152-
tag := tf.Tag
153-
154-
if flagName := tag.Get("flag"); flagName != "" {
155-
alias := tag.Get("alias")
156-
usage := tag.Get("usage")
157-
158-
required := false
159-
if vf.IsZero() && validate.IsRequired(tag) {
160-
required = true
161-
}
162-
163-
flag := &cli.StringFlag{
164-
Name: flagName,
165-
Required: required,
166-
}
167-
168-
if alias != "" {
169-
flag.Aliases = strings.Split(alias, ",")
170-
}
171-
172-
if !vf.IsZero() {
173-
flag.Value = vf.String()
174-
}
175-
176-
options := validate.GetOneOfItems(tag)
177-
if len(options) > 0 {
178-
usage += fmt.Sprintf(" (options: %s)", strings.Join(options, ", "))
179-
}
180-
flag.Usage = usage
181-
182-
log.WithFields(log.Fields{
183-
"name": flagName,
184-
"type": tf.Type.String(),
185-
"required": required,
186-
"alias": alias,
187-
"usage": usage,
188-
}).Debug("register flag")
189-
190-
efv.Flags = append(efv.Flags, flag)
191-
}
192-
return nil
193-
}
194-
195-
func (efv *ExtractFlagsVisitor) VisitBooleanType(tf reflect.StructField, vf reflect.Value, c *cli.Context) error {
196-
if flagName := tf.Tag.Get("flag"); flagName != "" {
197-
usage := tf.Tag.Get("usage")
198-
alias := tf.Tag.Get("alias")
199-
200-
flag := &cli.BoolFlag{
201-
Name: flagName,
202-
Usage: usage,
203-
}
204-
if alias != "" {
205-
flag.Aliases = strings.Split(alias, ",")
206-
}
207-
if !vf.IsZero() {
208-
flag.Value = vf.Bool()
209-
}
210-
211-
log.WithFields(log.Fields{
212-
"name": flagName,
213-
"type": tf.Type.String(),
214-
"alias": alias,
215-
"usage": usage,
216-
}).Debug("register flag")
217-
efv.Flags = append(efv.Flags, flag)
218-
}
219-
return nil
220-
}
221-
222-
func (efv *ExtractFlagsVisitor) VisitIntType(tf reflect.StructField, vf reflect.Value, c *cli.Context) error {
223-
if flagTag := tf.Tag.Get("flag"); flagTag != "" {
224-
usage := tf.Tag.Get("usage")
225-
alias := tf.Tag.Get("alias")
226-
227-
flagName := &cli.IntFlag{
228-
Name: flagTag,
229-
Usage: usage,
230-
}
231-
if alias != "" {
232-
flagName.Aliases = strings.Split(alias, ",")
233-
}
234-
if !vf.IsZero() {
235-
flagName.Value = int(vf.Int())
236-
}
237-
238-
log.WithFields(log.Fields{
239-
"name": flagName,
240-
"type": tf.Type.String(),
241-
"alias": alias,
242-
"usage": usage,
243-
}).Debug("register flag")
244-
efv.Flags = append(efv.Flags, flagName)
245-
}
246-
return nil
247-
}
248-
249-
type SetCliValueVisitor struct{}
250-
251-
func NewSetCliValueVisitor() *SetCliValueVisitor {
252-
return &SetCliValueVisitor{}
253-
}
254-
255-
var _ ConfigStructVisitor = &SetCliValueVisitor{}
256-
257-
func (efv *SetCliValueVisitor) VisitStringType(tf reflect.StructField, vf reflect.Value, c *cli.Context) error {
258-
if arg := tf.Tag.Get("arg"); arg != "" {
259-
index, err := strconv.Atoi(arg)
260-
if err != nil {
261-
panic(fmt.Sprintf("failed to convert [ %s ] to number", arg))
262-
}
263-
264-
kind := tf.Type.Kind()
265-
if kind == reflect.String {
266-
vf.SetString(c.Args().Get(index))
267-
} else {
268-
panic(fmt.Errorf("unsupported arg type: %s", kind.String()))
269-
}
270-
} else if flag := tf.Tag.Get("flag"); flag != "" {
271-
vf.SetString(c.String(flag))
272-
}
273-
return nil
274-
}
275-
276-
func (efv *SetCliValueVisitor) VisitBooleanType(tf reflect.StructField, vf reflect.Value, c *cli.Context) error {
277-
if flag := tf.Tag.Get("flag"); flag != "" {
278-
vf.SetBool(c.Bool(flag))
279-
}
280-
return nil
281-
}
282-
283-
func (efv *SetCliValueVisitor) VisitIntType(tf reflect.StructField, vf reflect.Value, c *cli.Context) error {
284-
if flag := tf.Tag.Get("flag"); flag != "" {
285-
vf.SetInt(int64(c.Int("flag")))
286-
}
287-
return nil
288-
}

internal/command/errors.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package command
2+
3+
type CommandError struct {
4+
cmd string
5+
err error
6+
}
7+
8+
func NewCommandError(cmd string, err error) *CommandError {
9+
return &CommandError{
10+
cmd, err,
11+
}
12+
}
13+
14+
func (ce CommandError) Error() string {
15+
return ce.err.Error()
16+
}
17+
18+
func (ce CommandError) Command() string {
19+
return ce.cmd
20+
}
21+
22+
func (ce CommandError) Unwrap() error {
23+
return ce.err
24+
}

0 commit comments

Comments
 (0)