cookoo

package module
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 6, 2014 License: MIT Imports: 6 Imported by: 480

README

Cookoo

A chain-of-command framework written in Go

Build Status GoDoc

Usage

$ cd $GOPATH
$ go get github.com/Masterminds/cookoo

Use it as follows:

package main

import (
	// This is the path to Cookoo
	"github.com/Masterminds/cookoo"
	"fmt"
)

func main() {

	// Build a new Cookoo app.
	registry, router, context := cookoo.Cookoo()

	// Fill the registry.
	registry.Route("TEST", "A test route").Does(HelloWorld, "hi") //...

	// Execute the route.
	router.HandleRequest("TEST", context, false)
}

func HelloWorld(cxt cookoo.Context, params *cookoo.Params) interface{} {
	fmt.Println("Hello World")
	return true
}

A Real Example

For a real example of Cookoo, take a look at Skunk.

Here's what Skunk's registry looks like:

	registry.
	Route("scaffold", "Scaffold a new app.").
		Does(LoadSettings, "settings").
			Using("file").WithDefault(homedir + "/settings.json").From("cxt:SettingsFile").
		Does(MakeDirectories, "dirs").
			Using("basedir").From("cxt:basedir").
			Using("directories").From("cxt:directories").
		Does(RenderTemplates, "template").
			Using("tpldir").From("cxt:homedir").
			Using("basedir").From("cxt:basedir").
			Using("templates").From("cxt:templates").
	Route("help", "Print help").
		Does(Usage, "Testing")

This has two routes:

  • scaffold
  • help

The help route just runs the command Usage, which looks like this:

func Usage(cxt cookoo.Context, params *cookoo.Params) interface{} {
	fmt.Println("Usage: skunk PROJECTNAME")
	return true
}

That is a good example of a basic command.

The scaffold route is more complex. It performs the following commands (in order):

  • LoadSettings: Load a settings.json file into the context.
  • MakeDirectories: Make a bunch of directories.
  • RenderTemplates: Perform template conversions on some files.

The MakeDirectories command is an example of a more complex command. It takes two parameters (declared with Using().From()):

  1. basedir: The base directory where the new subdirectories will be created. This comes from the cxt:basedir source, which means Cookoo looks in the Context object for a value named basedir.
  2. directoies: An array of directory names that this command will create. These come from cxt:directories, which means that the Context object is queried for the value of directories. In this case, that value is actually loaded from the settings.json file into the context by the LoadSettings command.`

With that in mind, let's look at the command:

// The MakeDirectories command.
// All commands take a Context and a Params object, and return an
// interface{}
func MakeDirectories(cxt cookoo.Context, params *cookoo.Params) interface{} {

	// This is how we get something out of the Params object. This is the
	// value that was passed in by `Using('basedir').From('cxt:basedir')
	basedir := params.Get("basedir", ".").(string)

	// This is another way to get a parameter value. This form allows us
	// to conveniently check that the parameter exists.
	d, ok := params.Has("directories")
	if !ok {
		// Did nothing. But we don't want to raise an error.
		return false
	}

	// We do have to do an explicit type conversion.
	directories := d.([]interface{})

	// Here we do the work of creating directories.
	for _, dir := range directories {
		dname := path.Join(basedir, dir.(string))
		os.MkdirAll(dname, 0755)
	}

	// We don't really have anything special to return, so we just
	// indicate that the command was successful.
	return true
}

This is a basic example of working with Cookoo. But far more sophisticated workflows can be built inexpensively and quickly, and in a style that encourages building small and re-usable chunks of code.

Documentation

Overview

Basic commands for Cookoo.

Package cookoo is a Chain-of-Command (CoCo) framework for writing applications.

A chain of command framework works as follows:

* A "route" is constructed as a chain of commands -- a series of single-purpose tasks that are run in sequence.

* An application is composed of one or more routes.

* Commands in a route communicate using a Context.

* An application Router is used to receive a route name and then execute the appropriate chain of commands.

To create a new Cookoo application, use cookoo.Cookoo(). This will configure and create a new registry, request router, and context. From there, use the Registry to build chains of commands, and then use the Router to execute chains of commands.

Example:

package main

import (
  //This is the path to Cookoo
  "github.com/Masterminds/cookoo/src/cookoo"
  "fmt"
)

func main() {
  // Build a new Cookoo app.
  registry, router, context := cookoo.Cookoo()

  // Fill the registry.
  registry.Route("TEST", "A test route").Does(HelloWorld, "hi") //...

  // Execute the route.
  router.HandleRequest("TEST", context, false)
}

func HelloWorld(cxt cookoo.Context, params *cookoo.Params) (interface{}, Interrupt) {
  fmt.Println("Hello World")
  return true, nil
}

Unlike other CoCo implementations (like Pronto.js or Fortissimo), Cookoo commands are just functions.

Interrupts:

There are four types of interrupts that you may wish to return: - FatalError: This will stop the route immediately. - RecoverableError: This will allow the route to continue moving. - Stop: This will stop the current request, but not as an error. - Reroute: This will stop executing the current route, and switch to executing another route.

To learn how to write Cookoo applications, you may wish to examine the small Skunk application: https://github.com/technosophos/skunk.

Index

Constants

View Source
const VERSION = "0.0.1"

VERSION provides the current version of Cookoo.

Variables

This section is empty.

Functions

func Cookoo

func Cookoo() (reg *Registry, router *Router, cxt Context)

Cookoo creates a new Cookoo app.

Types

type BasicRequestResolver

type BasicRequestResolver struct {
	// contains filtered or unexported fields
}

BasicRequestResolver is a basic resolver that assumes that the given request name *is* the route name.

func (*BasicRequestResolver) Init

func (r *BasicRequestResolver) Init(registry *Registry)

Init initializes the BasicRequestResolver.

func (*BasicRequestResolver) Resolve

func (r *BasicRequestResolver) Resolve(path string, cxt Context) (string, error)

Resolve returns the given path. This is a non-transforming resolver.

type Command

type Command func(cxt Context, params *Params) (interface{}, Interrupt)

Command executes a command and returns a result. A Cookoo app has a registry, which has zero or more routes. Each route executes a sequence of zero or more commands. A command is of this type.

type Context

type Context interface {
	// Add puts a name/value pair to the context.
	// DEPRECATED. This will be removed in Cookoo 2.0. Use
	// Put instead.
	Add(string, ContextValue)

	// Put inserts a name/value pair into the context.
	//
	// This is used to add data to a context. The context does nothing
	// to manage manipulation of context values. Values are placed in
	// as-is, and are retrieved as-is. Unless an implementor has
	// made a value immutable, context values are mutable.
	Put(string, ContextValue)

	// Given a name, get a value from the context.
	//
	// Get requires a default value (which may be nil).
	//
	// Example:
	// 	ip := cxt.Get("ip", "127.0.0.1").(string)
	//
	// Contrast this usage with that of cxt.Has(), which may be used for more
	// traditional field checking:
	//
	// Example:
	// 	ip, ok := cxt.Has("ip")
	// 	if !ok {
	// 		// do something error-ish
	// 	}
	// 	ipStr := ip.(string)
	//
	// The cxt.Get() call avoids the cumbersome check/type-assertion combo
	// that occurs with cxt.Has().
	Get(string, interface{}) ContextValue
	// Given a name, check if the key exists, and if it does return the value.
	Has(string) (ContextValue, bool)
	// Get a datasource by name.
	Datasource(string) Datasource
	// Get a map of all datasources.
	Datasources() map[string]Datasource
	// Check if a datasource exists, and return it if it does.
	HasDatasource(string) (Datasource, bool)
	// Add a datasource.
	AddDatasource(string, Datasource)
	// Remove a datasource from the context.
	RemoveDatasource(string)
	// Get the length of the context. This is the number of context values.
	// Datsources are not counted.
	Len() int
	// Make a shallow copy of the context.
	Copy() Context
	// Get the content (no datasources) as a map.
	AsMap() map[string]ContextValue
	// Get a logger.
	Logger(name string) (io.Writer, bool)
	// Add a logger.
	AddLogger(name string, logger io.Writer)
	// Remove a logger.
	RemoveLogger(name string)
	// Send a log with a prefix.
	Log(prefix string, v ...interface{})
	// Send a log and formatting string with a prefix.
	Logf(prefix string, format string, v ...interface{})
}

A Context is a collection of data that is associated with the current request.

Contexts are used to exchange information from command to command inside of a particular chain of commands (a route). Commands may access the data inside of a context, and may also modify a context.

A context maintains two different types of data: *context variables* and *datasources*.

Context variables are data that can be passed, in current form, from command to command -- analogous to passing variables via parameters in function calls.

Datasources are (as the name implies) sources of data. For example, a database, a file, a cache, and a key-value store are all datasources.

For long-running apps, it is generally assumed (though by no means required) that datasources are "long lived" and context variables are "short lived." While modifying a data source may impact other requests, generally it is safe to assume that modifying a variable is localized to the particular request.

Correct Usage: A Word of Warning

=================

The Cookoo system was designed around the theory that commands should generally work with datasources *directly* and context variables *indirectly*. Context variables should generally be passed into a command via a cookoo.Param. And a command generally should return a value that can then be placed into the context on its behalf.

The reason for this design is that it then makes it much easier for higher- level programming, such as changing input or modifying output at the registry level, not within the commands themselves.

Datasources, on the other hand, are designed to be leveraged primarily by commands. This involves a layer of conventionality, but it also pushes data access logic into the commands where it belongs.

So, for example, a SQL-based datasource should be *declared* at the top level of a program (where it will be added to the context), but the actual interaction with that datasource should happen inside of commands themselves, not at the registry level.

func NewContext

func NewContext() Context

NewContext creates a new empty cookoo.ExecutionContext and calls its Init() method.

func SyncContext added in v1.1.0

func SyncContext(cxt Context) Context

SyncContext wraps a context, syncronizing access to it.

This uses a read/write mutex which allows multiple reads at a time, but locks both reading and writing for writes.

To avoid really nefarious bugs, the same mutex locks context values and datasource values (since there is no guarantee that one is not backed by the other).

type ContextValue

type ContextValue interface{}

ContextValue is an empty interface defining a context value. Semantically, this is the same as interface{}

type Datasource

type Datasource interface{}

Datasource is an empty interface defining a Datasource. Semantically, this is the same as interface{}

type ExecutionContext

type ExecutionContext struct {
	// contains filtered or unexported fields
}

ExecutionContext is the core implementation of a Context.

An ExecutionContext is an unordered map-based context.

func (*ExecutionContext) Add

func (cxt *ExecutionContext) Add(name string, value ContextValue)

Add a name/value pair to the context. DEPRECATED: Use Put instead.

func (*ExecutionContext) AddDatasource

func (cxt *ExecutionContext) AddDatasource(name string, ds Datasource)

AddDatasource adds a datasource to the map of datasources. A datasource is typically something like a connection to a database that you want to keep open persistently and share between requests. To add a datasource to the map just add it with a name. e.g. cxt.AddDatasource("mysql", foo) where foo is the struct for the datasource.

func (*ExecutionContext) AddLogger

func (cxt *ExecutionContext) AddLogger(name string, logger io.Writer)

AddLogger adds a logger. The logging system can have one of more loggers keyed by name.

func (*ExecutionContext) AsMap

func (cxt *ExecutionContext) AsMap() map[string]ContextValue

AsMap returns the values of the context as a map keyed by a string.

func (*ExecutionContext) Copy

func (cxt *ExecutionContext) Copy() Context

Copy the context into a new context.

func (*ExecutionContext) Datasource

func (cxt *ExecutionContext) Datasource(name string) Datasource

Datasource get a datasource from the map of datasources. A datasource (e.g., a connection to a database) is retrieved as an interface so its type will need to be specified before it can be used. Take an example of the variable foo that is a struct of type Foo. foo = cxt.Datasource("foo").(*Foo)

func (*ExecutionContext) Datasources

func (cxt *ExecutionContext) Datasources() map[string]Datasource

Datasources gets the map of datasources.

func (*ExecutionContext) Get

func (cxt *ExecutionContext) Get(name string, defaultValue interface{}) ContextValue

Get retrieves a value from the context given a name. If a value does not exist on the context the default is returned.

func (*ExecutionContext) GetAll

func (cxt *ExecutionContext) GetAll() map[string]ContextValue

GetAll gets a map of all name/value pairs in the present context.

func (*ExecutionContext) Has

func (cxt *ExecutionContext) Has(name string) (value ContextValue, found bool)

Has is a special form of Get that also returns a flag indicating if the value is found. This fetches the value and also returns a flag indicating if the value was found. This is useful in cases where the value may legitimately be 0.

func (*ExecutionContext) HasDatasource

func (cxt *ExecutionContext) HasDatasource(name string) (Datasource, bool)

HasDatasource checks whether the named datasource exists, and return it if it does.

func (*ExecutionContext) Init

func (cxt *ExecutionContext) Init() *ExecutionContext

Init initializes a context.

If an existing context is re-initialized, all of its associated values, datasources, and loggers will be unset.

func (*ExecutionContext) Len

func (cxt *ExecutionContext) Len() int

Len returns the length of the context as in the length of the values stores.

func (*ExecutionContext) Log

func (cxt *ExecutionContext) Log(prefix string, v ...interface{})

Log logs a message to one of more loggers.

func (*ExecutionContext) Logf

func (cxt *ExecutionContext) Logf(prefix string, format string, v ...interface{})

Logf logs a message to one or more loggers and uses a format string.

func (*ExecutionContext) Logger

func (cxt *ExecutionContext) Logger(name string) (io.Writer, bool)

Logger gets a logger. The logging system can have one or more loggers that are stored keyed by name.

func (*ExecutionContext) Put

func (cxt *ExecutionContext) Put(name string, value ContextValue)

Put inserts a value into the context.

func (*ExecutionContext) RemoveDatasource

func (cxt *ExecutionContext) RemoveDatasource(name string)

RemoveDatasource removes a datasouce from the map of datasources.

func (*ExecutionContext) RemoveLogger

func (cxt *ExecutionContext) RemoveLogger(name string)

RemoveLogger removes a logger. The logging system can have one of more loggers keyed by name.

func (*ExecutionContext) SkipLogPrefix

func (cxt *ExecutionContext) SkipLogPrefix(prefixes ...string)

SkipLogPrefix ignores logging messages to any of the given prefixes.

While this is not a part of the Context interface, the ExecutionContext allows you to ignore certain logging prefixes. For example, to ignore the `debug` and `info` messages, you might want to do something like this:

cxt.(*ExecutionContext).SkipLogPrefix("debug", "info")
cxt.Logf("debug", "This message will be ignored.")

In the above case, the subsequent call to `Logf()` is ignored.

type FatalError

type FatalError struct {
	Message string
}

FatalError is a fatal error, which will stop the router from continuing a route.

func (*FatalError) Error

func (err *FatalError) Error() string

Error returns the error message.

type Interrupt

type Interrupt interface{}

Interrupt is a generic return for a command. Generally, a command should return one of the following in the interrupt slot: - A FatalError, which will stop processing. - A RecoverableError, which will continue the chain. - A Reroute, which will cause a different route to be run.

func AddToContext

func AddToContext(cxt Context, params *Params) (interface{}, Interrupt)

AddToContext adds all of the param name/value pairs into the context.

Params: - Any params will be added into the context.

func ForwardTo

func ForwardTo(cxt Context, params *Params) (interface{}, Interrupt)

ForwardTo forwards to the given route name.

To prevent possible loops or problematic re-routes, use ignoreRoutes.

Params: - route: The route to forward to. This is required. - ignoreRoutes: Route names that should be ignored (generate recoverable errors).

func LogMessage

func LogMessage(cxt Context, params *Params) (interface{}, Interrupt)

LogMessage prints a message to the log.

Params: - msg: The message to print - level: The log level (default: "info")

type KeyValueDatasource

type KeyValueDatasource interface {
	Value(key string) interface{}
}

KeyValueDatasource is a datasource that can retrieve values by (string) keys. Datsources can be just about anything. But a key/value datasource can be used for a special purpose. They can be accessed in From() clauses in a registry configuration.

type Params

type Params struct {
	// contains filtered or unexported fields
}

func NewParams

func NewParams(size int) *Params

func NewParamsWithValues

func NewParamsWithValues(initialVals map[string]interface{}) *Params

Create a new Params instance, initialized with the given map. Note that the given map is actually used (not copied).

func (*Params) AsMap

func (p *Params) AsMap() map[string]interface{}

Get all of the parameters.

This does no checking of the parameters.

func (*Params) Get

func (p *Params) Get(name string, defaultValue interface{}) interface{}

Get a parameter value, or return the default value.

func (*Params) Has

func (p *Params) Has(name string) (value interface{}, ok bool)

Check if a parameter exists, and return it if found.

func (*Params) Init

func (p *Params) Init(initialValues map[string]interface{})

func (*Params) Len

func (p *Params) Len() int

func (*Params) Requires

func (p *Params) Requires(paramNames ...string) (ok bool, missing []string)

Require that a given list of parameters are present. If they are all present, ok = true. Otherwise, ok = false and the `missing` array contains a list of missing params.

func (*Params) RequiresValue

func (p *Params) RequiresValue(paramNames ...string) (ok bool, missing []string)

Requires that given parameters are present and non-empty. This is more powerful than Requires(), which simply checks to see if the the Using() clause declared the value.

func (*Params) Validate

func (p *Params) Validate(name string, validator func(interface{}) bool) (value interface{}, ok bool)

Given a name and a validation function, return a valid value. If the value is not valid, ok = false.

type RecoverableError

type RecoverableError struct {
	Message string
}

RecoverableError is an error that should not cause the router to stop processing.

func (*RecoverableError) Error

func (err *RecoverableError) Error() string

Error returns the error message.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

A Registry contains the the callback routes and the commands each route executes.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns a new initialized registry.

func (*Registry) Does

func (r *Registry) Does(cmd Command, commandName string) *Registry

Does adds a command to the end of the chain of commands for the current (most recently specified) route.

func (*Registry) From

func (r *Registry) From(fromVal string) *Registry

From sepcifies where to get the value from for the most recently specified paramater as set by Using.

func (*Registry) Includes

func (r *Registry) Includes(route string) *Registry

Includes makes the commands from another route avaiable on this route.

func (*Registry) Init

func (r *Registry) Init() *Registry

Init initializes a registry. If a Registry is created through a means other than NewRegistry Init should be called on it.

func (*Registry) Route

func (r *Registry) Route(name, description string) *Registry

Route specifies a new route to add to the registry.

func (*Registry) RouteNames

func (r *Registry) RouteNames() []string

RouteNames gets a slice containing the names of every registered route.

The route names are returned in the order they were added to the registry. This is useful to some resolvers, which apply rules in order.

func (*Registry) RouteSpec

func (r *Registry) RouteSpec(routeName string) (spec *routeSpec, ok bool)

RouteSpec gets a ruote cased on its name.

func (*Registry) Routes

func (r *Registry) Routes() map[string]*routeSpec

Routes gets an unordered map of routes names to route specs.

If order is important, use RouteNames to get the names (in order).

func (*Registry) Using

func (r *Registry) Using(name string) *Registry

Using specifies a paramater to use for the most recently specified command as set by Does.

func (*Registry) WithDefault

func (r *Registry) WithDefault(value interface{}) *Registry

WithDefault specifies the default value for the most recently specified parameter as set by Using.

type RequestResolver

type RequestResolver interface {
	Init(registry *Registry)
	Resolve(path string, cxt Context) (string, error)
}

RequestResolver is the interface for the request resolver. A request resolver is responsible for transforming a request name to a route name. For example, a web-specific resolver may take a URI and return a route name. Or it make take an HTTP verb and return a route name.

type Reroute

type Reroute struct {
	Route string
}

Reroute is a command can return a Reroute to tell the router to execute a different route.

func (*Reroute) RouteTo

func (rr *Reroute) RouteTo() string

RouteTo returns the route to reroute to.

type RouteError

type RouteError struct {
	Message string
}

RouteError indicates that a route cannot be executed successfully.

func (*RouteError) Error

func (e *RouteError) Error() string

Error returns the error.

type Router

type Router struct {
	// contains filtered or unexported fields
}

Router is the Cookoo router. A Cookoo app works by passing a request into a router, and relying on the router to execute the appropriate chain of commands.

func NewRouter

func NewRouter(reg *Registry) *Router

NewRouter creates a new Router.

func (*Router) HandleRequest

func (r *Router) HandleRequest(name string, cxt Context, taint bool) error

HandleRequest does a request. This executes a request "named" name (this string is passed through the request resolver.) The context is cloned (shallow copy) and passed in as the base context.

If taint is `true`, then no routes that begin with `@` can be executed. Taint should be set to true on anything that relies on a name supplied by an external client.

This will do the following: - resolve the request name into a route name (using a RequestResolver) - look up the route - execute each command on the route in order

The following context variables are placed into the context during a run:

route.Name - Processed name of the current route
route.Description - Description of the current route
route.RequestName - raw route name as passed by the client
command.Name - current command name (changed with each command)

If an error occurred during processing, an error type is returned.

func (*Router) HasRoute

func (r *Router) HasRoute(name string) bool

HasRoute checks whether or not the route exists. Note that this does NOT resolve a request name into a route name. This expects a route name.

func (*Router) Init

func (r *Router) Init(registry *Registry) *Router

Init initializes the Router.

func (*Router) RequestResolver

func (r *Router) RequestResolver() RequestResolver

RequestResolver gets the request resolver.

func (*Router) ResolveRequest

func (r *Router) ResolveRequest(name string, cxt Context) (string, error)

ResolveRequest resolver a given string into a route name.

func (*Router) SetRegistry

func (r *Router) SetRegistry(reg *Registry)

SetRegistry sets the registry.

func (*Router) SetRequestResolver

func (r *Router) SetRequestResolver(resolver RequestResolver)

SetRequestResolver sets the request resolver. The resolver is responsible for taking an arbitrary string and resolving it to a registry route.

Example: Take a URI and translate it to a route.

type Stop

type Stop struct{}

Stop a route, but not as an error condition.

Directories

Path Synopsis
database
active
active package contains basic Active Record support.
active package contains basic Active Record support.
sql
SQL datasource and commands for Cookoo.
SQL datasource and commands for Cookoo.
web
Extra datasources for Web servers.
Extra datasources for Web servers.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL