Skip to content

Commit

Permalink
Add initial implementation (#1)
Browse files Browse the repository at this point in the history
This adds a naive implementation of a `Command`, `CommandFunc`, and
`CommandMux`.
  • Loading branch information
markuswustenberg authored Oct 3, 2024
1 parent 4ac94e7 commit 3a7ad34
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ go get maragu.dev/clir
```

Made with ✨sparkles✨ by [maragu](https://www.maragu.dev/).

Does your company depend on this project? [Contact me at markus@maragu.dk](mailto:markus@maragu.dk?Subject=Supporting%20your%20project) to discuss options for a one-time or recurring invoice to ensure its continued thriving.
1 change: 0 additions & 1 deletion clir.go

This file was deleted.

55 changes: 55 additions & 0 deletions command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Package clir provides a definition of a runnable [Command] as well as a [CommandMux], which is a multiplexer/router for commands.
package clir

import (
"context"
"io"
"os"
"os/signal"
"syscall"
)

// Context for a [Command] that runs.
type Context struct {
Args []string
Ctx context.Context
Err io.Writer
In io.Reader
Out io.Writer
}

// Command can be run with a Context.
type Command interface {
Run(ctx Context) error
}

// CommandFunc is a function which satisfies [Command].
type CommandFunc func(ctx Context) error

// Run satisfies [Command].
func (f CommandFunc) Run(ctx Context) error {
return f(ctx)
}

// Run a [Command] with default options, which are:
// - Get args from [os.Args]
// - Create context which is cancelled on [syscall.SIGTERM] or [syscall.SIGINT]
// - Use [os.Stdin] for input
// - Use [os.Stdout] for output
// - Use [os.Stderr] for errors
func Run(cmd Command) error {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer stop()

cmdCtx := Context{
Args: os.Args,
Ctx: ctx,
Err: os.Stderr,
In: os.Stdin,
Out: os.Stdout,
}

return cmd.Run(cmdCtx)
}

var _ Command = (*CommandFunc)(nil)
20 changes: 20 additions & 0 deletions command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package clir_test

import (
"strings"
"testing"

"maragu.dev/is"

"maragu.dev/clir"
)

func TestRun(t *testing.T) {
t.Run("can run a command", func(t *testing.T) {
err := clir.Run(clir.CommandFunc(func(ctx clir.Context) error {
is.True(t, strings.Contains(ctx.Args[0], "clir.test"))
return nil
}))
is.NotError(t, err)
})
}
11 changes: 11 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package clir

type Error string

func (e Error) Error() string {
return string(e)
}

const (
ErrorNotFound = Error("not found")
)
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module maragu.dev/clir

go 1.23

require maragu.dev/is v0.2.0
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
maragu.dev/is v0.2.0 h1:poeuVEA5GG3vrDpGmzo2KjWtIMZmqUyvGnOB0/pemig=
maragu.dev/is v0.2.0/go.mod h1:bviaM5S0fBshCw7wuumFGTju/izopZ/Yvq4g7Klc7y8=
44 changes: 44 additions & 0 deletions mux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package clir

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

func NewCommandMux() *CommandMux {
return &CommandMux{
commands: map[string]Command{},
}
}

func (c *CommandMux) Run(ctx Context) error {
if len(ctx.Args) == 0 {
root, ok := c.commands[""]
if !ok {
return ErrorNotFound
}
return root.Run(ctx)
}

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

return ErrorNotFound
}

func (c *CommandMux) Handle(pattern string, cmd Command) {
c.patterns = append(c.patterns, pattern)
c.commands[pattern] = cmd
}

func (c *CommandMux) HandleFunc(pattern string, cmd CommandFunc) {
c.Handle(pattern, cmd)
}

var _ Command = (*CommandMux)(nil)
62 changes: 62 additions & 0 deletions mux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package clir_test

import (
"testing"

"maragu.dev/is"

"maragu.dev/clir"
)

func TestRoute(t *testing.T) {
t.Run("can handle a root command", func(t *testing.T) {
mux := clir.NewCommandMux()

var called bool
mux.HandleFunc("", func(ctx clir.Context) error {
called = true
return nil
})

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

t.Run("errors if there is no root command", func(t *testing.T) {
mux := clir.NewCommandMux()

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

t.Run("can handle a subcommand", func(t *testing.T) {
mux := clir.NewCommandMux()

var called bool
mux.HandleFunc("party", func(ctx clir.Context) error {
called = true
is.Equal(t, 0, len(ctx.Args))
return nil
})

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

t.Run("errors if there is no subcommand", func(t *testing.T) {
mux := clir.NewCommandMux()

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

0 comments on commit 3a7ad34

Please sign in to comment.