Introducing Cookoo, A Unique Controller and Lightweight Framework for Go

When Matt Butcher introduced me to the chain-of-command pattern for a controller, some years ago, I was intrigued. It provided a unique way to re-use code in a controller while enabling things to link together in a manner that reminded me of tools like Yahoo Pipes.

When we learned how useful the Go programming language was and how productive we could be with it, we decided to create a chain-of-command framework for Go. That framework is cookoo and it just had its first major release.

I find the easiest way to get into something new like this is to take a look at some code.

package main

import (
    "github.com/Masterminds/cookoo"
    "github.com/Masterminds/cookoo/web"
)

func main() {
    registry, router, context := cookoo.Cookoo()

    registry.Route("GET /", "The Homepage").
        Does(MyMessage, "msg").
        Does(web.Flush, "out").
        Using("contentType").WithDefault("text/plain").
        Using("content").From("cxt:msg")

    web.Serve(registry, router, context)
}

This web example is more complicated than it needs to be in order to show how chains work. For the route "GET /", the commands MyMessage and web.Flush will happen in sequence.

First MyMessage executes when the route is matched. Cookoo adds the return value from MyMessage to the context with the key of "msg". Then web.Flush executes. The Params of "contentType" and "content" will be passed into web.Flush. A default value is passed into "contentType". For "content", the value on the context in the key "msg" is passed in.

The registry contains the mapping of commands, including their inputs and outputs, to route. Routes can be console commands, URL paths, and so one. The router does the matching between the input to the application and the routes. The context is passed along the chain and is available in each command. It provides easy access to logging, data sources that are long lived and shared between requests, and so on.

Let’s take a look at an example command.

// MyMessage builds a simple text message and put it in the context.
func MyMessage(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {
    tpl := "Route %s executed on %s"
    now := time.Now().Format("Jan 2, 2006 at 3:04pm (MST)")
    name := c.Get("route.Name", "unknown").(string)
    msg := fmt.Sprintf(tpl, name, now)

    return msg, nil
}

The shared Context and the Params for this command are passed in and it expects a return value and an Interrupt. The Interrupt is used for errors, changing routes, and so on.

These are the base building blocks for building something with cookoo. There is a lot more you can do. If you’re interested in learning more, checkout the documentation or tutorials.