diff --git a/ast/ast.go b/ast/ast.go index 9d5159a..d6406ab 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -17,14 +17,14 @@ The tree looks like this (in JSON-style syntax): */ package ast -// Type Root represents the root of the AST tree. +// Root represents the root of the AST tree. type Root struct { Begin Begin `json:"begin"` Topics map[string]*Topic `json:"topics"` Objects []*Object `json:"objects"` } -// Type Begin represents the "begin block" style data (configuration). +// Begin represents the "begin block" style data (configuration). type Begin struct { Global map[string]string `json:"global"` Var map[string]string `json:"var"` @@ -33,14 +33,14 @@ type Begin struct { Array map[string][]string `json:"array"` // Map of string (names) to arrays-of-strings } -// Type Topic represents a topic of conversation. +// Topic represents a topic of conversation. type Topic struct { Triggers []*Trigger `json:"triggers"` Includes map[string]bool `json:"includes"` Inherits map[string]bool `json:"inherits"` } -// Type Trigger has a trigger pattern and all the subsequent handlers for it. +// Trigger has a trigger pattern and all the subsequent handlers for it. type Trigger struct { Trigger string `json:"trigger"` Reply []string `json:"reply"` @@ -49,7 +49,7 @@ type Trigger struct { Previous string `json:"previous"` } -// Type Object contains source code of dynamically parsed object macros. +// Object contains source code of dynamically parsed object macros. type Object struct { Name string `json:"name"` Language string `json:"language"` @@ -58,22 +58,22 @@ type Object struct { // New creates a new, empty, abstract syntax tree. func New() *Root { - ast := new(Root) - - // Initialize all the structures. - ast.Begin.Global = map[string]string{} - ast.Begin.Var = map[string]string{} - ast.Begin.Sub = map[string]string{} - ast.Begin.Person = map[string]string{} - ast.Begin.Array = map[string][]string{} + ast := &Root{ + // Initialize all the structures. + Begin: Begin{ + Global: map[string]string{}, + Var: map[string]string{}, + Sub: map[string]string{}, + Person: map[string]string{}, + Array: map[string][]string{}, + }, + Topics: map[string]*Topic{}, + Objects: []*Object{}, + } // Initialize the 'random' topic. - ast.Topics = map[string]*Topic{} ast.AddTopic("random") - // Objects - ast.Objects = []*Object{} - return ast } diff --git a/cmd/rivescript/main.go b/cmd/rivescript/main.go index e2e3f40..245aae6 100644 --- a/cmd/rivescript/main.go +++ b/cmd/rivescript/main.go @@ -27,17 +27,31 @@ import ( "github.com/aichaos/rivescript-go/lang/javascript" ) +var ( + // Command line arguments. + version bool + debug bool + utf8 bool + depth uint + nostrict bool + nocolor bool +) + +func init() { + flag.BoolVar(&version, "version", false, "Show the version number and exit.") + flag.BoolVar(&debug, "debug", false, "Enable debug mode.") + flag.BoolVar(&utf8, "utf8", false, "Enable UTF-8 mode.") + flag.UintVar(&depth, "depth", 50, "Recursion depth limit (default 50)") + flag.BoolVar(&nostrict, "nostrict", false, "Disable strict syntax checking") + flag.BoolVar(&nocolor, "nocolor", false, "Disable ANSI colors") +} + func main() { // Collect command line arguments. - version := flag.Bool("version", false, "Show the version number and exit.") - debug := flag.Bool("debug", false, "Enable debug mode.") - utf8 := flag.Bool("utf8", false, "Enable UTF-8 mode.") - depth := flag.Uint("depth", 50, "Recursion depth limit (default 50)") - nostrict := flag.Bool("nostrict", false, "Disable strict syntax checking") flag.Parse() args := flag.Args() - if *version == true { + if version { fmt.Printf("RiveScript-Go version %s\n", rivescript.VERSION) os.Exit(0) } @@ -51,10 +65,10 @@ func main() { // Initialize the bot. bot := rivescript.New(&rivescript.Config{ - Debug: *debug, - Strict: !*nostrict, - Depth: *depth, - UTF8: *utf8, + Debug: debug, + Strict: !nostrict, + Depth: depth, + UTF8: utf8, }) // JavaScript object macro handler. @@ -85,31 +99,76 @@ Type a message to the bot and press Return to send it. // Drop into the interactive command shell. reader := bufio.NewReader(os.Stdin) for { - fmt.Print("You> ") + color(yellow, "You>") text, _ := reader.ReadString('\n') text = strings.TrimSpace(text) if len(text) == 0 { continue } - if strings.Index(text, "/help") == 0 { + if strings.Contains(text, "/help") { help() - } else if strings.Index(text, "/quit") == 0 { + } else if strings.Contains(text, "/quit") { os.Exit(0) + } else if strings.Contains(text, "/debug t") { + bot.SetGlobal("debug", "true") + color(cyan, "Debug mode enabled.", "\n") + } else if strings.Contains(text, "/debug f") { + bot.SetGlobal("debug", "false") + color(cyan, "Debug mode disabled.", "\n") + } else if strings.Contains(text, "/debug") { + debug, _ := bot.GetGlobal("debug") + color(cyan, "Debug mode is currently:", debug, "\n") + } else if strings.Contains(text, "/dump t") { + bot.DumpTopics() + } else if strings.Contains(text, "/dump s") { + bot.DumpSorted() } else { reply, err := bot.Reply("localuser", text) if err != nil { - fmt.Printf("Error> %s\n", err) + color(red, "Error>", err.Error(), "\n") } else { - fmt.Printf("Bot> %s\n", reply) + color(green, "RiveScript>", reply, "\n") } } } } +// Names for pretty ANSI colors. +const ( + red = `31;1` + yellow = `33;1` + green = `32;1` + cyan = `36;1` +) + +func color(color string, text ...string) { + if nocolor { + fmt.Printf( + "%s %s", + text[0], + strings.Join(text[1:], " "), + ) + } else { + fmt.Printf( + "\x1b[%sm%s\x1b[0m %s", + color, + text[0], + strings.Join(text[1:], " "), + ) + } +} + func help() { fmt.Printf(`Supported commands: -- /help : Show this text. -- /quit : Exit the program. +- /help + Show this text. +- /quit + Exit the program. +- /debug [true|false] + Enable or disable debug mode. If no setting is given, it prints + the current debug mode. +- /dump + For debugging purposes, dump the topic and sorted trigger trees. `) } diff --git a/doc_test.go b/doc_test.go index f009baf..d8fd010 100644 --- a/doc_test.go +++ b/doc_test.go @@ -1,4 +1,4 @@ -package rivescript +package rivescript_test import ( "fmt" @@ -108,7 +108,7 @@ func ExampleRiveScript_subroutine() { // Define an object macro named `setname` bot.SetSubroutine("setname", func(rs *rss.RiveScript, args []string) string { - uid := rs.CurrentUser() + uid, _ := rs.CurrentUser() rs.SetUservar(uid, args[0], args[1]) return "" }) diff --git a/parser/parser.go b/parser/parser.go index 857f913..006cdea 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -48,9 +48,7 @@ type Parser struct { // New creates and returns a new instance of a RiveScript Parser. func New(config ParserConfig) *Parser { - self := new(Parser) - self.C = config - return self + return &Parser{config} } // say proxies to the OnDebug handler. diff --git a/rivescript_test.go b/rivescript_test.go new file mode 100644 index 0000000..2f3c328 --- /dev/null +++ b/rivescript_test.go @@ -0,0 +1,57 @@ +package rivescript_test + +// This test file contains the unit tests that had to be segregated from the +// others in the `src/` package. +// +// The only one here so far is an object macro test. It needed to use the public +// RiveScript API because the JavaScript handler expects an object of that type, +// and so it couldn't be in the `src/` package or it would create a dependency +// cycle. + +import ( + "testing" + + rivescript "github.com/aichaos/rivescript-go" + "github.com/aichaos/rivescript-go/lang/javascript" +) + +// This one has to test the public interface because of the JavaScript handler +// expecting a *RiveScript of the correct color. +func TestJavaScript(t *testing.T) { + rs := rivescript.New(nil) + rs.SetHandler("javascript", javascript.New(rs)) + rs.Stream(` + > object reverse javascript + var msg = args.join(" "); + return msg.split("").reverse().join(""); + < object + + > object nolang + return "No language provided!" + < object + + + reverse * + - reverse + + + no lang + - nolang + `) + rs.SortReplies() + + // Helper function to assert replies via the public interface. + assert := func(input, expected string) { + reply, err := rs.Reply("local-user", input) + if err != nil { + t.Errorf("Got error when trying to get a reply: %v", err) + } else if reply != expected { + t.Errorf("Got unexpected reply. Expected %s, got %s", expected, reply) + } + } + + assert("reverse hello world", "dlrow olleh") + assert("no lang", "[ERR: Object Not Found]") + + // Disable support. + rs.RemoveHandler("javascript") + assert("reverse hello world", "[ERR: Object Not Found]") +} diff --git a/src/astmap.go b/src/astmap.go index d971b32..fec8614 100644 --- a/src/astmap.go +++ b/src/astmap.go @@ -54,12 +54,3 @@ type astObject struct { language string code []string } - -// This is like astTopic but is just for %Previous mapping -type thatTopic struct { - triggers map[string]*thatTrigger -} - -type thatTrigger struct { - previous map[string]*astTrigger -} diff --git a/src/config.go b/src/config.go index 93e574f..0a61689 100644 --- a/src/config.go +++ b/src/config.go @@ -4,6 +4,9 @@ package rivescript import ( "errors" + "fmt" + "strconv" + "strings" "github.com/aichaos/rivescript-go/macro" "github.com/aichaos/rivescript-go/sessions" @@ -54,6 +57,23 @@ func (rs *RiveScript) SetGlobal(name, value string) { rs.cLock.Lock() defer rs.cLock.Unlock() + // Special globals that reconfigure the interpreter. + if name == "debug" { + switch strings.ToLower(value) { + case "true", "t", "on", "yes": + rs.Debug = true + default: + rs.Debug = false + } + } else if name == "depth" { + depth, err := strconv.Atoi(value) + if err != nil { + rs.warn("Can't set global `depth` to `%s`: %s\n", value, err) + } else { + rs.Depth = uint(depth) + } + } + if value == UNDEFINED { delete(rs.global, name) } else { @@ -114,10 +134,17 @@ func (rs *RiveScript) GetGlobal(name string) (string, error) { rs.cLock.Lock() defer rs.cLock.Unlock() + // Special globals. + if name == "debug" { + return fmt.Sprintf("%v", rs.Debug), nil + } else if name == "depth" { + return strconv.Itoa(int(rs.Depth)), nil + } + if _, ok := rs.global[name]; ok { return rs.global[name], nil } - return UNDEFINED, errors.New("Global variable not found.") + return UNDEFINED, fmt.Errorf("global variable %s not found", name) } // GetVariable retrieves the value of a bot variable. @@ -128,7 +155,7 @@ func (rs *RiveScript) GetVariable(name string) (string, error) { if _, ok := rs.vars[name]; ok { return rs.vars[name], nil } - return UNDEFINED, errors.New("Variable not found.") + return UNDEFINED, fmt.Errorf("bot variable %s not found", name) } // GetUservar retrieves the value of a user variable. diff --git a/src/inheritance.go b/src/inheritance.go index 85b8f6e..ed2664e 100644 --- a/src/inheritance.go +++ b/src/inheritance.go @@ -18,17 +18,15 @@ inheritance depth. Params: topic: The name of the topic to scan through - topics: The `rs.topics` structure if crawling the normal reply tree - thats: The `rs.thats` structure if crawling the %Previous reply tree - -You must *ONLY* provide `topics` OR `thats`, not both. Make the other one `nil`. + thats: Whether to get only triggers that have %Previous. + `false` returns all triggers. Each "trigger" returned from this function is actually an array, where index 0 is the trigger text and index 1 is the pointer to the trigger's data within the original topic structure. */ -func (rs *RiveScript) getTopicTriggers(topic string, topics map[string]*astTopic, thats map[string]*thatTopic) []sortedTriggerEntry { - return rs._getTopicTriggers(topic, topics, thats, 0, 0, false) +func (rs *RiveScript) getTopicTriggers(topic string, thats bool) []sortedTriggerEntry { + return rs._getTopicTriggers(topic, thats, 0, 0, false) } /* @@ -51,7 +49,7 @@ The inherited option is true if this is a recursive call, from a topic that inherits other topics. This forces the {inherits} tag to be added to the triggers. This only applies when the topic 'includes' another topic. */ -func (rs *RiveScript) _getTopicTriggers(topic string, topics map[string]*astTopic, thats map[string]*thatTopic, depth uint, inheritance int, inherited bool) []sortedTriggerEntry { +func (rs *RiveScript) _getTopicTriggers(topic string, thats bool, depth uint, inheritance int, inherited bool) []sortedTriggerEntry { // Break if we're in too deep. if depth > rs.Depth { rs.warn("Deep recursion while scanning topic inheritance!") @@ -81,22 +79,17 @@ func (rs *RiveScript) _getTopicTriggers(topic string, topics map[string]*astTopi // Get those that exist in this topic directly. inThisTopic := []sortedTriggerEntry{} - if thats == nil { - // The non-thats structure is: {topics}->[ array of triggers ] - if _, ok := rs.topics[topic]; ok { - for _, trigger := range rs.topics[topic].triggers { + + if _, ok := rs.topics[topic]; ok { + for _, trigger := range rs.topics[topic].triggers { + if !thats { + // All triggers. entry := sortedTriggerEntry{trigger.trigger, trigger} inThisTopic = append(inThisTopic, entry) - } - } - } else { - // The 'that' structure is: {topic}->{cur trig}->{prev trig}->{trigger info} - if _, ok := rs.thats[topic]; ok { - for _, curTrig := range rs.thats[topic].triggers { - for _, previous := range curTrig.previous { - // fmt.Printf("Previous: %v, curTrig: %v\n", previous, curTrig) - entry := sortedTriggerEntry{previous.trigger, previous} - inThisTopic = append(inThisTopic, entry) + } else { + // Only triggers that have %Previous. + if trigger.previous != "" { + inThisTopic = append(inThisTopic, sortedTriggerEntry{trigger.previous, trigger}) } } } @@ -106,7 +99,7 @@ func (rs *RiveScript) _getTopicTriggers(topic string, topics map[string]*astTopi if _, ok := rs.includes[topic]; ok { for includes := range rs.includes[topic] { rs.say("Topic %s includes %s", topic, includes) - triggers = append(triggers, rs._getTopicTriggers(includes, topics, thats, depth+1, inheritance+1, false)...) + triggers = append(triggers, rs._getTopicTriggers(includes, thats, depth+1, inheritance+1, false)...) } } @@ -114,7 +107,7 @@ func (rs *RiveScript) _getTopicTriggers(topic string, topics map[string]*astTopi if _, ok := rs.inherits[topic]; ok { for inherits := range rs.inherits[topic] { rs.say("Topic %s inherits %s", topic, inherits) - triggers = append(triggers, rs._getTopicTriggers(inherits, topics, thats, depth+1, inheritance+1, true)...) + triggers = append(triggers, rs._getTopicTriggers(inherits, thats, depth+1, inheritance+1, true)...) } } diff --git a/src/object_test.go b/src/object_test.go index 19bf4bb..2378f3d 100644 --- a/src/object_test.go +++ b/src/object_test.go @@ -3,52 +3,8 @@ package rivescript import ( "strings" "testing" - - rivescript "github.com/aichaos/rivescript-go" - "github.com/aichaos/rivescript-go/lang/javascript" ) -// This one has to test the public interface because of the JavaScript handler -// expecting a *RiveScript of the correct color. -func TestJavaScript(t *testing.T) { - rs := rivescript.New(nil) - rs.SetHandler("javascript", javascript.New(rs)) - rs.Stream(` - > object reverse javascript - var msg = args.join(" "); - return msg.split("").reverse().join(""); - < object - - > object nolang - return "No language provided!" - < object - - + reverse * - - reverse - - + no lang - - nolang - `) - rs.SortReplies() - - // Helper function to assert replies via the public interface. - assert := func(input, expected string) { - reply, err := rs.Reply("local-user", input) - if err != nil { - t.Errorf("Got error when trying to get a reply: %v", err) - } else if reply != expected { - t.Errorf("Got unexpected reply. Expected %s, got %s", expected, reply) - } - } - - assert("reverse hello world", "dlrow olleh") - assert("no lang", "[ERR: Object Not Found]") - - // Disable support. - rs.RemoveHandler("javascript") - assert("reverse hello world", "[ERR: Object Not Found]") -} - // Mock up an object macro handler for the private API testing, for code // coverage. The MockHandler just returns its text as a string. func TestMacroParsing(t *testing.T) { diff --git a/src/parser.go b/src/parser.go index d43a650..d951c3d 100644 --- a/src/parser.go +++ b/src/parser.go @@ -78,23 +78,6 @@ func (rs *RiveScript) parse(path string, lines []string) error { trigger.previous = trig.Previous rs.topics[topic].triggers = append(rs.topics[topic].triggers, trigger) - - // Does this one have a %Previous? If so, make a pointer to this - // exact trigger in rs.thats - if trigger.previous != "" { - // Initialize the structure first. - if _, ok := rs.thats[topic]; !ok { - rs.thats[topic] = new(thatTopic) - rs.say("%q", rs.thats[topic]) - rs.thats[topic].triggers = map[string]*thatTrigger{} - } - if _, ok := rs.thats[topic].triggers[trigger.trigger]; !ok { - rs.say("%q", rs.thats[topic].triggers[trigger.trigger]) - rs.thats[topic].triggers[trigger.trigger] = new(thatTrigger) - rs.thats[topic].triggers[trigger.trigger].previous = map[string]*astTrigger{} - } - rs.thats[topic].triggers[trigger.trigger].previous[trigger.previous] = trigger - } } } diff --git a/src/rivescript.go b/src/rivescript.go index 28a0f97..673c5b4 100644 --- a/src/rivescript.go +++ b/src/rivescript.go @@ -55,7 +55,6 @@ type RiveScript struct { handlers map[string]macro.MacroInterface // object language handlers subroutines map[string]Subroutine // Golang object handlers topics map[string]*astTopic // main topic structure - thats map[string]*thatTopic // %Previous mapper sorted *sortBuffer // Sorted data from SortReplies() // State information. @@ -69,38 +68,37 @@ type RiveScript struct { // New creates a new RiveScript instance with the default configuration. func New() *RiveScript { - rs := new(RiveScript) - - // Set the default config objects that don't have good zero-values. - rs.Strict = true - rs.Depth = 50 - rs.sessions = memory.New() - - rs.UnicodePunctuation = regexp.MustCompile(`[.,!?;:]`) - - // Initialize helpers. + rs := &RiveScript{ + // Set the default config objects that don't have good zero-values. + Strict: true, + Depth: 50, + sessions: memory.New(), + + // Default punctuation that gets removed from messages in UTF-8 mode. + UnicodePunctuation: regexp.MustCompile(`[.,!?;:]`), + + // Initialize all internal data structures. + global: map[string]string{}, + vars: map[string]string{}, + sub: map[string]string{}, + person: map[string]string{}, + array: map[string][]string{}, + includes: map[string]map[string]bool{}, + inherits: map[string]map[string]bool{}, + objlangs: map[string]string{}, + handlers: map[string]macro.MacroInterface{}, + subroutines: map[string]Subroutine{}, + topics: map[string]*astTopic{}, + sorted: new(sortBuffer), + } + + // Helpers. rs.parser = parser.New(parser.ParserConfig{ - Strict: rs.Strict, - UTF8: rs.UTF8, + Strict: true, OnDebug: rs.say, OnWarn: rs.warnSyntax, }) - // Initialize all the data structures. - rs.global = map[string]string{} - rs.vars = map[string]string{} - rs.sub = map[string]string{} - rs.person = map[string]string{} - rs.array = map[string][]string{} - rs.includes = map[string]map[string]bool{} - rs.inherits = map[string]map[string]bool{} - rs.objlangs = map[string]string{} - rs.handlers = map[string]macro.MacroInterface{} - rs.subroutines = map[string]Subroutine{} - rs.topics = map[string]*astTopic{} - rs.thats = map[string]*thatTopic{} - rs.sorted = new(sortBuffer) - return rs } @@ -113,6 +111,14 @@ func (rs *RiveScript) Configure(debug, strict, utf8 bool, depth uint, rs.UTF8 = utf8 rs.Depth = depth rs.sessions = sessions + + // Reconfigure the parser. + rs.parser = parser.New(parser.ParserConfig{ + Strict: strict, + UTF8: utf8, + OnDebug: rs.say, + OnWarn: rs.warnSyntax, + }) } // SetUnicodePunctuation allows for overriding the regexp for punctuation. diff --git a/src/sorting.go b/src/sorting.go index 80f5f8d..d25c935 100644 --- a/src/sorting.go +++ b/src/sorting.go @@ -28,13 +28,13 @@ func (rs *RiveScript) SortReplies() error { // Collect a list of all the triggers we're going to worry about. If this // topic inherits another topic, we need to recursively add those to the // list as well. - allTriggers := rs.getTopicTriggers(topic, rs.topics, nil) + allTriggers := rs.getTopicTriggers(topic, false) // Sort these triggers. rs.sorted.topics[topic] = rs.sortTriggerSet(allTriggers, true) // Get all of the %Previous triggers for this topic. - thatTriggers := rs.getTopicTriggers(topic, nil, rs.thats) + thatTriggers := rs.getTopicTriggers(topic, true) // And sort them, too. rs.sorted.thats[topic] = rs.sortTriggerSet(thatTriggers, false)