Last active
May 3, 2016 20:02
-
-
Save tobert/014f3806704c24979fb9e368fbdf1544 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package hal | |
import ( | |
"regexp" | |
"time" | |
"github.com/netflix/hal-9001/hal" | |
) | |
/* hal.Cmd, hal.Param, hal.CmdInst, and hal.ParamInst are handles for hal's | |
* command parsing. While it's possible to use the standard library flags or an | |
* off-the-github command-line parser, they have proven to be clunky and often | |
* hacky to use. This API is purpose-built for building bot plugins, where folks | |
* expect a little more flexibility and the ability to use events as context. | |
* Rules (as they form)! | |
* 1. Cmd and Param are parsed in the order they were defined. | |
* 2a. "*" as user input means "whatever, from the current context" e.g. --room * | |
* 2b. "*" as a Cmd.Token means "anything and everything remaining in argv" | |
*/ | |
// common REs for Param.ValidRE | |
const IntRE = `^\d+$` | |
const FloatRE = `^(\d+|\d+.\d+)$` | |
const BoolRE = `(?i:^(true|false)$)` | |
const CmdRE = `^!\w+$` | |
const SubCmdRE = `^\w+$` | |
// Cmd models a tree of commands and subcommands along with their parameters. | |
// The tree will almost always be 1 or 2 levels deep. Deeper is possible but | |
// unlikely to be much higher, KISS. | |
// | |
// Key is the command name, e.g. | |
// "!uptime" => Cmd{"uptime"} | |
// "!mark ohai" => Cmd{Token: "mark", Cmds: []Cmd{Cmd{"*"}}} | |
// "!prefs set" => Cmd{Token: "prefs", MustSubCmd: true, Cmds: []Cmd{Cmd{"set"}}} | |
type Cmd struct { | |
Token string `json:"token"` // * => slurp everything remaining | |
Usage string `json:"usage"` | |
Params []*Param `json:"parameters"` | |
SubCmds []*Cmd `json:"subcommands"` | |
Prev *Cmd // parent command, nil for root | |
MustSubCmd bool // a subcommand is always required | |
} | |
// does anyone else feel weird writing ParamInsts? | |
type CmdInst struct { | |
Cmd *Cmd `json:"command"` | |
SubCmd *Cmd `json:"subcommand"` | |
ParamInsts []*ParamInst `json:"parameters"` | |
} | |
// TODO: consider whether it makes sense to generate typed params e.g. | |
// type IntParam struct { Default int } | |
// func (p *IntParamInst) Value() int { return p.value } | |
// There aren't many of them, they are unlikely change much, and the resulting | |
// code would get full type checking. | |
// Same for CmdInst. | |
// Param defines a parameter of a command. | |
type Param struct { | |
Key string `json:"key"` | |
Default string `json:"default"` | |
Usage string `json:"usage"` | |
Required bool `json:"required"` | |
ValidRE string `json:"validation_re2"` | |
validre *regexp.Regexp | |
} | |
type ParamInst struct { | |
Param *Param `json:"param"` | |
Found bool // was the parameter set? | |
Value string // provided value or the default | |
Cmd *Cmd // the command the parameter is attached to | |
} | |
func NewCmd(token string) *Cmd { | |
cmd := Cmd{ | |
Token: token, | |
Params: make([]*Param, 0), | |
SubCmds: make([]*Cmd, 0), | |
} | |
return &cmd | |
} | |
func (c *Cmd) subcmds() []*Cmd { | |
if c.SubCmds == nil { | |
c.SubCmds = make([]*Cmd, 0) | |
} | |
return c.SubCmds | |
} | |
func (c *Cmd) params() []*Param { | |
if c.Params == nil { | |
c.Params = make([]*Param, 0) | |
} | |
return c.Params | |
} | |
// chaining methods - see example() below | |
// TODO: these are stubs | |
func (c *Cmd) AddParam(key, def string, required bool) *Cmd { return c } | |
func (c *Cmd) AddAlias(name, alias string) *Cmd { return c } | |
func (c *Cmd) AddUsage(name, usage string) *Cmd { return c } | |
func (c *Cmd) AddPParam(position int, def string, required bool) *Cmd { return c } | |
func (c *Cmd) AddCmd(name string) *Cmd { return c } | |
// GetParam gets a parameter by its key. Returns nil for no match. | |
func (c *Cmd) GetParam(key string) *Param { | |
for _, p := range c.params() { | |
if p.Key == key { | |
return p | |
} | |
} | |
return nil | |
} | |
// GetSubCmd gets a subcommand by its token. Returns nil for no match. | |
func (c *Cmd) GetSubCmd(token string) *Cmd { | |
for _, s := range c.subcmds() { | |
if s.Token == token { | |
return s | |
} | |
} | |
return nil | |
} | |
// KeyParam adds "key" parameter with validation and can be chained. | |
// TODO: if there are going to be a few of these, maybe they should be generated. | |
func (c *Cmd) KeyParam(required bool) *Cmd { | |
p := Param{ | |
Key: "key", | |
ValidRE: "^key$", | |
Usage: "--key/-k the key string", | |
Required: required, | |
} | |
p.validre = regexp.MustCompile("^key$") | |
c.Params = append(c.Params, &p) | |
return c | |
} | |
// TODO: Add SetDefault(string), etc. if method chaining proves to be useful. | |
// TODO: these are stubs | |
func (c *Cmd) AddUserParam(def string, required bool) *Cmd { return c } | |
func (c *Cmd) AddRoomParam(def string, required bool) *Cmd { return c } | |
func (c *Cmd) AddBrokerParam(def string, required bool) *Cmd { return c } | |
func (c *Cmd) AddPluginParam(def string, required bool) *Cmd { return c } | |
func (c *Cmd) AddIdParam(def string, required bool) *Cmd { return c } | |
// parse a list of argv-style strings (0 is always the command name e.g. []string{"prefs"}) | |
func (c *Cmd) Process(argv []string) *CmdInst { return &CmdInst{} } | |
//prefs := pc.ProcessEvent(evt) | |
func (c *Cmd) ProcessEvent(evt *hal.Evt) *CmdInst { | |
argv := evt.BodyAsArgv() | |
ci := c.Process(argv) | |
// TODO: process "*" options and fill them in from evt | |
return ci | |
} | |
// SubCmdKey returns the subcommand's key string. Returns empty string | |
// if there is no subcommand. | |
func (c *CmdInst) SubCmdToken() string { | |
if c.SubCmd != nil { | |
return c.SubCmd.Token | |
} | |
return "" | |
} | |
// helpers to convert strings to commonly-used types | |
// TODO: these are stubs | |
func (p *ParamInst) String() string { return "" } | |
func (p *ParamInst) Int() int { return 0 } | |
func (p *ParamInst) Float() float64 { return 0.0 } | |
func (p *ParamInst) Bool() bool { return false } | |
// include almost-but-not-quite-precise intervals like day, week, and year | |
// TODO: this is a stub | |
func (p *ParamInst) Duration() time.Duration { return time.Second } | |
// handle a variety of formats automatically | |
// TODO: this is a stub | |
func (p *ParamInst) Time() time.Time { return time.Now() } | |
func example(evt hal.Evt) { | |
// example 1 | |
oc := Cmd{ | |
Token: "oncall", | |
MustSubCmd: true, | |
Usage: "search Pagerduty escalation policies for a string", | |
SubCmds: []*Cmd{ | |
NewCmd("cache-status"), | |
NewCmd("cache-interval").AddPParam(0, "1h", true), | |
NewCmd("*"), // everything else is a search string | |
}, | |
} | |
oc.GetSubCmd("cache-status").Usage = "check the status of the background caching job" | |
oc.GetSubCmd("cache-interval").Usage = "set the background caching job interval" | |
oc.GetSubCmd("*").Usage = "create a mark in time with an (optional) text note" | |
// hmm maybe we can abuse varargs a bit without ruining safety.... | |
// basically achieves a type-safe kwargs... | |
// NewCmd("*", Usage{"create a mark in time with an (optional) text note"}) | |
oci := oc.Process(evt.BodyAsArgv()) | |
switch oci.SubCmdToken() { | |
case "cache-status": | |
cacheStatus(&evt) | |
case "cache-interval": | |
cacheInterval(&evt, oci) | |
case "*": | |
search(&evt, oci) | |
} | |
// example 2 | |
// Alias: requiring explicit aliases instead of guessing seems right | |
// the following appear in a couple plugins so they'll probably have built | |
// in helpers for consistency, e.g. pc.Cmd("set").KeyParam(true).RoomParam(false) | |
pc := NewCmd("prefs") | |
pc.AddCmd("set"). | |
AddParam("key", "", true). | |
AddAlias("key", "k"). // vertically aligned for your viewing pleasure | |
AddParam("value", "", true). | |
AddAlias("value", "v"). | |
AddParam("room", "", false). | |
AddAlias("room", "r"). | |
AddUsage("room", "Set the room ID"). | |
AddParam("user", "", false). | |
AddAlias("user", "u"). | |
AddParam("broker", "", false). | |
AddAlias("broker", "b") | |
// consider doing evt.ProcessCmd(cmd) or something like that | |
// so this can go in its own package that isn't coupled to the rest of hal | |
prefs := pc.ProcessEvent(&evt) | |
switch prefs.SubCmdToken() { | |
case "set": | |
setPref(&evt, prefs) | |
} | |
} | |
// stubs for example | |
func cacheStatus(evt *hal.Evt) {} | |
func cacheInterval(evt *hal.Evt, oci *CmdInst) {} | |
func search(evt *hal.Evt, oci *CmdInst) {} | |
func setPref(evt *hal.Evt, oci *CmdInst) {} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment