diff --git a/cmd/bosun/conf/conf.go b/cmd/bosun/conf/conf.go index 92987e1d25..f0f04c2e1c 100644 --- a/cmd/bosun/conf/conf.go +++ b/cmd/bosun/conf/conf.go @@ -295,6 +295,35 @@ func (ns *Notifications) Get(c *Conf, tags opentsdb.TagSet) map[string]*Notifica return nots } +// GetNotificationChains returns the warn or crit notification chains for a configured +// alert. Each chain is a list of notification names. If a notification name +// as already been seen in the chain it ends the list with the notification +// name with a of "..." which indicates that the chain will loop. +func GetNotificationChains(c *Conf, n map[string]*Notification) [][]string { + chains := [][]string{} + for _, root := range n { + chain := []string{} + seen := make(map[string]bool) + var walkChain func(next *Notification) + walkChain = func(next *Notification) { + if next == nil { + chains = append(chains, chain) + return + } + if seen[next.Name] { + chain = append(chain, fmt.Sprintf("...%v", next.Name)) + chains = append(chains, chain) + return + } + chain = append(chain, next.Name) + seen[next.Name] = true + walkChain(next.Next) + } + walkChain(root) + } + return chains +} + // parseNotifications parses the comma-separated string v for notifications and // returns them. func (c *Conf) parseNotifications(v string) (map[string]*Notification, error) { @@ -340,10 +369,6 @@ type Notification struct { body string } -func (n *Notification) MarshalJSON() ([]byte, error) { - return nil, fmt.Errorf("conf: cannot json marshal notifications") -} - type Vars map[string]string func ParseFile(fname string) (*Conf, error) { diff --git a/cmd/bosun/sched/filter.go b/cmd/bosun/sched/filter.go deleted file mode 100644 index c53b7da088..0000000000 --- a/cmd/bosun/sched/filter.go +++ /dev/null @@ -1,112 +0,0 @@ -package sched - -import ( - "fmt" - "strings" - - "bosun.org/cmd/bosun/conf" - "bosun.org/models" -) - -func makeFilter(filter string) (func(*conf.Conf, *conf.Alert, *models.IncidentState) bool, error) { - fields := strings.Fields(filter) - if len(fields) == 0 { - return func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool { - return true - }, nil - } - fs := make(map[string][]func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool) - for _, f := range fields { - negate := strings.HasPrefix(f, "!") - if negate { - f = f[1:] - } - if f == "" { - return nil, fmt.Errorf("filter required") - } - sp := strings.SplitN(f, ":", 2) - value := sp[len(sp)-1] - key := sp[0] - if len(sp) == 1 { - key = "" - } - add := func(fn func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool) { - fs[key] = append(fs[key], func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool { - v := fn(c, a, s) - if negate { - v = !v - } - return v - }) - } - switch key { - case "": - add(func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool { - ak := s.AlertKey - return strings.Contains(string(ak), value) || strings.Contains(string(s.Subject), value) - }) - case "ack": - var v bool - switch value { - case "true": - v = true - case "false": - v = false - default: - return nil, fmt.Errorf("unknown %s value: %s", key, value) - } - add(func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool { - return s.NeedAck != v - }) - case "notify": - add(func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool { - r := false - f := func(ns *conf.Notifications) { - for k := range ns.Get(c, s.AlertKey.Group()) { - if strings.Contains(k, value) { - r = true - break - } - } - } - f(a.CritNotification) - f(a.WarnNotification) - return r - }) - case "status": - var v models.Status - switch value { - case "normal": - v = models.StNormal - case "warning": - v = models.StWarning - case "critical": - v = models.StCritical - case "unknown": - v = models.StUnknown - default: - return nil, fmt.Errorf("unknown %s value: %s", key, value) - } - add(func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool { - return s.LastAbnormalStatus == v - }) - default: - return nil, fmt.Errorf("unknown filter key: %s", key) - } - } - return func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool { - for _, ors := range fs { - match := false - for _, f := range ors { - if f(c, a, s) { - match = true - break - } - } - if !match { - return false - } - } - return true - }, nil -} diff --git a/cmd/bosun/sched/sched.go b/cmd/bosun/sched/sched.go index c0bdf85eab..9b2b14044a 100644 --- a/cmd/bosun/sched/sched.go +++ b/cmd/bosun/sched/sched.go @@ -22,6 +22,7 @@ import ( "github.com/MiniProfiler/go/miniprofiler" "github.com/boltdb/bolt" "github.com/bradfitz/slice" + "github.com/kylebrandt/boolq" "github.com/tatsushid/go-fastping" ) @@ -372,12 +373,13 @@ func (s *Schedule) MarshalGroups(T miniprofiler.Timer, filter string) (*StateGro } t.FailingAlerts, t.UnclosedErrors = s.getErrorCounts() T.Step("Setup", func(miniprofiler.Timer) { - matches, err2 := makeFilter(filter) + status2, err2 := s.GetOpenStates() if err2 != nil { err = err2 return } - status2, err2 := s.GetOpenStates() + var parsedExpr *boolq.Tree + parsedExpr, err2 = boolq.Parse(filter) if err2 != nil { err = err2 return @@ -391,7 +393,14 @@ func (s *Schedule) MarshalGroups(T miniprofiler.Timer, filter string) (*StateGro } continue } - if matches(s.Conf, a, v) { + is := MakeIncidentSummary(s.Conf, silenced, v) + match := false + match, err2 = boolq.AskParsedExpr(parsedExpr, is) + if err2 != nil { + err = err2 + return + } + if match { status[k] = v } } diff --git a/cmd/bosun/sched/views.go b/cmd/bosun/sched/views.go new file mode 100644 index 0000000000..74ea30a566 --- /dev/null +++ b/cmd/bosun/sched/views.go @@ -0,0 +1,238 @@ +package sched + +import ( + "fmt" + "strings" + "time" + + "bosun.org/cmd/bosun/conf" + "bosun.org/models" + "bosun.org/opentsdb" + "github.com/ryanuber/go-glob" +) + +// Views + +type EventSummary struct { + Status models.Status + Time int64 +} + +// EventSummary is like a models.Event but strips the Results and Unevaluated +func MakeEventSummary(e models.Event) (EventSummary, bool) { + return EventSummary{ + Status: e.Status, + Time: e.Time.Unix(), + }, e.Unevaluated +} + +type EpochAction struct { + User string + Message string + Time int64 + Type models.ActionType +} + +func MakeEpochAction(a models.Action) EpochAction { + return EpochAction{ + User: a.User, + Message: a.Message, + Time: a.Time.UTC().Unix(), + Type: a.Type, + } +} + +type IncidentSummaryView struct { + Id int64 + Subject string + Start int64 + AlertName string + Tags opentsdb.TagSet + TagsString string + CurrentStatus models.Status + WorstStatus models.Status + LastAbnormalStatus models.Status + LastAbnormalTime int64 + Unevaluated bool + NeedAck bool + Silenced bool + Actions []EpochAction + Events []EventSummary + WarnNotificationChains [][]string + CritNotificationChains [][]string +} + +func MakeIncidentSummary(c *conf.Conf, s SilenceTester, is *models.IncidentState) IncidentSummaryView { + warnNotifications := c.Alerts[is.AlertKey.Name()].WarnNotification.Get(c, is.AlertKey.Group()) + critNotifications := c.Alerts[is.AlertKey.Name()].CritNotification.Get(c, is.AlertKey.Group()) + eventSummaries := []EventSummary{} + for _, event := range is.Events { + if eventSummary, unevaluated := MakeEventSummary(event); !unevaluated { + eventSummaries = append(eventSummaries, eventSummary) + } + } + actions := make([]EpochAction, len(is.Actions)) + for i, action := range is.Actions { + actions[i] = MakeEpochAction(action) + } + return IncidentSummaryView{ + Id: is.Id, + Subject: is.Subject, + Start: is.Start.Unix(), + AlertName: is.AlertKey.Name(), + Tags: is.AlertKey.Group(), + TagsString: is.AlertKey.Group().String(), + CurrentStatus: is.CurrentStatus, + WorstStatus: is.WorstStatus, + LastAbnormalStatus: is.LastAbnormalStatus, + LastAbnormalTime: is.LastAbnormalTime, + Unevaluated: is.Unevaluated, + NeedAck: is.NeedAck, + Silenced: s(is.AlertKey) != nil, + Actions: actions, + Events: eventSummaries, + WarnNotificationChains: conf.GetNotificationChains(c, warnNotifications), + CritNotificationChains: conf.GetNotificationChains(c, critNotifications), + } +} + +func (is IncidentSummaryView) Ask(filter string) (bool, error) { + sp := strings.SplitN(filter, ":", 2) + if len(sp) != 2 { + return false, fmt.Errorf("bad filter, filter must be in k:v format, got %v", filter) + } + key := sp[0] + value := sp[1] + switch key { + case "ack": + switch value { + case "true": + return is.NeedAck == false, nil + case "false": + return is.NeedAck == true, nil + default: + return false, fmt.Errorf("unknown %s value: %s", key, value) + } + case "hasTag": + if strings.Contains(value, "=") { + if strings.HasPrefix(value, "=") { + q := strings.TrimPrefix(value, "=") + for _, v := range is.Tags { + if glob.Glob(q, v) { + return true, nil + } + } + return false, nil + } + if strings.HasSuffix(value, "=") { + q := strings.TrimSuffix(value, "=") + _, ok := is.Tags[q] + return ok, nil + } + sp := strings.Split(value, "=") + if len(sp) != 2 { + return false, fmt.Errorf("unexpected tag specification: %v", value) + } + for k, v := range is.Tags { + if k == sp[0] && glob.Glob(sp[1], v) { + return true, nil + } + } + return false, nil + } + q := strings.TrimRight(value, "=") + _, ok := is.Tags[q] + return ok, nil + case "hidden": + hide := is.Silenced || is.Unevaluated + switch value { + case "true": + return hide == true, nil + case "false": + return hide == false, nil + default: + return false, fmt.Errorf("unknown %s value: %s", key, value) + } + case "name": + return glob.Glob(value, is.AlertName), nil + case "user": + for _, action := range is.Actions { + if action.User == value { + return true, nil + } + } + return false, nil + case "notify": + for _, chain := range is.WarnNotificationChains { + for _, wn := range chain { + if glob.Glob(value, wn) { + return true, nil + } + } + } + for _, chain := range is.CritNotificationChains { + for _, cn := range chain { + if glob.Glob(value, cn) { + return true, nil + } + } + } + return false, nil + case "silenced": + switch value { + case "true": + return is.Silenced == true, nil + case "false": + return is.Silenced == false, nil + default: + return false, fmt.Errorf("unknown %s value: %s", key, value) + } + case "start": + var op string + val := value + if strings.HasPrefix(value, "<") { + op = "<" + val = strings.TrimLeft(value, op) + } + if strings.HasPrefix(value, ">") { + op = ">" + val = strings.TrimLeft(value, op) + } + d, err := opentsdb.ParseDuration(val) + if err != nil { + return false, err + } + startTime := time.Unix(is.Start, 0) + // might want to make Now a property of incident summary for viewing things in the past + // but not going there at the moment. This is because right now I'm working with open + // incidents. And "What did incidents look like at this time?" is a different question + // since those incidents will no longer be open. + relativeTime := time.Now().UTC().Add(time.Duration(-d)) + switch op { + case ">", "": + return startTime.After(relativeTime), nil + case "<": + return startTime.Before(relativeTime), nil + default: + return false, fmt.Errorf("unexpected op: %v", op) + } + case "unevaluated": + switch value { + case "true": + return is.Unevaluated == true, nil + case "false": + return is.Unevaluated == false, nil + default: + return false, fmt.Errorf("unknown %s value: %s", key, value) + } + case "status": // CurrentStatus + return is.CurrentStatus.String() == value, nil + case "worstStatus": + return is.WorstStatus.String() == value, nil + case "lastAbnormalStatus": + return is.LastAbnormalStatus.String() == value, nil + case "subject": + return glob.Glob(value, is.Subject), nil + } + return false, nil +} diff --git a/cmd/bosun/web/incident.go b/cmd/bosun/web/incident.go new file mode 100644 index 0000000000..d774e4ac57 --- /dev/null +++ b/cmd/bosun/web/incident.go @@ -0,0 +1,41 @@ +package web + +import ( + "fmt" + "net/http" + + "bosun.org/cmd/bosun/sched" + + "github.com/MiniProfiler/go/miniprofiler" + "github.com/kylebrandt/boolq" +) + +func ListOpenIncidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) { + // TODO: Retune this when we no longer store email bodies with incidents + list, err := schedule.DataAccess.State().GetAllOpenIncidents() + if err != nil { + return nil, err + } + suppressor := schedule.Silenced() + if suppressor == nil { + return nil, fmt.Errorf("failed to get silences") + } + summaries := []sched.IncidentSummaryView{} + filterText := r.FormValue("filter") + var parsedExpr *boolq.Tree + parsedExpr, err = boolq.Parse(filterText) + if err != nil { + return nil, fmt.Errorf("bad filter: %v", err) + } + for _, iState := range list { + is := sched.MakeIncidentSummary(schedule.Conf, suppressor, iState) + match, err := boolq.AskParsedExpr(parsedExpr, is) + if err != nil { + return nil, err + } + if match { + summaries = append(summaries, is) + } + } + return summaries, nil +} diff --git a/cmd/bosun/web/web.go b/cmd/bosun/web/web.go index 89c6526788..93ad727871 100644 --- a/cmd/bosun/web/web.go +++ b/cmd/bosun/web/web.go @@ -105,6 +105,7 @@ func Listen(listenAddr string, devMode bool, tsdbHost string) error { router.Handle("/api/host", JSON(Host)) router.Handle("/api/last", JSON(Last)) router.Handle("/api/incidents", JSON(Incidents)) + router.Handle("/api/incidents/open", JSON(ListOpenIncidents)) router.Handle("/api/incidents/events", JSON(IncidentEvents)) router.Handle("/api/metadata/get", JSON(GetMetadata)) router.Handle("/api/metadata/metrics", JSON(MetadataMetrics)) diff --git a/vendor/github.com/kylebrandt/boolq/LICENSE b/vendor/github.com/kylebrandt/boolq/LICENSE new file mode 100644 index 0000000000..55117fd357 --- /dev/null +++ b/vendor/github.com/kylebrandt/boolq/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Kyle Brandt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/kylebrandt/boolq/README.md b/vendor/github.com/kylebrandt/boolq/README.md new file mode 100644 index 0000000000..a0ab765684 --- /dev/null +++ b/vendor/github.com/kylebrandt/boolq/README.md @@ -0,0 +1,41 @@ +# boolq +build simple bool expressions. Supports `!`, `AND`, `OR`, and `()` grouping. Individual items start with a letter and continue until whitespace. How you treat those items is up to you and is based on the Ask method. + +# Example: + +``` +package main + +import ( + "fmt" + "log" + + "github.com/kylebrandt/boolq" +) + +func init() { + log.SetFlags(log.LstdFlags | log.Lshortfile) +} + +func main() { + f := foo{} + ask := "(true AND true) AND !false" + q, err := boolq.AskExpr(ask, f) + if err != nil { + log.Fatal(err) + } + log.Println(q) +} + +type foo struct{} + +func (f foo) Ask(ask string) (bool, error) { + switch ask { + case "true": + return true, nil + case "false": + return false, nil + } + return false, fmt.Errorf("couldn't parse ask arg") +} +``` \ No newline at end of file diff --git a/vendor/github.com/kylebrandt/boolq/boolq.go b/vendor/github.com/kylebrandt/boolq/boolq.go new file mode 100644 index 0000000000..1ebf8fafbc --- /dev/null +++ b/vendor/github.com/kylebrandt/boolq/boolq.go @@ -0,0 +1,97 @@ +// Package boolq lets you build generic query expressions. + +package boolq + +import ( + "fmt" + + "github.com/kylebrandt/boolq/parse" +) + +// An Asker is something that can be queried using boolq. The string passed +// to Ask will be the component in an expression. For example with the expression +// `(foo:bar AND baz:biz)` Ask will be called twice, once with the argument "foo:bar" +// and another time with the argument "baz:biz" +type Asker interface { + Ask(string) (bool, error) +} + +// AskExpr takes an expression and an Asker. It then parses the expression +// calling the Asker's Ask on expressions AskNodes and returns if the +// expression is true or not for the given asker. +func AskExpr(expr string, asker Asker) (bool, error) { + q, err := parse.Parse(expr) + if err != nil { + return false, err + } + return walk(q.Root, asker) +} + +// AskParsedExpr is like AskExpr but takes an expression that has already +// been parsed by parse.Parse on the expression. This is useful if you are calling +// the same expression multiple times. +func AskParsedExpr(q *Tree, asker Asker) (bool, error) { + if q.Tree.Root == nil { + return true, nil + } + return walk(q.Root, asker) +} + +type Tree struct { + *parse.Tree +} + +// Parse parses an expression and returns the parsed expression. +// It can be used wtih AskParsedExpr +func Parse(text string) (*Tree, error) { + tree := &Tree{} + if text == "" { + tree.Tree = &parse.Tree{} + return tree, nil + } + var err error + tree.Tree, err = parse.Parse(text) + return tree, err +} + +func walk(node parse.Node, asker Asker) (bool, error) { + switch node := node.(type) { + case *parse.AskNode: + return asker.Ask(node.Text) + case *parse.BinaryNode: + return walkBinary(node, asker) + case *parse.UnaryNode: + return walkUnary(node, asker) + default: + return false, fmt.Errorf("can not walk type %v", node.Type()) + } +} + +func walkBinary(node *parse.BinaryNode, asker Asker) (bool, error) { + l, err := walk(node.Args[0], asker) + if err != nil { + return false, err + } + r, err := walk(node.Args[1], asker) + if err != nil { + return false, err + } + if node.OpStr == "AND" { + return l && r, nil + } + if node.OpStr == "OR" { + return l || r, nil + } + return false, fmt.Errorf("Unrecognized operator: %v", node.OpStr) +} + +func walkUnary(node *parse.UnaryNode, asker Asker) (bool, error) { + r, err := walk(node.Arg, asker) + if err != nil { + return false, err + } + if node.OpStr == "!" { + return !r, nil + } + return false, fmt.Errorf("unknown unary operator: %v", node.OpStr) +} diff --git a/vendor/github.com/kylebrandt/boolq/parse/lex.go b/vendor/github.com/kylebrandt/boolq/parse/lex.go new file mode 100644 index 0000000000..9f2cf30192 --- /dev/null +++ b/vendor/github.com/kylebrandt/boolq/parse/lex.go @@ -0,0 +1,192 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package parse + +import ( + "fmt" + "strings" + "unicode" + "unicode/utf8" +) + +// item represents a token or text string returned from the scanner. +type item struct { + typ itemType // The type of this item. + pos Pos // The starting position, in bytes, of this item in the input string. + val string // The value of this item. +} + +type itemType int + +const ( + itemError itemType = iota // error occurred; value is text of error + itemEOF + + // Literals + itemAsk // field:query + + itemNot // '!' + itemAnd // AND + itemOr // OR + itemLeftParen // ( + itemRightParen // ) +) + +const eof = -1 + +// stateFn represents the state of the scanner as a function that returns the next state. +type stateFn func(*lexer) stateFn + +// lexer holds the state of the scanner. +type lexer struct { + input string // the string being scanned + state stateFn // the next lexing function to enter + pos Pos // current position in the input + start Pos // start position of this item + width Pos // width of last rune read from input + lastPos Pos // position of most recent item returned by nextItem + items chan item // channel of scanned items +} + +// next returns the next rune in the input. +func (l *lexer) next() rune { + if int(l.pos) >= len(l.input) { + l.width = 0 + return eof + } + r, w := utf8.DecodeRuneInString(l.input[l.pos:]) + l.width = Pos(w) + l.pos += l.width + return r +} + +// peek returns but does not consume the next rune in the input. +func (l *lexer) peek() rune { + r := l.next() + l.backup() + return r +} + +// backup steps back one rune. Can only be called once per call of next. +func (l *lexer) backup() { + l.pos -= l.width +} + +// emit passes an item back to the client. +func (l *lexer) emit(t itemType) { + l.items <- item{t, l.start, l.input[l.start:l.pos]} + l.start = l.pos +} + +// accept consumes the next rune if it's from the valid set. +func (l *lexer) accept(valid string) bool { + if strings.IndexRune(valid, l.next()) >= 0 { + return true + } + l.backup() + return false +} + +// acceptRun consumes a run of runes from the valid set. +func (l *lexer) acceptRun(valid string) { + for strings.IndexRune(valid, l.next()) >= 0 { + } + l.backup() +} + +// ignore skips over the pending input before this point. +func (l *lexer) ignore() { + l.start = l.pos +} + +// lineNumber reports which line we're on, based on the position of +// the previous item returned by nextItem. Doing it this way +// means we don't have to worry about peek double counting. +func (l *lexer) lineNumber() int { + return 1 + strings.Count(l.input[:l.lastPos], "\n") +} + +// errorf returns an error token and terminates the scan by passing +// back a nil pointer that will be the next state, terminating l.nextItem. +func (l *lexer) errorf(format string, args ...interface{}) stateFn { + l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)} + return nil +} + +// nextItem returns the next item from the input. +func (l *lexer) nextItem() item { + item := <-l.items + l.lastPos = item.pos + return item +} + +// lex creates a new scanner for the input string. +func lex(input string) *lexer { + l := &lexer{ + input: input, + items: make(chan item), + } + go l.run() + return l +} + +// run runs the state machine for the lexer. +func (l *lexer) run() { + for l.state = lexItem; l.state != nil; { + l.state = l.state(l) + } +} + +// state functions + +func lexItem(l *lexer) stateFn { +Loop: + for { + switch r := l.next(); { + case unicode.IsLetter(r): + return lexWord + case r == '(': + l.emit(itemLeftParen) + case r == ')': + l.emit(itemRightParen) + case r == '!': + l.emit(itemNot) + case isSpace(r): + l.ignore() + case r == eof: + l.emit(itemEOF) + break Loop + default: + return l.errorf("invalid character: %s", string(r)) + } + } + return nil +} + +func lexWord(l *lexer) stateFn { + for { + switch r := l.next(); { + case !isSpace(r) && r != eof && r != ')': + // absorb + default: + l.backup() + s := l.input[l.start:l.pos] + switch s { + case "AND": + l.emit(itemAnd) + case "OR": + l.emit(itemOr) + default: + l.emit(itemAsk) + } + return lexItem + } + } +} + +// isSpace reports whether r is a space character. +func isSpace(r rune) bool { + return unicode.IsSpace(r) +} diff --git a/vendor/github.com/kylebrandt/boolq/parse/node.go b/vendor/github.com/kylebrandt/boolq/parse/node.go new file mode 100644 index 0000000000..6acd20ef15 --- /dev/null +++ b/vendor/github.com/kylebrandt/boolq/parse/node.go @@ -0,0 +1,126 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Parse nodes. +package parse + +import "fmt" + +var textFormat = "%s" // Changed to "%q" in tests for better error messages. + +// A Node is an element in the parse tree. The interface is trivial. +// The interface contains an unexported method so that only +// types local to this package can satisfy it. +type Node interface { + Type() NodeType + String() string + Position() Pos // byte position of start of node in full original input string + // Make sure only functions in this package can create Nodes. + unexported() +} + +// NodeType identifies the type of a parse tree node. +type NodeType int + +// Pos represents a byte position in the original input text from which +// this template was parsed. +type Pos int + +func (p Pos) Position() Pos { + return p +} + +// unexported keeps Node implementations local to the package. +// All implementations embed Pos, so this takes care of it. +func (Pos) unexported() { +} + +// Type returns itself and provides an easy default implementation +// for embedding in a Node. Embedded in all non-trivial Nodes. +func (t NodeType) Type() NodeType { + return t +} + +const ( + NodeAsk NodeType = iota // key:value expression. + NodeBinary NodeType = iota // + NodeUnary NodeType = iota // +) + +// BinaryNode holds two arguments and an operator. +type BinaryNode struct { + NodeType + Pos + Args [2]Node + Operator item + OpStr string +} + +func newBinary(operator item, arg1, arg2 Node) *BinaryNode { + return &BinaryNode{NodeType: NodeBinary, Pos: operator.pos, Args: [2]Node{arg1, arg2}, Operator: operator, OpStr: operator.val} +} + +func (b *BinaryNode) String() string { + return fmt.Sprintf("%s %s %s", b.Args[0], b.Operator.val, b.Args[1]) +} + +func (b *BinaryNode) StringAST() string { + return fmt.Sprintf("%s(%s, %s)", b.Operator.val, b.Args[0], b.Args[1]) +} + +// UnaryNode holds one argument and an operator. +type UnaryNode struct { + NodeType + Pos + Arg Node + Operator item + OpStr string +} + +func newUnary(operator item, arg Node) *UnaryNode { + return &UnaryNode{NodeType: NodeUnary, Pos: operator.pos, Arg: arg, Operator: operator, OpStr: operator.val} +} + +func (u *UnaryNode) String() string { + return fmt.Sprintf("%s%s", u.Operator.val, u.Arg) +} + +func (u *UnaryNode) StringAST() string { + return fmt.Sprintf("%s(%s)", u.Operator.val, u.Arg) +} + +// Walk invokes f on n and sub-nodes of n. +func Walk(n Node, f func(Node)) { + f(n) + switch n := n.(type) { + case *BinaryNode: + Walk(n.Args[0], f) + Walk(n.Args[1], f) + case *AskNode: + // Ignore. + case *UnaryNode: + Walk(n.Arg, f) + default: + panic(fmt.Errorf("other type: %T", n)) + } +} + +// AskNode holds a filter invocation. +type AskNode struct { + NodeType + Pos + Text string +} + +func (a *AskNode) String() string { + return fmt.Sprintf("%s", a.Text) +} + +func newAsk(pos Pos, text string) *AskNode { + return &AskNode{ + NodeType: NodeAsk, + Pos: pos, + Text: text, + } +} \ No newline at end of file diff --git a/vendor/github.com/kylebrandt/boolq/parse/parse.go b/vendor/github.com/kylebrandt/boolq/parse/parse.go new file mode 100644 index 0000000000..f330f9cbca --- /dev/null +++ b/vendor/github.com/kylebrandt/boolq/parse/parse.go @@ -0,0 +1,204 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package parse builds parse trees for expressions as defined by expr. Clients +// should use that package to construct expressions rather than this one, which +// provides shared internal data structures not intended for general use. +package parse + +import ( + "fmt" + "runtime" +) + +// Tree is the representation of a single parsed expression. +type Tree struct { + Text string // text parsed to create the expression. + Root Node // top-level root of the tree, returns a number. + + // Parsing only; cleared after parse. + lex *lexer + token [1]item // one-token lookahead for parser. + peekCount int +} + +// Parse returns a Tree, created by parsing the expression described in the +// argument string. If an error is encountered, parsing stops and an empty Tree +// is returned with the error. +func Parse(text string) (t *Tree, err error) { + t = New() + t.Text = text + err = t.Parse(text) + return +} + +// next returns the next token. +func (t *Tree) next() item { + if t.peekCount > 0 { + t.peekCount-- + } else { + t.token[0] = t.lex.nextItem() + } + return t.token[t.peekCount] +} + +// backup backs the input stream up one token. +func (t *Tree) backup() { + t.peekCount++ +} + +// peek returns but does not consume the next token. +func (t *Tree) peek() item { + if t.peekCount > 0 { + return t.token[t.peekCount-1] + } + t.peekCount = 1 + t.token[0] = t.lex.nextItem() + return t.token[0] +} + +// Parsing. + +// New allocates a new parse tree with the given name. +func New() *Tree { + return &Tree{} +} + +// errorf formats the error and terminates processing. +func (t *Tree) errorf(format string, args ...interface{}) { + t.Root = nil + format = fmt.Sprintf("expr: %s", format) + panic(fmt.Errorf(format, args...)) +} + +// error terminates processing. +func (t *Tree) error(err error) { + t.errorf("%s", err) +} + +// expect consumes the next token and guarantees it has the required type. +func (t *Tree) expect(expected itemType, context string) item { + token := t.next() + if token.typ != expected { + t.unexpected(token, context) + } + return token +} + +// expectOneOf consumes the next token and guarantees it has one of the required types. +func (t *Tree) expectOneOf(expected1, expected2 itemType, context string) item { + token := t.next() + if token.typ != expected1 && token.typ != expected2 { + t.unexpected(token, context) + } + return token +} + +// unexpected complains about the token and terminates processing. +func (t *Tree) unexpected(token item, context string) { + t.errorf("unexpected %s in %s", token, context) +} + +// recover is the handler that turns panics into returns from the top level of Parse. +func (t *Tree) recover(errp *error) { + e := recover() + if e != nil { + if _, ok := e.(runtime.Error); ok { + panic(e) + } + if t != nil { + t.stopParse() + } + *errp = e.(error) + } + return +} + +// startParse initializes the parser, using the lexer. +func (t *Tree) startParse(lex *lexer) { + t.Root = nil + t.lex = lex +} + +// stopParse terminates parsing. +func (t *Tree) stopParse() { + t.lex = nil +} + +// Parse parses the expression definition string to construct a representation +// of the expression for execution. +func (t *Tree) Parse(text string) (err error) { + defer t.recover(&err) + t.startParse(lex(text)) + t.Text = text + t.parse() + t.stopParse() + return nil +} + +// parse is the top-level parser for an expression. +// It runs to EOF. +func (t *Tree) parse() { + t.Root = t.O() + t.expect(itemEOF, "input") +} + +/* Grammar: +O -> A {"AND" A} +A -> C {"OR" C} +C -> v | "(" O ")" | "!" O +v -> ask +*/ + +// expr: +func (t *Tree) O() Node { + n := t.A() + for { + switch t.peek().typ { + case itemOr: + n = newBinary(t.next(), n, t.A()) + default: + return n + } + } +} + +func (t *Tree) A() Node { + n := t.C() + for { + switch t.peek().typ { + case itemAnd: + n = newBinary(t.next(), n, t.C()) + default: + return n + } + } +} + +func (t *Tree) C() Node { + switch token := t.peek(); token.typ { + case itemAsk: + return t.v() + case itemNot: + return newUnary(t.next(), t.C()) + case itemLeftParen: + t.next() + n := t.O() + t.expect(itemRightParen, "input") + return n + default: + t.unexpected(token, "input") + } + return nil +} + +func (t *Tree) v() Node { + switch token := t.next(); token.typ { + case itemAsk: + return newAsk(token.pos, token.val) + default: + t.unexpected(token, "input") + } + return nil +} diff --git a/vendor/github.com/ryanuber/go-glob/LICENSE b/vendor/github.com/ryanuber/go-glob/LICENSE new file mode 100644 index 0000000000..bdfbd95149 --- /dev/null +++ b/vendor/github.com/ryanuber/go-glob/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ryan Uber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/ryanuber/go-glob/README.md b/vendor/github.com/ryanuber/go-glob/README.md new file mode 100644 index 0000000000..48f7fcb05a --- /dev/null +++ b/vendor/github.com/ryanuber/go-glob/README.md @@ -0,0 +1,29 @@ +# String globbing in golang [![Build Status](https://travis-ci.org/ryanuber/go-glob.svg)](https://travis-ci.org/ryanuber/go-glob) + +`go-glob` is a single-function library implementing basic string glob support. + +Globs are an extremely user-friendly way of supporting string matching without +requiring knowledge of regular expressions or Go's particular regex engine. Most +people understand that if you put a `*` character somewhere in a string, it is +treated as a wildcard. Surprisingly, this functionality isn't found in Go's +standard library, except for `path.Match`, which is intended to be used while +comparing paths (not arbitrary strings), and contains specialized logic for this +use case. A better solution might be a POSIX basic (non-ERE) regular expression +engine for Go, which doesn't exist currently. + +Example +======= + +``` +package main + +import "github.com/ryanuber/go-glob" + +func main() { + glob.Glob("*World!", "Hello, World!") // true + glob.Glob("Hello,*", "Hello, World!") // true + glob.Glob("*ello,*", "Hello, World!") // true + glob.Glob("World!", "Hello, World!") // false + glob.Glob("/home/*", "/home/ryanuber/.bashrc") // true +} +``` diff --git a/vendor/github.com/ryanuber/go-glob/glob.go b/vendor/github.com/ryanuber/go-glob/glob.go new file mode 100644 index 0000000000..d9d46379a8 --- /dev/null +++ b/vendor/github.com/ryanuber/go-glob/glob.go @@ -0,0 +1,51 @@ +package glob + +import "strings" + +// The character which is treated like a glob +const GLOB = "*" + +// Glob will test a string pattern, potentially containing globs, against a +// subject string. The result is a simple true/false, determining whether or +// not the glob pattern matched the subject text. +func Glob(pattern, subj string) bool { + // Empty pattern can only match empty subject + if pattern == "" { + return subj == pattern + } + + // If the pattern _is_ a glob, it matches everything + if pattern == GLOB { + return true + } + + parts := strings.Split(pattern, GLOB) + + if len(parts) == 1 { + // No globs in pattern, so test for equality + return subj == pattern + } + + leadingGlob := strings.HasPrefix(pattern, GLOB) + trailingGlob := strings.HasSuffix(pattern, GLOB) + end := len(parts) - 1 + + // Check the first section. Requires special handling. + if !leadingGlob && !strings.HasPrefix(subj, parts[0]) { + return false + } + + // Go over the middle parts and ensure they match. + for i := 1; i < end; i++ { + if !strings.Contains(subj, parts[i]) { + return false + } + + // Trim evaluated text from subj as we loop over the pattern. + idx := strings.Index(subj, parts[i]) + len(parts[i]) + subj = subj[idx:] + } + + // Reached the last section. Requires special handling. + return trailingGlob || strings.HasSuffix(subj, parts[end]) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 7102b0ac30..9d2535596f 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -402,6 +402,16 @@ "revision": "f61123ea07e1921ac4f174e8c42225fd39bd6c6a", "revisionTime": "2015-10-16T12:35:57-05:00" }, + { + "path": "github.com/kylebrandt/boolq", + "revision": "f869a7265c7ec4f3e2618a39b7b3877a5629eb17", + "revisionTime": "2016-06-08T12:45:48-04:00" + }, + { + "path": "github.com/kylebrandt/boolq/parse", + "revision": "3c0efef9c6cd5f400eba1871a3be6191796db71a", + "revisionTime": "2016-06-01T16:00:19-04:00" + }, { "path": "github.com/kylebrandt/gohop", "revision": "605b5abd5cb7b630eb91cf1f35d365121ccdb6fd", @@ -432,6 +442,11 @@ "revision": "9fdb4e763e833f166e76009e5a33132877c32bfb", "revisionTime": "2015-11-28T12:32:46+01:00" }, + { + "path": "github.com/ryanuber/go-glob", + "revision": "572520ed46dbddaed19ea3d9541bdd0494163693", + "revisionTime": "2016-02-26T00:37:05-08:00" + }, { "path": "github.com/siddontang/go/bson", "revision": "b151716326d7c7faf22473c0b04fb7ceac88b587",