Skip to content

Commit

Permalink
Rename Route to Sub and add Use
Browse files Browse the repository at this point in the history
  • Loading branch information
markuswustenberg committed Oct 8, 2024
1 parent 92548e9 commit 328bebb
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 19 deletions.
4 changes: 2 additions & 2 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (c Context) Errorfln(format string, a ...any) {
_, _ = fmt.Fprintf(c.Err, format+"\n", a...)
}

// Command can be run with a Context.
// Command can be run with a [Context].
type Command interface {
Run(ctx Context) error
}
Expand All @@ -60,7 +60,7 @@ func Run(cmd Command) {
defer stop()

cmdCtx := Context{
Args: os.Args,
Args: os.Args[1:],
Ctx: ctx,
Err: os.Stderr,
In: os.Stdin,
Expand Down
2 changes: 0 additions & 2 deletions command_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package clir_test

import (
"strings"
"testing"

"maragu.dev/is"
Expand All @@ -14,7 +13,6 @@ func TestRun(t *testing.T) {
var called bool
clir.Run(clir.CommandFunc(func(ctx clir.Context) error {
called = true
is.True(t, strings.Contains(ctx.Args[0], "clir.test"))
return nil
}))
is.True(t, called)
Expand Down
42 changes: 35 additions & 7 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package clir

// CommandRouter is a router for commands which itself satisfies [Command].
type CommandRouter struct {
patterns []string
commands map[string]Command
commands map[string]Command
middlewares []Middleware
patterns []string
}

func NewCommandRouter() *CommandRouter {
Expand All @@ -15,31 +16,58 @@ func NewCommandRouter() *CommandRouter {
// Run satisfies [Command].
func (c *CommandRouter) Run(ctx Context) error {
if len(ctx.Args) == 0 {
root, ok := c.commands[""]
cmd, ok := c.commands[""]
if !ok {
return ErrorNotFound
}
return root.Run(ctx)

// Apply middlewares in reverse order, so that middlewares are applied in the order they were added.
for i := len(c.middlewares) - 1; i >= 0; i-- {
cmd = c.middlewares[i](cmd)
}

return cmd.Run(ctx)
}

for _, pattern := range c.patterns {
if ctx.Args[0] == pattern {
cmd := c.commands[pattern]
ctx.Args = ctx.Args[1:]

for i := len(c.middlewares) - 1; i >= 0; i-- {
cmd = c.middlewares[i](cmd)
}

return cmd.Run(ctx)
}
}

return ErrorNotFound
}

func (c *CommandRouter) Route(pattern string, cmd Command) {
// Sub adds a subcommand to the router with the given pattern.
func (c *CommandRouter) Sub(pattern string, cmd Command) {
c.patterns = append(c.patterns, pattern)
c.commands[pattern] = cmd
}

func (c *CommandRouter) RouteFunc(pattern string, cmd CommandFunc) {
c.Route(pattern, cmd)
// SubFunc is a convenience method for adding a subcommand with a [CommandFunc].
// It's the same as calling [CommandRouter.Sub] with a [CommandFunc], but you don't have to wrap the function.
func (c *CommandRouter) SubFunc(pattern string, cmd CommandFunc) {
c.Sub(pattern, cmd)
}

// Group commands with a new middleware stack.
func (c *CommandRouter) Group(cb func(r *CommandRouter)) {
// TODO
}

type Middleware = func(next Command) Command

// Use a middleware for all commands. If called on the root router, it will apply to all commands.
// If called in a Group, it will apply to all commands in that group.
func (c *CommandRouter) Use(middlewares ...Middleware) {
c.middlewares = append(c.middlewares, middlewares...)
}

var _ Command = (*CommandRouter)(nil)
57 changes: 49 additions & 8 deletions router_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package clir_test

import (
"strings"
"testing"

"maragu.dev/is"
Expand All @@ -13,32 +14,28 @@ func TestCommandRouter_Run(t *testing.T) {
r := clir.NewCommandRouter()

var called bool
r.RouteFunc("", func(ctx clir.Context) error {
r.SubFunc("", func(ctx clir.Context) error {
called = true
return nil
})

err := r.Run(clir.Context{
Args: []string{},
})
err := r.Run(clir.Context{})
is.NotError(t, err)
is.True(t, called)
})

t.Run("errors on run if there is no root command", func(t *testing.T) {
r := clir.NewCommandRouter()

err := r.Run(clir.Context{
Args: []string{},
})
err := r.Run(clir.Context{})
is.Error(t, err, clir.ErrorNotFound)
})

t.Run("can route and run a subcommand", func(t *testing.T) {
r := clir.NewCommandRouter()

var called bool
r.RouteFunc("party", func(ctx clir.Context) error {
r.SubFunc("party", func(ctx clir.Context) error {
called = true
is.Equal(t, 0, len(ctx.Args))
return nil
Expand All @@ -59,4 +56,48 @@ func TestCommandRouter_Run(t *testing.T) {
})
is.Error(t, err, clir.ErrorNotFound)
})

t.Run("can use middlewares on root and subcommands", func(t *testing.T) {
r := clir.NewCommandRouter()

m1 := newMiddleware(t, "m1")
m2 := newMiddleware(t, "m2")

r.Use(m1, m2)

r.SubFunc("", func(ctx clir.Context) error {
ctx.Println("root")
return nil
})

r.SubFunc("party", func(ctx clir.Context) error {
ctx.Println("party")
return nil
})

var b strings.Builder
err := r.Run(clir.Context{
Out: &b,
})
is.NotError(t, err)
is.Equal(t, "m1\nm2\nroot\n", b.String())

b.Reset()

err = r.Run(clir.Context{
Args: []string{"party"},
Out: &b,
})
is.NotError(t, err)
is.Equal(t, "m1\nm2\nparty\n", b.String())
})
}

func newMiddleware(t *testing.T, name string) clir.Middleware {
return func(next clir.Command) clir.Command {
return clir.CommandFunc(func(ctx clir.Context) error {
ctx.Println(name)
return next.Run(ctx)
})
}
}

0 comments on commit 328bebb

Please sign in to comment.