Yet another TODO app! With Redis and Go [Part 2]
Tags: redis, showdev, go, tutorial
Published: 2020-04-09 12:29:03

This is the second (and final) part of a blog series which covers some of the Redis data structures with the help of a simple yet practical **todo app** 😉 built with [`Go`](https://golang.org/) [`cobra`](https://github.com/spf13/cobra) (a popular library for CLI apps) and `Redis` as the backend store.

[Part 1](https://dev.to/itnext/yet-another-todo-app-with-redis-and-go-part-1-1dce) covered the overview, setup and the process of trying out the `todo` app. In this part, we will peek under the hood and walk through the  code

> The code is [available on Github](https://github.com/abhirockzz/redis-todo-cli-app)

Before we dive in, here is a refresher of what you can do with the `todo` CLI - good old `CRUD`! From `todo --help`:

```
Yet another TODO app. Uses Go and Redis

Usage:
  todo [command]

Available Commands:
  create      create a todo with description
  delete      delete a todo
  help        Help about any command
  list        list all todos
  update      update todo description, status or both

Flags:
  -h, --help      help for todo
  -v, --version   version for todo

Use "todo [command] --help" for more information about a command.
```

[`Cobra`](https://github.com/spf13/cobra) is used as the CLI framework. Its a popular project and powers CLI tools such as `docker`, `kubectl` etc.

This is what the code structure looks like:

```
.
├── cmd
│   ├── create.go
│   ├── delete.go
│   ├── list.go
│   ├── root.go
│   └── update.go
├── db
│   └── todo-redis.go
├── go.mod
├── go.sum
└── main.go
```

Each `todo` operation (`create`, `list` etc.) is governed by a `cobra` command. The `cmd` pacakge contains the implementations e.g. `create.go`, `list.go` etc.

## The `root` command

In `cobra`, the root command is the parent command which is at the top-most level. Other sub-commands can be added to it. In this case, `todo` is the root command and `create`, `list` etc. are sub-commands (also, each command can have one or more flags which can be passed during invocation)

The `todo` root command is defined in `cmd/root.go`

```
var rootCmd = &cobra.Command{Use: "todo", Short: "manage your todos", Version: "0.1.0"}
```

Note that it does not have any specific flags - it just has sub-commands. Let's look at them

## Sub-commands - `create`, `list`, `update`, `delete`

The sub-command implementation logic follows the same pattern:

- use an `init()` function to bootstrap the command - set its flags, mark mandatory flag (if needed) and add it to the root command
- define the execution logic/function - in our case, it's the `Redis` related operations (we will dive into them soon)

Here is what the `todo create` implementation looks like (in `cmd/create.go`)

```
...
var createCmd = &cobra.Command{Use: "create", Short: "create a todo with description", Run: Create}

func init() {
	createCmd.Flags().String("description", "", "create todo with description")
	createCmd.MarkFlagRequired("description")
	rootCmd.AddCommand(createCmd)
}

// Create - todo create --description <text>
func Create(cmd *cobra.Command, args []string) {
	desc := cmd.Flag("description").Value.String()
	db.CreateTodo(desc)
}
...
```

`todo create` needs a mandatory value for `description` and uses the `func Create(cmd *cobra.Command, args []string)` to perform the actual `todo` creation. It invokes `db.CreateTodo(desc)` for Redis related operations required to save `todo` info in Redis.

Here is a snippet for `todo update` implementation (in `cmd/update.go`)

```
...
var updateCmd = &cobra.Command{Use: "update", Short: "update todo description, status or both", Run: Update}

func init() {
	updateCmd.Flags().String("id", "", "id of the todo you want to update")
	updateCmd.MarkFlagRequired("id")
	updateCmd.Flags().String("description", "", "new description")
	updateCmd.Flags().String("status", "", "new status: completed, pending, in-progress")

	rootCmd.AddCommand(updateCmd)
}

// Update - todo update --id <id> --status <new status> --description <new description>
func Update(cmd *cobra.Command, args []string) {
	id := cmd.Flag("id").Value.String()
	desc := cmd.Flag("description").Value.String()

	status := cmd.Flag("status").Value.String()

	if desc == "" && status == "" {
		log.Fatalf("either description or status is required")
	}

	if status == "completed" || status == "pending" || status == "in-progress" || status == "" {
		db.UpdateTodo(id, desc, status)
	} else {
		log.Fatalf("provide valid status - completed, pending or in-progress")
	}
}
```

`todo update` needs a mandatory `--id` flag and either a `status` or `description` (can be both) need to be provided. It further invokes `db.UpdateTodo` for Redis related operations to update the `todo` info passed in via CLI

### Redis

Now, let's explore the meat of the app - Redis related logic to manage `todo` info. Redis operations have been centralized in one single place i.e. the `todo-redis.go` in the `db` package. It has four functions, each of which maps to the respective `todo` sub-commands:

- `CreateTodo`
- `ListTodos`
- `UpdateTodo`
- `DeleteTodo`

I have used the [`go-redis`](https://github.com/go-redis/redis)

> [`redigo`](https://github.com/gomodule/redigo) is another popular client

All the commands start off by connecting to `Redis` (obviously!)

```
	c := redis.NewClient(&redis.Options{Addr: redisHost})
	err := c.Ping().Err()
	if err != nil {
		log.Fatal("redis connect failed", err)
	}
	defer c.Close()
```

This is a single user `cli` app, not a long running server component. So we can connect and disconnect after individual operations rather than handling Redis client `redis.Client` at a global level. Now we will look at relevant snippets of each of the operations

**CreateTodo**

To create a `todo` in Redis, we use a counter to serve as the todo `id`. Redis allows you to use `String`s as atomic counters using `INCR` (and related commands)

```
id, err := c.Incr(todoIDCounter).Result()
```

> check `INCR` command for reference https://redis.io/commands/incr

This incremented ID (prepended with `todo:`) in a Redis `SET`. A Redis `Set` is an unordered collection of `Strings` and does not allow duplicates. This `id` forms the name of the `HASH` (next step) in which we will store the `todo` details e.g. for `todo` with id 42, the details will be stored in a `HASH` named `todo:42`. This makes it easy to `list`, `update` and `delete` todos

```
err = c.SAdd(todoIDsSet, todoid).Err()
```

> check `SADD` for details https://redis.io/commands/sadd

Finally, the `todo` info (`id`, `description`, `status`) is stored in a `HASH`. Redis `Hash`es are maps between string fields and string values. This makes them suitable for storing object representations such as `todo` info in this case

```
todo := map[string]interface{}{"desc": desc, "status": statusPending}
err = c.HMSet(todoid, todo).Err()
```

> check `HMSET` for refernece https://redis.io/commands/hmset

**ListTodos**

To get todos, we just fetch all the members in the set i.e. `todo:1`, `todo:2` etc.

```
todoHashNames, err := c.SMembers(todoIDsSet).Result()
```

> check `SMEMBERS` for reference https://redis.io/commands/smembers

We loop through these and search each `HASH`, extract the info (`id`, `description`, `status`), create a slice of `db.Todo`s which is finally presented in a tabular format in the CLI app

```
for _, todoHashName := range todoHashNames {
	id := strings.Split(todoHashName, ":")[1]
	todoMap, err := c.HGetAll(todoHashName).Result()
    ....
    todo = Todo{id, todoMap["desc"], todoMap["status"]}
    ....
    todos = append(todos, todo)
....
```

> check `HGETALL` for refernece https://redis.io/commands/hgetall

**DeleteTodo**

To delete a todo, the `HASH` containing the info is deleted

```
c.Del("todo:" + id).Result()
```

> check `DEL` for refernece https://redis.io/commands/del

This is followed by removal of the `todo` id entry from the `SET`

```
err = c.SRem(todoIDsSet, "todo:"+id).Err()
```

> check `SREM` for refernece https://redis.io/commands/srem

**UpdateTodo**

In order to update a `todo` by id, we first need to confirm whether its a valid `id`. For this, all we need to is check whether the `SET` contains that `todo`

```
exists, err := c.SIsMember(todoIDsSet, "todo:"+id).Result()
```

If it does, we can proceed to update its info. We create a `map` with the new `status`, `description` (or both) as passed in by the user and invoke `HMSet` function

```
    ....
	updatedTodo := map[string]interface{}{}
	if status != "" {
		updatedTodo["status"] = status
	}

	if desc != "" {
		updatedTodo["desc"] = desc
	}
    c.HMSet("todo:"+id, updatedTodo).Err()
    ....
```

> check `HMSET` for refernece https://redis.io/commands/hmset

This concludes the two-part blog series. As always, stay tuned for more! If found this useful, don't forget to like and share 😃 Happy to get your feedback via [Twitter](https://twitter.com/abhi_tweeter) or just drop a comment 🙏🏻