From 8642f5ec2441de77208844598154a20ac37977ea Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Wed, 1 Feb 2017 23:01:22 -0800 Subject: [PATCH 1/2] Low-hanging bugs, unit test and code restructure --- Changes.md | 48 +++ Makefile | 2 +- cmd/rivescript/main.go | 3 +- config.go | 45 +++ config/config.go | 54 ---- deprecated.go | 67 ++++ doc_test.go | 53 ++- rivescript.go | 214 ++++++------- src/base_test.go | 30 +- src/begin_test.go | 50 +++ src/bot_variable_test.go | 48 +++ src/brain.go | 3 +- src/options_test.go | 52 +++ src/reply_test.go | 187 +++++++++++ src/rivescript.go | 38 +-- src/rivescript_test.go | 677 --------------------------------------- src/sessions_test.go | 12 +- src/substitution_test.go | 42 +++ src/tags.go | 16 +- src/topic_test.go | 120 +++++++ src/trigger_test.go | 138 ++++++++ src/unicode_test.go | 70 ++++ 22 files changed, 1054 insertions(+), 915 deletions(-) create mode 100644 config.go delete mode 100644 config/config.go create mode 100644 deprecated.go create mode 100644 src/begin_test.go create mode 100644 src/bot_variable_test.go create mode 100644 src/options_test.go create mode 100644 src/reply_test.go delete mode 100644 src/rivescript_test.go create mode 100644 src/substitution_test.go create mode 100644 src/topic_test.go create mode 100644 src/trigger_test.go create mode 100644 src/unicode_test.go diff --git a/Changes.md b/Changes.md index 4a43786..4dc2824 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,54 @@ This documents the history of significant changes to `rivescript-go`. +## v0.1.1 - TBD + +This update focuses on bug fixes and code reorganization. + +### API Breaking Changes + +* `rivescript.New()` and the `Config` struct have been refactored. `Config` + now comes from the `rivescript` package directly rather than needing to + import from `rivescript/config`. + + For your code, this means you can remove the `aichaos/rivescript-go/config` + import and change the `config.Config` name to `rivescript.Config`: + + ```go + import "github.com/aichaos/rivescript-go" + + func main() { + // Example defining the struct to override defaults. + bot := rivescript.New(&rivescript.Config{Debug: true}) + + // For the old `config.Basic()` that provided default settings, just + // pass in a nil Config object. + bot = rivescript.New(nil) + + // For the old `config.UTF8()` helper function that provided a Config with + // UTF-8 mode enabled, instead call rivescript.WithUTF8() + bot = rivescript.New(rivescript.WithUTF8()) + } + ``` + +### Changes + +* Separate the unit tests into multiple files and put them in the `rivescript` + package instead of `rivescript_test`; this enables test code coverage + reporting (we're at 72.1% coverage!) +* Handle module configuration at the root package instead of in the `src` + package. This enabled getting rid of the `rivescript/config` package and + making the public API more sane. +* Add more documentation and examples to the Go doc. +* Fix `@Redirects` not working sometimes when tags like `` insert capital + letters (bug #1) +* Fix an incorrect regexp that makes wildcards inside of optionals, like `[_]`, + not matchable in `` tags. For example, with `+ my favorite [_] is *` + and a message of "my favorite color is red", `` would be "red" because + the optional makes its wildcard non-capturing (bug #15) +* Fix the `` tag handling to support star numbers greater than ``: + you can use as many star numbers as will be captured by your trigger (bug #16) + ## v0.1.0 - Dec 11, 2016 This update changes some function prototypes in the API which breaks backward diff --git a/Makefile b/Makefile index 1f57248..f043c3c 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ fmt: # `make test` to run unit tests test: gopath - GOPATH=$(GOPATH) GO15VENDOREXPERIMENT=1 go test github.com/aichaos/rivescript-go/src + GOPATH=$(GOPATH) GO15VENDOREXPERIMENT=1 go test ./src # `make build` to build the binary build: gopath diff --git a/cmd/rivescript/main.go b/cmd/rivescript/main.go index 1bc9387..2f2f1af 100644 --- a/cmd/rivescript/main.go +++ b/cmd/rivescript/main.go @@ -24,7 +24,6 @@ import ( "strings" "github.com/aichaos/rivescript-go" - "github.com/aichaos/rivescript-go/config" "github.com/aichaos/rivescript-go/lang/javascript" ) @@ -51,7 +50,7 @@ func main() { root := args[0] // Initialize the bot. - bot := rivescript.New(&config.Config{ + bot := rivescript.New(&rivescript.Config{ Debug: *debug, Strict: !*nostrict, Depth: *depth, diff --git a/config.go b/config.go new file mode 100644 index 0000000..512fa30 --- /dev/null +++ b/config.go @@ -0,0 +1,45 @@ +package rivescript + +import "github.com/aichaos/rivescript-go/sessions" + +/* +Type Config provides options to configure the RiveScript bot. + +Create a pointer to this type and send it to the New() constructor to change +the default settings. You only need to provide settings you want to override; +the zero-values of all the options are handled appropriately by the RiveScript +library. + +The default values are documented below. +*/ +type Config struct { + // Debug enables verbose logging to standard output. Default false. + Debug bool + + // Strict enables strict syntax checking, where a syntax error in RiveScript + // code is considered fatal at parse time. Default true. + Strict bool + + // UTF8 enables UTF-8 mode within the bot. Default false. + // + // When UTF-8 mode is enabled, triggers in the RiveScript source files are + // allowed to contain foreign characters. Additionally, the user's incoming + // messages are left *mostly* intact, so that they send messages with + // foreign characters to the bot. + UTF8 bool + + // Depth controls the global limit for recursive functions within + // RiveScript. Default 50. + Depth uint + + // SessionManager is an implementation of the same name for managing user + // variables for the bot. The default is the in-memory session handler. + SessionManager sessions.SessionManager +} + +// WithUTF8 provides a Config object that enables UTF-8 mode. +func WithUTF8() *Config { + return &Config{ + UTF8: true, + } +} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 203bf8d..0000000 --- a/config/config.go +++ /dev/null @@ -1,54 +0,0 @@ -// Package config provides the RiveScript configuration type. -package config - -import ( - "github.com/aichaos/rivescript-go/sessions" - "github.com/aichaos/rivescript-go/sessions/memory" -) - -// Type Config configures a RiveScript instance. -type Config struct { - // Debug enables verbose debug logging to your standard output. - Debug bool - - // Strict enables strict syntax checking. - Strict bool - - // Depth sets the recursion depth limit. The zero value will default to - // 50 levels deep. - Depth uint - - // UTF8 enables UTF-8 support for user messages and triggers. - UTF8 bool - - // SessionManager chooses a session manager for user variables. - SessionManager sessions.SessionManager -} - -// Basic creates a default configuration: -// -// - Strict: true -// - Depth: 50 -// - UTF8: false -func Basic() *Config { - return &Config{ - Strict: true, - Depth: 50, - UTF8: false, - SessionManager: memory.New(), - } -} - -// UTF8 creates a default configuration with UTF-8 mode enabled. -// -// - Strict: true -// - Depth: 50 -// - UTF8: true -func UTF8() *Config { - return &Config{ - Strict: true, - Depth: 50, - UTF8: true, - SessionManager: memory.New(), - } -} diff --git a/deprecated.go b/deprecated.go new file mode 100644 index 0000000..5920c6d --- /dev/null +++ b/deprecated.go @@ -0,0 +1,67 @@ +package rivescript + +// deprecated.go is where functions that are deprecated move to. + +import ( + "fmt" + "os" +) + +// common function to put the deprecated note. +func deprecated(name, since string) { + fmt.Fprintf( + os.Stderr, + "Use of 'rivescript.%s()' is deprecated since v%s (this is v%s)\n", + name, + since, + VERSION, + ) +} + +// SetDebug enables or disable debug mode. +func (self *RiveScript) SetDebug(value bool) { + deprecated("SetDebug", "0.1.0") + self.rs.Debug = value +} + +// GetDebug tells you the current status of the debug mode. +func (self *RiveScript) GetDebug() bool { + deprecated("GetDebug", "0.1.0") + return self.rs.Debug +} + +// SetUTF8 enables or disabled UTF-8 mode. +func (self *RiveScript) SetUTF8(value bool) { + deprecated("SetUTF8", "0.1.0") + self.rs.UTF8 = value +} + +// GetUTF8 returns the current status of UTF-8 mode. +func (self *RiveScript) GetUTF8() bool { + deprecated("GetUTF8", "0.1.0") + return self.rs.UTF8 +} + +// SetDepth lets you override the recursion depth limit (default 50). +func (self *RiveScript) SetDepth(value uint) { + deprecated("SetDepth", "0.1.0") + self.rs.Depth = value +} + +// GetDepth returns the current recursion depth limit. +func (self *RiveScript) GetDepth() uint { + deprecated("GetDepth", "0.1.0") + return self.rs.Depth +} + +// SetStrict enables strict syntax checking when parsing RiveScript code. +func (self *RiveScript) SetStrict(value bool) { + deprecated("SetStrict", "0.1.0") + self.rs.Strict = value +} + +// GetStrict returns the strict syntax check setting. +func (self *RiveScript) GetStrict() bool { + deprecated("GetStrict", "0.1.0") + return self.rs.Strict +} diff --git a/doc_test.go b/doc_test.go index 7195007..8a4cb32 100644 --- a/doc_test.go +++ b/doc_test.go @@ -1,16 +1,28 @@ -package rivescript_test +package rivescript import ( "fmt" "github.com/aichaos/rivescript-go" - "github.com/aichaos/rivescript-go/config" "github.com/aichaos/rivescript-go/lang/javascript" rss "github.com/aichaos/rivescript-go/src" ) -func ExampleRiveScript() { - bot := rivescript.New(config.Basic()) +func Example() { + // Create a new RiveScript instance, which represents an individual bot + // with its own brain and memory of users. + // + // You can provide a rivescript.Config struct to configure the bot and + // provide values that differ from the defaults: + bot := rivescript.New(&rivescript.Config{ + UTF8: true, // enable UTF-8 mode + Debug: true, // enable debug mode + }) + + // Or if you want the default configuration, provide a nil config. + // See the documentation for the rivescript.Config type for information + // on what the defaults are. + bot = rivescript.New(nil) // Load a directory full of RiveScript documents (.rive files) bot.LoadDirectory("eg/brain") @@ -18,6 +30,12 @@ func ExampleRiveScript() { // Load an individual file. bot.LoadFile("testsuite.rive") + // Stream in more RiveScript code dynamically from a string. + bot.Stream(` + + hello bot + - Hello, human! + `) + // Sort the replies after loading them! bot.SortReplies() @@ -26,10 +44,30 @@ func ExampleRiveScript() { fmt.Printf("The bot says: %s", reply) } +func ExampleRiveScript_utf8() { + // Examples of using UTF-8 mode in RiveScript. + bot := rivescript.New(rivescript.WithUTF8()) + + bot.Stream(` + // Without UTF-8 mode enabled, this trigger would be a syntax error + // for containing non-ASCII characters; but in UTF-8 mode you can use it. + + comment ça va + - ça va bien. + `) + + // Always call SortReplies when you're done loading replies. + bot.SortReplies() + + // Without UTF-8 mode enabled, the user's message "comment ça va" would + // have the ç symbol removed; but in UTF-8 mode it's preserved and can + // match the trigger we defined. + reply := bot.Reply("local-user", "Comment ça va?") + fmt.Println(reply) // "ça va bien." +} + func ExampleRiveScript_javascript() { // Example for configuring the JavaScript object macro handler via Otto. - - bot := rivescript.New(config.Basic()) + bot := rivescript.New(nil) // Create the JS handler. bot.SetHandler("javascript", javascript.New(bot)) @@ -66,8 +104,7 @@ func ExampleRiveScript_javascript() { func ExampleRiveScript_subroutine() { // Example for defining a Go function as an object macro. // import rss "github.com/aichaos/rivescript-go/src" - - bot := rivescript.New(config.Basic()) + bot := rivescript.New(nil) // Define an object macro named `setname` bot.SetSubroutine("setname", func(rs *rss.RiveScript, args []string) string { diff --git a/rivescript.go b/rivescript.go index baa83eb..c82de26 100644 --- a/rivescript.go +++ b/rivescript.go @@ -13,89 +13,65 @@ package rivescript */ import ( - "fmt" - "os" - - "github.com/aichaos/rivescript-go/config" "github.com/aichaos/rivescript-go/macro" "github.com/aichaos/rivescript-go/sessions" + "github.com/aichaos/rivescript-go/sessions/memory" "github.com/aichaos/rivescript-go/src" ) -const VERSION string = "0.1.0" +// VERSION describes the module version. +const VERSION string = "0.1.1" +// RiveScript represents an individual chatbot instance. type RiveScript struct { rs *rivescript.RiveScript } -func New(config *config.Config) *RiveScript { - bot := new(RiveScript) - bot.rs = rivescript.New(config) - return bot -} +/* +New creates a new RiveScript instance. + +A RiveScript instance represents one chat bot personality; it has its own +replies and its own memory of user data. You could make multiple bots in the +same program, each with its own replies loaded from different sources. +*/ +func New(cfg *Config) *RiveScript { + bot := &RiveScript{ + rs: rivescript.New(), + } + + // If no config was given, default to the BasicConfig. + if cfg == nil { + cfg = &Config{ + Strict: true, + Depth: 50, + } + } + + // If no session manager configured, default to the in-memory one. + if cfg.SessionManager == nil { + cfg.SessionManager = memory.New() + } + + // Default depth if not given is 50. + if cfg.Depth <= 0 { + cfg.Depth = 50 + } + + bot.rs.Configure(cfg.Debug, cfg.Strict, cfg.UTF8, cfg.Depth, memory.New()) -func deprecated(name string) { - fmt.Fprintf(os.Stderr, "Use of 'rivescript.%s()' is deprecated\n", name) + return bot } // Version returns the RiveScript library version. -func (self *RiveScript) Version() string { +func (rs *RiveScript) Version() string { return VERSION } -// SetDebug enables or disable debug mode. -func (self *RiveScript) SetDebug(value bool) { - deprecated("SetDebug") - self.rs.Debug = value -} - -// GetDebug tells you the current status of the debug mode. -func (self *RiveScript) GetDebug() bool { - deprecated("GetDebug") - return self.rs.Debug -} - -// SetUTF8 enables or disabled UTF-8 mode. -func (self *RiveScript) SetUTF8(value bool) { - deprecated("SetUTF8") - self.rs.UTF8 = value -} - -// GetUTF8 returns the current status of UTF-8 mode. -func (self *RiveScript) GetUTF8() bool { - deprecated("GetUTF8") - return self.rs.UTF8 -} - // SetUnicodePunctuation allows you to override the text of the unicode // punctuation regexp. Provide a string literal that will validate in // `regexp.MustCompile()` -func (self *RiveScript) SetUnicodePunctuation(value string) { - self.rs.SetUnicodePunctuation(value) -} - -// SetDepth lets you override the recursion depth limit (default 50). -func (self *RiveScript) SetDepth(value uint) { - deprecated("SetDepth") - self.rs.Depth = value -} - -// GetDepth returns the current recursion depth limit. -func (self *RiveScript) GetDepth() uint { - deprecated("GetDepth") - return self.rs.Depth -} - -// SetStrict enables strict syntax checking when parsing RiveScript code. -func (self *RiveScript) SetStrict(value bool) { - deprecated("SetStrict") - self.rs.Strict = value -} - -// GetStrict returns the strict syntax check setting. -func (self *RiveScript) GetStrict() bool { - deprecated("GetStrict") - return self.rs.Strict +func (rs *RiveScript) SetUnicodePunctuation(value string) { + rs.rs.SetUnicodePunctuation(value) } //////////////////////////////////////////////////////////////////////////////// @@ -109,8 +85,8 @@ Parameters path: Path to a RiveScript source file. */ -func (self *RiveScript) LoadFile(path string) error { - return self.rs.LoadFile(path) +func (rs *RiveScript) LoadFile(path string) error { + return rs.rs.LoadFile(path) } /* @@ -122,8 +98,8 @@ Parameters extensions...: List of file extensions to filter on, default is '.rive' and '.rs' */ -func (self *RiveScript) LoadDirectory(path string, extensions ...string) error { - return self.rs.LoadDirectory(path, extensions...) +func (rs *RiveScript) LoadDirectory(path string, extensions ...string) error { + return rs.rs.LoadDirectory(path, extensions...) } /* @@ -134,8 +110,8 @@ Parameters code: Raw source code of a RiveScript document, with line breaks after each line. */ -func (self *RiveScript) Stream(code string) error { - return self.rs.Stream(code) +func (rs *RiveScript) Stream(code string) error { + return rs.rs.Stream(code) } /* @@ -145,8 +121,8 @@ After you have finished loading your RiveScript code, call this method to populate the various sort buffers. This is absolutely necessary for reply matching to work efficiently! */ -func (self *RiveScript) SortReplies() { - self.rs.SortReplies() +func (rs *RiveScript) SortReplies() { + rs.rs.SortReplies() } //////////////////////////////////////////////////////////////////////////////// @@ -161,8 +137,8 @@ Parameters lang: What your programming language is called, e.g. "javascript" handler: An implementation of macro.MacroInterface. */ -func (self *RiveScript) SetHandler(name string, handler macro.MacroInterface) { - self.rs.SetHandler(name, handler) +func (rs *RiveScript) SetHandler(name string, handler macro.MacroInterface) { + rs.rs.SetHandler(name, handler) } /* @@ -172,8 +148,8 @@ Parameters lang: The programming language for the handler to remove. */ -func (self *RiveScript) RemoveHandler(lang string) { - self.rs.RemoveHandler(lang) +func (rs *RiveScript) RemoveHandler(lang string) { + rs.rs.RemoveHandler(lang) } /* @@ -184,8 +160,8 @@ Parameters name: The name of your subroutine for the `` tag in RiveScript. fn: A function with a prototype `func(*RiveScript, []string) string` */ -func (self *RiveScript) SetSubroutine(name string, fn rivescript.Subroutine) { - self.rs.SetSubroutine(name, fn) +func (rs *RiveScript) SetSubroutine(name string, fn rivescript.Subroutine) { + rs.rs.SetSubroutine(name, fn) } /* @@ -195,8 +171,8 @@ Parameters name: The name of the object macro to be deleted. */ -func (self *RiveScript) DeleteSubroutine(name string) { - self.rs.DeleteSubroutine(name) +func (rs *RiveScript) DeleteSubroutine(name string) { + rs.rs.DeleteSubroutine(name) } /* @@ -205,8 +181,8 @@ SetGlobal sets a global variable. This is equivalent to `! global` in RiveScript. Set the value to `undefined` to delete a global. */ -func (self *RiveScript) SetGlobal(name, value string) { - self.rs.SetGlobal(name, value) +func (rs *RiveScript) SetGlobal(name, value string) { + rs.rs.SetGlobal(name, value) } /* @@ -215,8 +191,8 @@ GetGlobal gets a global variable. This is equivalent to `` in RiveScript. Returns `undefined` if the variable isn't defined. */ -func (self *RiveScript) GetGlobal(name string) (string, error) { - return self.rs.GetGlobal(name) +func (rs *RiveScript) GetGlobal(name string) (string, error) { + return rs.rs.GetGlobal(name) } /* @@ -225,8 +201,8 @@ SetVariable sets a bot variable. This is equivalent to `! var` in RiveScript. Set the value to `undefined` to delete a bot variable. */ -func (self *RiveScript) SetVariable(name, value string) { - self.rs.SetVariable(name, value) +func (rs *RiveScript) SetVariable(name, value string) { + rs.rs.SetVariable(name, value) } /* @@ -235,8 +211,8 @@ GetVariable gets a bot variable. This is equivalent to `` in RiveScript. Returns `undefined` if the variable isn't defined. */ -func (self *RiveScript) GetVariable(name string) (string, error) { - return self.rs.GetVariable(name) +func (rs *RiveScript) GetVariable(name string) (string, error) { + return rs.rs.GetVariable(name) } /* @@ -245,8 +221,8 @@ SetSubstitution sets a substitution pattern. This is equivalent to `! sub` in RiveScript. Set the value to `undefined` to delete a substitution. */ -func (self *RiveScript) SetSubstitution(name, value string) { - self.rs.SetSubstitution(name, value) +func (rs *RiveScript) SetSubstitution(name, value string) { + rs.rs.SetSubstitution(name, value) } /* @@ -255,8 +231,8 @@ SetPerson sets a person substitution pattern. This is equivalent to `! person` in RiveScript. Set the value to `undefined` to delete a person substitution. */ -func (self *RiveScript) SetPerson(name, value string) { - self.rs.SetPerson(name, value) +func (rs *RiveScript) SetPerson(name, value string) { + rs.rs.SetPerson(name, value) } /* @@ -265,8 +241,8 @@ SetUservar sets a variable for a user. This is equivalent to `` in RiveScript. Set the value to `undefined` to delete a substitution. */ -func (self *RiveScript) SetUservar(username, name, value string) { - self.rs.SetUservar(username, name, value) +func (rs *RiveScript) SetUservar(username, name, value string) { + rs.rs.SetUservar(username, name, value) } /* @@ -275,8 +251,8 @@ SetUservars sets a map of variables for a user. Set multiple user variables by providing a map[string]string of key/value pairs. Equivalent to calling `SetUservar()` for each pair in the map. */ -func (self *RiveScript) SetUservars(username string, data map[string]string) { - self.rs.SetUservars(username, data) +func (rs *RiveScript) SetUservars(username string, data map[string]string) { + rs.rs.SetUservars(username, data) } /* @@ -285,8 +261,8 @@ GetUservar gets a user variable. This is equivalent to `` in RiveScript. Returns `undefined` if the variable isn't defined. */ -func (self *RiveScript) GetUservar(username, name string) (string, error) { - return self.rs.GetUservar(username, name) +func (rs *RiveScript) GetUservar(username, name string) (string, error) { + return rs.rs.GetUservar(username, name) } /* @@ -294,8 +270,8 @@ GetUservars gets all the variables for a user. This returns a `map[string]string` containing all the user's variables. */ -func (self *RiveScript) GetUservars(username string) (*sessions.UserData, error) { - return self.rs.GetUservars(username) +func (rs *RiveScript) GetUservars(username string) (*sessions.UserData, error) { + return rs.rs.GetUservars(username) } /* @@ -304,18 +280,18 @@ GetAllUservars gets all the variables for all the users. This returns a map of username (strings) to `map[string]string` of their variables. */ -func (self *RiveScript) GetAllUservars() map[string]*sessions.UserData { - return self.rs.GetAllUservars() +func (rs *RiveScript) GetAllUservars() map[string]*sessions.UserData { + return rs.rs.GetAllUservars() } // ClearAllUservars clears all variables for all users. -func (self *RiveScript) ClearAllUservars() { - self.rs.ClearAllUservars() +func (rs *RiveScript) ClearAllUservars() { + rs.rs.ClearAllUservars() } // ClearUservars clears all a user's variables. -func (self *RiveScript) ClearUservars(username string) { - self.rs.ClearUservars(username) +func (rs *RiveScript) ClearUservars(username string) { + rs.rs.ClearUservars(username) } /* @@ -324,8 +300,8 @@ FreezeUservars freezes the variable state of a user. This will clone and preserve the user's entire variable state, so that it can be restored later with `ThawUservars()`. */ -func (self *RiveScript) FreezeUservars(username string) error { - return self.rs.FreezeUservars(username) +func (rs *RiveScript) FreezeUservars(username string) error { + return rs.rs.FreezeUservars(username) } /* @@ -336,13 +312,13 @@ The `action` can be one of the following: * discard: Don't restore the variables, just delete the frozen copy. * keep: Keep the frozen copy after restoring. */ -func (self *RiveScript) ThawUservars(username string, action sessions.ThawAction) error { - return self.rs.ThawUservars(username, action) +func (rs *RiveScript) ThawUservars(username string, action sessions.ThawAction) error { + return rs.rs.ThawUservars(username, action) } // LastMatch returns the user's last matched trigger. -func (self *RiveScript) LastMatch(username string) (string, error) { - return self.rs.LastMatch(username) +func (rs *RiveScript) LastMatch(username string) (string, error) { + return rs.rs.LastMatch(username) } /* @@ -352,8 +328,8 @@ This is only useful from within an object macro, to get the ID of the user who invoked the macro. This value is set at the beginning of `Reply()` and unset at the end, so this function will return empty outside of a reply context. */ -func (self *RiveScript) CurrentUser() string { - return self.rs.CurrentUser() +func (rs *RiveScript) CurrentUser() string { + return rs.rs.CurrentUser() } //////////////////////////////////////////////////////////////////////////////// @@ -368,8 +344,8 @@ Parameters username: The name of the user requesting a reply. message: The user's message. */ -func (self *RiveScript) Reply(username, message string) string { - return self.rs.Reply(username, message) +func (rs *RiveScript) Reply(username, message string) string { + return rs.rs.Reply(username, message) } //////////////////////////////////////////////////////////////////////////////// @@ -377,11 +353,11 @@ func (self *RiveScript) Reply(username, message string) string { //////////////////////////////////////////////////////////////////////////////// // DumpSorted is a debug method which dumps the sort tree from the bot's memory. -func (self *RiveScript) DumpSorted() { - self.rs.DumpSorted() +func (rs *RiveScript) DumpSorted() { + rs.rs.DumpSorted() } // DumpTopics is a debug method which dumps the topic structure from the bot's memory. -func (self *RiveScript) DumpTopics() { - self.rs.DumpTopics() +func (rs *RiveScript) DumpTopics() { + rs.rs.DumpTopics() } diff --git a/src/base_test.go b/src/base_test.go index 108aeac..662aff5 100644 --- a/src/base_test.go +++ b/src/base_test.go @@ -1,4 +1,4 @@ -package rivescript_test +package rivescript // NOTE: while these test files live in the 'src' package, they import the // public facing API from the root rivescript-go package. @@ -7,34 +7,38 @@ import ( "fmt" "testing" - "github.com/aichaos/rivescript-go" - "github.com/aichaos/rivescript-go/config" + "github.com/aichaos/rivescript-go/sessions" + "github.com/aichaos/rivescript-go/sessions/memory" ) type RiveScriptTest struct { - bot *rivescript.RiveScript + bot *RiveScript t *testing.T username string } func NewTest(t *testing.T) *RiveScriptTest { - return &RiveScriptTest{ - bot: rivescript.New(config.Basic()), - t: t, - username: "soandso", - } + return NewTestWithConfig(t, false, false, memory.New()) +} + +func NewTestWithUTF8(t *testing.T) *RiveScriptTest { + return NewTestWithConfig(t, false, true, memory.New()) } -func NewTestWithConfig(t *testing.T, config *config.Config) *RiveScriptTest { - return &RiveScriptTest{ - bot: rivescript.New(config), +func NewTestWithConfig(t *testing.T, debug, utf8 bool, ses sessions.SessionManager) *RiveScriptTest { + test := &RiveScriptTest{ + bot: New(), t: t, username: "soandso", } + test.bot.Debug = debug + test.bot.UTF8 = utf8 + test.bot.sessions = ses + return test } // RS exposes the underlying RiveScript API. -func (rst *RiveScriptTest) RS() *rivescript.RiveScript { +func (rst *RiveScriptTest) RS() *RiveScript { return rst.bot } diff --git a/src/begin_test.go b/src/begin_test.go new file mode 100644 index 0000000..1c26126 --- /dev/null +++ b/src/begin_test.go @@ -0,0 +1,50 @@ +package rivescript + +import "testing" + +func TestNoBeginBlock(t *testing.T) { + bot := NewTest(t) + bot.extend(` + + hello bot + - Hello human. + `) + bot.reply("Hello bot", "Hello human.") +} + +func TestSimpleBeginBlock(t *testing.T) { + bot := NewTest(t) + bot.extend(` + > begin + + request + - {ok} + < begin + + + hello bot + - Hello human. + `) + bot.reply("Hello bot", "Hello human.") +} + +func TestConditionalBeginBlock(t *testing.T) { + bot := NewTest(t) + bot.extend(` + > begin + + request + * == undefined => {ok} + * != undefined => : {ok} + - {ok} + < begin + + + hello bot + - Hello human. + + + my name is * + - >Hello, . + `) + bot.reply("Hello bot", "Hello human.") + bot.uservar("met", "true") + bot.uservar("name", "undefined") + bot.reply("My name is bob", "Hello, Bob.") + bot.uservar("name", "Bob") + bot.reply("Hello Bot", "Bob: Hello human.") +} diff --git a/src/bot_variable_test.go b/src/bot_variable_test.go new file mode 100644 index 0000000..e389276 --- /dev/null +++ b/src/bot_variable_test.go @@ -0,0 +1,48 @@ +package rivescript + +import "testing" + +//////////////////////////////////////////////////////////////////////////////// +// Bot Variable Tests +//////////////////////////////////////////////////////////////////////////////// + +func TestBotVariables(t *testing.T) { + bot := NewTest(t) + bot.extend(` + ! var name = Aiden + ! var age = 5 + + + what is your name + - My name is . + + + how old are you + - I am . + + + what are you + - I'm . + + + happy birthday + - Thanks! + `) + bot.reply("What is your name?", "My name is Aiden.") + bot.reply("How old are you?", "I am 5.") + bot.reply("What are you?", "I'm undefined.") + bot.reply("Happy birthday!", "Thanks!") + bot.reply("How old are you?", "I am 6.") +} + +func TestGlobalVariables(t *testing.T) { + bot := NewTest(t) + bot.extend(` + ! global debug = false + + + debug mode + - Debug mode is: + + + set debug mode * + - >Switched to . + `) + bot.reply("Debug mode.", "Debug mode is: false") + bot.reply("Set debug mode true", "Switched to true.") + bot.reply("Debug mode?", "Debug mode is: true") +} diff --git a/src/brain.go b/src/brain.go index 968a7c7..1c61d29 100644 --- a/src/brain.go +++ b/src/brain.go @@ -10,7 +10,7 @@ import ( // Brain logic for RiveScript -func (rs *RiveScript) Reply(username string, message string) string { +func (rs *RiveScript) Reply(username, message string) string { rs.say("Asked to reply to [%s] %s", username, message) // Initialize a user profile for this user? @@ -243,6 +243,7 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st rs.say("Redirecting us to %s", matched.redirect) redirect := matched.redirect redirect = rs.processTags(username, message, redirect, stars, thatStars, 0) + redirect = strings.ToLower(redirect) rs.say("Pretend user said: %s", redirect) reply = rs.getReply(username, redirect, isBegin, step+1) break diff --git a/src/options_test.go b/src/options_test.go new file mode 100644 index 0000000..8a1cf21 --- /dev/null +++ b/src/options_test.go @@ -0,0 +1,52 @@ +package rivescript + +import "testing" + +func TestConcat(t *testing.T) { + bot := NewTest(t) + bot.extend(` + // Default concat mode = none + + test concat default + - Hello + ^ world! + + ! local concat = space + + test concat space + - Hello + ^ world! + + ! local concat = none + + test concat none + - Hello + ^ world! + + ! local concat = newline + + test concat newline + - Hello + ^ world! + + // invalid concat setting is equivalent to 'none' + ! local concat = foobar + + test concat foobar + - Hello + ^ world! + + // the option is file scoped so it can be left at + // any setting and won't affect subsequent parses + ! local concat = newline + `) + bot.extend(` + // concat mode should be restored to the default in a + // separate file/stream parse + + test concat second file + - Hello + ^ world! + `) + + bot.reply("test concat default", "Helloworld!") + bot.reply("test concat space", "Hello world!") + bot.reply("test concat none", "Helloworld!") + bot.reply("test concat newline", "Hello\nworld!") + bot.reply("test concat foobar", "Helloworld!") + bot.reply("test concat second file", "Helloworld!") +} diff --git a/src/reply_test.go b/src/reply_test.go new file mode 100644 index 0000000..b1892ec --- /dev/null +++ b/src/reply_test.go @@ -0,0 +1,187 @@ +package rivescript + +import ( + "fmt" + "testing" +) + +func TestPrevious(t *testing.T) { + bot := NewTest(t) + bot.extend(` + ! sub who's = who is + ! sub it's = it is + ! sub didn't = did not + + + knock knock + - Who's there? + + + * + % who is there + - who? + + + * + % * who + - Haha! ! + + + * + - I don't know. + `) + bot.reply("knock knock", "Who's there?") + bot.reply("Canoe", "Canoe who?") + bot.reply("Canoe help me with my homework?", "Haha! Canoe help me with my homework!") + bot.reply("hello", "I don't know.") +} + +func TestContinuations(t *testing.T) { + bot := NewTest(t) + bot.extend(` + + tell me a poem + - There once was a man named Tim,\s + ^ who never quite learned how to swim.\s + ^ He fell off a dock, and sank like a rock,\s + ^ and that was the end of him. + `) + bot.reply("Tell me a poem.", "There once was a man named Tim, who never quite learned how to swim. He fell off a dock, and sank like a rock, and that was the end of him.") +} + +// Test matching a large number of stars, greater than - +func TestManyStars(t *testing.T) { + bot := NewTest(t) + bot.extend(` + // 10 stars. + + * * * * * * * * * * + - That's a long one. 1=; 5=; 9=; 10=; + + // 16 stars! + + * * * * * * * * * * * * * * * * + - Wow! 1=; 3=; 7=; 14=; 15=; 16=; + `) + bot.reply( + "one two three four five six seven eight nine ten eleven", + "That's a long one. 1=one; 5=five; 9=nine; 10=ten eleven;", + ) + bot.reply( + "0 1 2 3 4 5 6 7 8 9 A B C D E F G H I J K", + "Wow! 1=0; 3=2; 7=6; 14=d; 15=e; 16=f g h i j k;", + ) +} + +func TestRedirects(t *testing.T) { + bot := NewTest(t) + bot.extend(` + + hello + - Hi there! + + + hey + @ hello + + + hi there + - {@hello} + + // Variables can throw off redirects with their capitalizations, + // so make sure redirects handle this properly. + ! var master = Kirsle + + + my name is () + - >That's my botmaster's name too. + + + call me + @ my name is + `) + bot.reply("hello", "Hi there!") + bot.reply("hey", "Hi there!") + bot.reply("hi there", "Hi there!") + + bot.reply("my name is Kirsle", "That's my botmaster's name too.") + bot.reply("call me kirsle", "That's my botmaster's name too.") +} + +func TestConditionals(t *testing.T) { + bot := NewTest(t) + bot.extend(` + + i am # years old + - >OK. + + + what can i do + * == undefined => I don't know. + * > 25 => Anything you want. + * == 25 => Rent a car for cheap. + * >= 21 => Drink. + * >= 18 => Vote. + * < 18 => Not much of anything. + + + am i your master + * == true => Yes. + - No. + `) + age_q := "What can I do?" + bot.reply(age_q, "I don't know.") + + ages := map[string]string{ + "16": "Not much of anything.", + "18": "Vote.", + "20": "Vote.", + "22": "Drink.", + "24": "Drink.", + "25": "Rent a car for cheap.", + "27": "Anything you want.", + } + for age, expect := range ages { + bot.reply(fmt.Sprintf("I am %s years old.", age), "OK.") + bot.reply(age_q, expect) + } + + bot.reply("Am I your master?", "No.") + bot.bot.SetUservar(bot.username, "master", "true") + bot.reply("Am I your master?", "Yes.") +} + +func TestEmbeddedTags(t *testing.T) { + bot := NewTest(t) + bot.extend(` + + my name is * + * != undefined => >I thought\s + ^ your name was ? + ^ > + - >OK. + + + what is my name + - Your name is , right? + + + html test + - Name>This has some non-RS tags in it. + `) + bot.reply("What is my name?", "Your name is undefined, right?") + bot.reply("My name is Alice.", "OK.") + bot.reply("My name is Bob.", "I thought your name was Alice?") + bot.reply("What is my name?", "Your name is Bob, right?") + bot.reply("HTML Test", "This has some non-RS tags in it.") +} + +func TestSetUservars(t *testing.T) { + bot := NewTest(t) + bot.extend(` + + what is my name + - Your name is . + + + how old am i + - You are . + `) + bot.bot.SetUservars(bot.username, map[string]string{ + "name": "Aiden", + "age": "5", + }) + bot.reply("What is my name?", "Your name is Aiden.") + bot.reply("How old am I?", "You are 5.") +} + +func TestQuestionmark(t *testing.T) { + bot := NewTest(t) + bot.extend(` + + google * + - Results are here + `) + bot.reply("google golang", + `Results are here`, + ) +} diff --git a/src/rivescript.go b/src/rivescript.go index b67b117..71aae27 100644 --- a/src/rivescript.go +++ b/src/rivescript.go @@ -22,7 +22,6 @@ import ( "regexp" "sync" - "github.com/aichaos/rivescript-go/config" "github.com/aichaos/rivescript-go/macro" "github.com/aichaos/rivescript-go/parser" "github.com/aichaos/rivescript-go/sessions" @@ -65,27 +64,13 @@ type RiveScript struct { * Constructor and Debug Methods * ******************************************************************************/ -func New(cfg *config.Config) *RiveScript { +func New() *RiveScript { rs := new(RiveScript) - if cfg == nil { - cfg = config.Basic() - } - - if cfg.SessionManager == nil { - rs.say("No SessionManager config: using default MemoryStore") - cfg.SessionManager = memory.New() - } - - if cfg.Depth <= 0 { - rs.say("No depth config: using default 50") - cfg.Depth = 50 - } - - rs.Debug = cfg.Debug - rs.Strict = cfg.Strict - rs.UTF8 = cfg.UTF8 - rs.Depth = cfg.Depth - rs.sessions = cfg.SessionManager + + // 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(`[.,!?;:]`) @@ -115,6 +100,17 @@ func New(cfg *config.Config) *RiveScript { return rs } +// Configure is a convenience function for the public API to set all of its +// settings at once. +func (rs *RiveScript) Configure(debug, strict, utf8 bool, depth uint, + sessions sessions.SessionManager) { + rs.Debug = debug + rs.Strict = strict + rs.UTF8 = utf8 + rs.Depth = depth + rs.sessions = sessions +} + func (rs *RiveScript) SetDebug(value bool) { rs.Debug = value } diff --git a/src/rivescript_test.go b/src/rivescript_test.go deleted file mode 100644 index 6f2a84e..0000000 --- a/src/rivescript_test.go +++ /dev/null @@ -1,677 +0,0 @@ -package rivescript_test - -import ( - "fmt" - "testing" -) - -//////////////////////////////////////////////////////////////////////////////// -// BEGIN Block Tests -//////////////////////////////////////////////////////////////////////////////// - -func TestNoBeginBlock(t *testing.T) { - bot := NewTest(t) - bot.extend(` - + hello bot - - Hello human. - `) - bot.reply("Hello bot", "Hello human.") -} - -func TestSimpleBeginBlock(t *testing.T) { - bot := NewTest(t) - bot.extend(` - > begin - + request - - {ok} - < begin - - + hello bot - - Hello human. - `) - bot.reply("Hello bot", "Hello human.") -} - -func TestConditionalBeginBlock(t *testing.T) { - bot := NewTest(t) - bot.extend(` - > begin - + request - * == undefined => {ok} - * != undefined => : {ok} - - {ok} - < begin - - + hello bot - - Hello human. - - + my name is * - - >Hello, . - `) - bot.reply("Hello bot", "Hello human.") - bot.uservar("met", "true") - bot.uservar("name", "undefined") - bot.reply("My name is bob", "Hello, Bob.") - bot.uservar("name", "Bob") - bot.reply("Hello Bot", "Bob: Hello human.") -} - -//////////////////////////////////////////////////////////////////////////////// -// Bot Variable Tests -//////////////////////////////////////////////////////////////////////////////// - -func TestBotVariables(t *testing.T) { - bot := NewTest(t) - bot.extend(` - ! var name = Aiden - ! var age = 5 - - + what is your name - - My name is . - - + how old are you - - I am . - - + what are you - - I'm . - - + happy birthday - - Thanks! - `) - bot.reply("What is your name?", "My name is Aiden.") - bot.reply("How old are you?", "I am 5.") - bot.reply("What are you?", "I'm undefined.") - bot.reply("Happy birthday!", "Thanks!") - bot.reply("How old are you?", "I am 6.") -} - -func TestGlobalVariables(t *testing.T) { - bot := NewTest(t) - bot.extend(` - ! global debug = false - - + debug mode - - Debug mode is: - - + set debug mode * - - >Switched to . - `) - bot.reply("Debug mode.", "Debug mode is: false") - bot.reply("Set debug mode true", "Switched to true.") - bot.reply("Debug mode?", "Debug mode is: true") -} - -//////////////////////////////////////////////////////////////////////////////// -// Substitution Tests -//////////////////////////////////////////////////////////////////////////////// - -func TestSubstitutions(t *testing.T) { - bot := NewTest(t) - bot.extend(` - + whats up - - nm. - - + what is up - - Not much. - `) - bot.reply("whats up", "nm.") - bot.reply("what's up?", "nm.") - bot.reply("what is up?", "Not much.") - - bot.extend(` - ! sub whats = what is - ! sub what's = what is - `) - bot.reply("whats up", "Not much.") - bot.reply("what's up?", "Not much.") - bot.reply("What is up?", "Not much.") -} - -func TestPersonSubstitutions(t *testing.T) { - bot := NewTest(t) - bot.extend(` - + say * - - - `) - bot.reply("say I am cool", "i am cool") - bot.reply("say You are dumb", "you are dumb") - - bot.extend(` - ! person i am = you are - ! person you are = I am - `) - bot.reply("say I am cool", "you are cool") - bot.reply("say You are dumb", "I am dumb") -} - -//////////////////////////////////////////////////////////////////////////////// -// Trigger Tests -//////////////////////////////////////////////////////////////////////////////// - -func TestAtomicTriggers(t *testing.T) { - bot := NewTest(t) - bot.extend(` - + hello bot - - Hello human. - - + what are you - - I am a RiveScript bot. - `) - bot.reply("Hello bot", "Hello human.") - bot.reply("What are you?", "I am a RiveScript bot.") -} - -func TestWildcardTriggers(t *testing.T) { - bot := NewTest(t) - bot.extend(` - + my name is * - - Nice to meet you, . - - + * told me to say * - - Why did tell you to say ? - - + i am # years old - - A lot of people are . - - + i am _ years old - - Say that with numbers. - - + i am * years old - - Say that with fewer words. - `) - bot.reply("my name is Bob", "Nice to meet you, bob.") - bot.reply("bob told me to say hi", "Why did bob tell you to say hi?") - bot.reply("i am 5 years old", "A lot of people are 5.") - bot.reply("i am five years old", "Say that with numbers.") - bot.reply("i am twenty five years old", "Say that with fewer words.") -} - -func TestAlternativesAndOptionals(t *testing.T) { - bot := NewTest(t) - bot.extend(` - + what (are|is) you - - I am a robot. - - + what is your (home|office|cell) [phone] number - - It is 555-1234. - - + [please|can you] ask me a question - - Why is the sky blue? - - + (aa|bb|cc) [bogus] - - Matched. - - + (yo|hi) [computer|bot] * - - Matched. - `) - bot.reply("What are you?", "I am a robot.") - bot.reply("What is you?", "I am a robot.") - - bot.reply("What is your home phone number?", "It is 555-1234.") - bot.reply("What is your home number?", "It is 555-1234.") - bot.reply("What is your cell phone number?", "It is 555-1234.") - bot.reply("What is your office number?", "It is 555-1234.") - - bot.reply("Can you ask me a question?", "Why is the sky blue?") - bot.reply("Please ask me a question?", "Why is the sky blue?") - bot.reply("Ask me a question.", "Why is the sky blue?") - - bot.reply("aa", "Matched.") - bot.reply("bb", "Matched.") - bot.reply("aa bogus", "Matched.") - bot.reply("aabogus", "ERR: No Reply Matched") - bot.reply("bogus", "ERR: No Reply Matched") - - bot.reply("hi Aiden", "Matched.") - bot.reply("hi bot how are you?", "Matched.") - bot.reply("yo computer what time is it?", "Matched.") - bot.reply("yoghurt is yummy", "ERR: No Reply Matched") - bot.reply("hide and seek is fun", "ERR: No Reply Matched") - bot.reply("hip hip hurrah", "ERR: No Reply Matched") -} - -func TestTriggerArrays(t *testing.T) { - bot := NewTest(t) - bot.extend(` - ! array colors = red blue green yellow white - ^ dark blue|light blue - - + what color is my (@colors) * - - Your is . - - + what color was * (@colors) * - - It was . - - + i have a @colors * - - Tell me more about your . - `) - bot.reply("What color is my red shirt?", "Your shirt is red.") - bot.reply("What color is my blue car?", "Your car is blue.") - bot.reply("What color is my pink house?", "ERR: No Reply Matched") - bot.reply("What color is my dark blue jacket?", "Your jacket is dark blue.") - bot.reply("What color was Napoleoan's white horse?", "It was white.") - bot.reply("What color was my red shirt?", "It was red.") - bot.reply("I have a blue car.", "Tell me more about your car.") - bot.reply("I have a cyan car.", "ERR: No Reply Matched") -} - -func TestWeightedTriggers(t *testing.T) { - bot := NewTest(t) - bot.extend(` - + * or something{weight=10} - - Or something. <@> - - + can you run a google search for * - - Sure! - - + hello *{weight=20} - - Hi there! - `) - bot.reply("Hello robot.", "Hi there!") - bot.reply("Hello or something.", "Hi there!") - bot.reply("Can you run a Google search for Node", "Sure!") - bot.reply("Can you run a Google search for Node or something", "Or something. Sure!") -} - -//////////////////////////////////////////////////////////////////////////////// -// Reply Tests -//////////////////////////////////////////////////////////////////////////////// - -func TestPrevious(t *testing.T) { - bot := NewTest(t) - bot.extend(` - ! sub who's = who is - ! sub it's = it is - ! sub didn't = did not - - + knock knock - - Who's there? - - + * - % who is there - - who? - - + * - % * who - - Haha! ! - - + * - - I don't know. - `) - bot.reply("knock knock", "Who's there?") - bot.reply("Canoe", "Canoe who?") - bot.reply("Canoe help me with my homework?", "Haha! Canoe help me with my homework!") - bot.reply("hello", "I don't know.") -} - -func TestContinuations(t *testing.T) { - bot := NewTest(t) - bot.extend(` - + tell me a poem - - There once was a man named Tim,\s - ^ who never quite learned how to swim.\s - ^ He fell off a dock, and sank like a rock,\s - ^ and that was the end of him. - `) - bot.reply("Tell me a poem.", "There once was a man named Tim, who never quite learned how to swim. He fell off a dock, and sank like a rock, and that was the end of him.") -} - -func TestRedirects(t *testing.T) { - bot := NewTest(t) - bot.extend(` - + hello - - Hi there! - - + hey - @ hello - - + hi there - - {@hello} - `) - bot.reply("hello", "Hi there!") - bot.reply("hey", "Hi there!") - bot.reply("hi there", "Hi there!") -} - -func TestConditionals(t *testing.T) { - bot := NewTest(t) - bot.extend(` - + i am # years old - - >OK. - - + what can i do - * == undefined => I don't know. - * > 25 => Anything you want. - * == 25 => Rent a car for cheap. - * >= 21 => Drink. - * >= 18 => Vote. - * < 18 => Not much of anything. - - + am i your master - * == true => Yes. - - No. - `) - age_q := "What can I do?" - bot.reply(age_q, "I don't know.") - - ages := map[string]string{ - "16": "Not much of anything.", - "18": "Vote.", - "20": "Vote.", - "22": "Drink.", - "24": "Drink.", - "25": "Rent a car for cheap.", - "27": "Anything you want.", - } - for age, expect := range ages { - bot.reply(fmt.Sprintf("I am %s years old.", age), "OK.") - bot.reply(age_q, expect) - } - - bot.reply("Am I your master?", "No.") - bot.bot.SetUservar(bot.username, "master", "true") - bot.reply("Am I your master?", "Yes.") -} - -func TestEmbeddedTags(t *testing.T) { - bot := NewTest(t) - bot.extend(` - + my name is * - * != undefined => >I thought\s - ^ your name was ? - ^ > - - >OK. - - + what is my name - - Your name is , right? - - + html test - - Name>This has some non-RS tags in it. - `) - bot.reply("What is my name?", "Your name is undefined, right?") - bot.reply("My name is Alice.", "OK.") - bot.reply("My name is Bob.", "I thought your name was Alice?") - bot.reply("What is my name?", "Your name is Bob, right?") - bot.reply("HTML Test", "This has some non-RS tags in it.") -} - -func TestSetUservars(t *testing.T) { - bot := NewTest(t) - bot.extend(` - + what is my name - - Your name is . - - + how old am i - - You are . - `) - bot.bot.SetUservars(bot.username, map[string]string{ - "name": "Aiden", - "age": "5", - }) - bot.reply("What is my name?", "Your name is Aiden.") - bot.reply("How old am I?", "You are 5.") -} - -func TestQuestionmark(t *testing.T) { - bot := NewTest(t) - bot.extend(` - + google * - - Results are here - `) - bot.reply("google golang", - `Results are here`, - ) -} - -//////////////////////////////////////////////////////////////////////////////// -// Object Macro Tests -//////////////////////////////////////////////////////////////////////////////// - -// TODO - -//////////////////////////////////////////////////////////////////////////////// -// Topic Tests -//////////////////////////////////////////////////////////////////////////////// - -func TestPunishmentTopic(t *testing.T) { - bot := NewTest(t) - bot.extend(` - + hello - - Hi there! - - + swear word - - How rude! Apologize or I won't talk to you again.{topic=sorry} - - + * - - Catch-all. - - > topic sorry - + sorry - - It's ok!{topic=random} - - + * - - Say you're sorry! - < topic - `) - bot.reply("hello", "Hi there!") - bot.reply("How are you?", "Catch-all.") - bot.reply("Swear word!", "How rude! Apologize or I won't talk to you again.") - bot.reply("hello", "Say you're sorry!") - bot.reply("How are you?", "Say you're sorry!") - bot.reply("Sorry!", "It's ok!") - bot.reply("hello", "Hi there!") - bot.reply("How are you?", "Catch-all.") -} - -func TestTopicInheritance(t *testing.T) { - bot := NewTest(t) - RS_ERR_MATCH := "ERR: No Reply Matched" - bot.extend(` - > topic colors - + what color is the sky - - Blue. - + what color is the sun - - Yellow. - < topic - - > topic linux - + name a red hat distro - - Fedora. - + name a debian distro - - Ubuntu. - < topic - - > topic stuff includes colors linux - + say stuff - - "Stuff." - < topic - - > topic override inherits colors - + what color is the sun - - Purple. - < topic - - > topic morecolors includes colors - + what color is grass - - Green. - < topic - - > topic evenmore inherits morecolors - + what color is grass - - Blue, sometimes. - < topic - `) - bot.bot.SetUservar(bot.username, "topic", "colors") - bot.reply("What color is the sky?", "Blue.") - bot.reply("What color is the sun?", "Yellow.") - bot.reply("What color is grass?", RS_ERR_MATCH) - bot.reply("Name a Red Hat distro.", RS_ERR_MATCH) - bot.reply("Name a Debian distro.", RS_ERR_MATCH) - bot.reply("Say stuff.", RS_ERR_MATCH) - - bot.bot.SetUservar(bot.username, "topic", "linux") - bot.reply("What color is the sky?", RS_ERR_MATCH) - bot.reply("What color is the sun?", RS_ERR_MATCH) - bot.reply("What color is grass?", RS_ERR_MATCH) - bot.reply("Name a Red Hat distro.", "Fedora.") - bot.reply("Name a Debian distro.", "Ubuntu.") - bot.reply("Say stuff.", RS_ERR_MATCH) - - bot.bot.SetUservar(bot.username, "topic", "stuff") - bot.reply("What color is the sky?", "Blue.") - bot.reply("What color is the sun?", "Yellow.") - bot.reply("What color is grass?", RS_ERR_MATCH) - bot.reply("Name a Red Hat distro.", "Fedora.") - bot.reply("Name a Debian distro.", "Ubuntu.") - bot.reply("Say stuff.", `"Stuff."`) - - bot.bot.SetUservar(bot.username, "topic", "override") - bot.reply("What color is the sky?", "Blue.") - bot.reply("What color is the sun?", "Purple.") - bot.reply("What color is grass?", RS_ERR_MATCH) - bot.reply("Name a Red Hat distro.", RS_ERR_MATCH) - bot.reply("Name a Debian distro.", RS_ERR_MATCH) - bot.reply("Say stuff.", RS_ERR_MATCH) - - bot.bot.SetUservar(bot.username, "topic", "morecolors") - bot.reply("What color is the sky?", "Blue.") - bot.reply("What color is the sun?", "Yellow.") - bot.reply("What color is grass?", "Green.") - bot.reply("Name a Red Hat distro.", RS_ERR_MATCH) - bot.reply("Name a Debian distro.", RS_ERR_MATCH) - bot.reply("Say stuff.", RS_ERR_MATCH) - - bot.bot.SetUservar(bot.username, "topic", "evenmore") - bot.reply("What color is the sky?", "Blue.") - bot.reply("What color is the sun?", "Yellow.") - bot.reply("What color is grass?", "Blue, sometimes.") - bot.reply("Name a Red Hat distro.", RS_ERR_MATCH) - bot.reply("Name a Debian distro.", RS_ERR_MATCH) - bot.reply("Say stuff.", RS_ERR_MATCH) -} - -//////////////////////////////////////////////////////////////////////////////// -// Parser Option Tests -//////////////////////////////////////////////////////////////////////////////// - -func TestConcat(t *testing.T) { - bot := NewTest(t) - bot.extend(` - // Default concat mode = none - + test concat default - - Hello - ^ world! - - ! local concat = space - + test concat space - - Hello - ^ world! - - ! local concat = none - + test concat none - - Hello - ^ world! - - ! local concat = newline - + test concat newline - - Hello - ^ world! - - // invalid concat setting is equivalent to 'none' - ! local concat = foobar - + test concat foobar - - Hello - ^ world! - - // the option is file scoped so it can be left at - // any setting and won't affect subsequent parses - ! local concat = newline - `) - bot.extend(` - // concat mode should be restored to the default in a - // separate file/stream parse - + test concat second file - - Hello - ^ world! - `) - - bot.reply("test concat default", "Helloworld!") - bot.reply("test concat space", "Hello world!") - bot.reply("test concat none", "Helloworld!") - bot.reply("test concat newline", "Hello\nworld!") - bot.reply("test concat foobar", "Helloworld!") - bot.reply("test concat second file", "Helloworld!") -} - -//////////////////////////////////////////////////////////////////////////////// -// Unicode Tests -//////////////////////////////////////////////////////////////////////////////// - -func TestUnicode(t *testing.T) { - bot := NewTest(t) - bot.bot.SetUTF8(true) - bot.extend(` - ! sub who's = who is - + äh - - What's the matter? - - + ブラッキー - - エーフィ - - // Make sure %Previous continues working in UTF-8 mode. - + knock knock - - Who's there? - - + * - % who is there - - who? - - + * - % * who - - Haha! ! - - // And with UTF-8. - + tëll më ä pöëm - - Thërë öncë wäs ä män nämëd Tïm - - + more - % thërë öncë wäs ä män nämëd tïm - - Whö nëvër qüïtë lëärnëd höw tö swïm - - + more - % whö nëvër qüïtë lëärnëd höw tö swïm - - Hë fëll öff ä döck, änd sänk lïkë ä röck - - + more - % hë fëll öff ä döck änd sänk lïkë ä röck - - Änd thät wäs thë ënd öf hïm. - `) - - bot.reply("äh", "What's the matter?") - bot.reply("ブラッキー", "エーフィ") - bot.reply("knock knock", "Who's there?") - bot.reply("Orange", "Orange who?") - bot.reply("banana", "Haha! Banana!") - bot.reply("tëll më ä pöëm", "Thërë öncë wäs ä män nämëd Tïm") - bot.reply("more", "Whö nëvër qüïtë lëärnëd höw tö swïm") - bot.reply("more", "Hë fëll öff ä döck, änd sänk lïkë ä röck") - bot.reply("more", "Änd thät wäs thë ënd öf hïm.") -} - -func TestPunctuation(t *testing.T) { - bot := NewTest(t) - bot.bot.SetUTF8(true) - bot.extend(` - + hello bot - - Hello human! - `) - - bot.reply("Hello bot", "Hello human!") - bot.reply("Hello, bot!", "Hello human!") - bot.reply("Hello: Bot", "Hello human!") - bot.reply("Hello... bot?", "Hello human!") - - bot.bot.SetUnicodePunctuation(`xxx`) - bot.reply("Hello bot", "Hello human!") - bot.reply("Hello, bot!", "ERR: No Reply Matched") -} diff --git a/src/sessions_test.go b/src/sessions_test.go index bd826ce..10cea5d 100644 --- a/src/sessions_test.go +++ b/src/sessions_test.go @@ -1,11 +1,9 @@ -package rivescript_test +package rivescript import ( "testing" - "github.com/aichaos/rivescript-go/config" "github.com/aichaos/rivescript-go/sessions" - "github.com/aichaos/rivescript-go/sessions/memory" "github.com/aichaos/rivescript-go/sessions/null" ) @@ -32,9 +30,7 @@ var commonSessionTest = ` ` func TestNullSession(t *testing.T) { - bot := NewTestWithConfig(t, &config.Config{ - SessionManager: null.New(), - }) + bot := NewTestWithConfig(t, false, false, null.New()) bot.extend(commonSessionTest) bot.reply("My name is Aiden", "Nice to meet you, undefined.") bot.reply("Who am I?", "Aren't you undefined?") @@ -45,9 +41,7 @@ func TestNullSession(t *testing.T) { } func TestMemorySession(t *testing.T) { - bot := NewTestWithConfig(t, &config.Config{ - SessionManager: memory.New(), - }) + bot := NewTest(t) bot.extend(commonSessionTest) bot.reply("My name is Aiden", "Nice to meet you, Aiden.") bot.reply("What did I just say?", "You just said: my name is aiden") diff --git a/src/substitution_test.go b/src/substitution_test.go new file mode 100644 index 0000000..3c092b8 --- /dev/null +++ b/src/substitution_test.go @@ -0,0 +1,42 @@ +package rivescript + +import "testing" + +func TestSubstitutions(t *testing.T) { + bot := NewTest(t) + bot.extend(` + + whats up + - nm. + + + what is up + - Not much. + `) + bot.reply("whats up", "nm.") + bot.reply("what's up?", "nm.") + bot.reply("what is up?", "Not much.") + + bot.extend(` + ! sub whats = what is + ! sub what's = what is + `) + bot.reply("whats up", "Not much.") + bot.reply("what's up?", "Not much.") + bot.reply("What is up?", "Not much.") +} + +func TestPersonSubstitutions(t *testing.T) { + bot := NewTest(t) + bot.extend(` + + say * + - + `) + bot.reply("say I am cool", "i am cool") + bot.reply("say You are dumb", "you are dumb") + + bot.extend(` + ! person i am = you are + ! person you are = I am + `) + bot.reply("say I am cool", "you are cool") + bot.reply("say You are dumb", "I am dumb") +} diff --git a/src/tags.go b/src/tags.go index 466f409..ec744f9 100644 --- a/src/tags.go +++ b/src/tags.go @@ -78,7 +78,7 @@ func (rs *RiveScript) triggerRegexp(username string, pattern string) string { pipes := strings.Join(opts, "|") pipes = strings.Replace(pipes, `(.+?)`, `(?:.+?)`, -1) pipes = strings.Replace(pipes, `(\d+?)`, `(?:\d+?)`, -1) - pipes = strings.Replace(pipes, `(\d+?)`, `(?:\w+?)`, -1) + pipes = strings.Replace(pipes, `(\w+?)`, `(?:\w+?)`, -1) pattern = regReplace(pattern, fmt.Sprintf(`\s*\[%s\]\s*`, quotemeta(match[1])), @@ -219,13 +219,11 @@ func (rs *RiveScript) processTags(username string, message string, reply string, reply = re_weight.ReplaceAllString(reply, "") // Remove {weight} tags. reply = strings.Replace(reply, "", stars[1], -1) reply = strings.Replace(reply, "", botstars[1], -1) - for i := 1; i <= 9; i++ { - if len(stars) > i { - reply = strings.Replace(reply, fmt.Sprintf("", i), stars[i], -1) - } - if len(botstars) > i { - reply = strings.Replace(reply, fmt.Sprintf("", i), botstars[i], -1) - } + for i := 1; i < len(stars); i++ { + reply = strings.Replace(reply, fmt.Sprintf("", i), stars[i], -1) + } + for i := 1; i < len(botstars); i++ { + reply = strings.Replace(reply, fmt.Sprintf("", i), botstars[i], -1) } // and @@ -515,12 +513,10 @@ func (rs *RiveScript) substitute(message string, subs map[string]string, sorted pi++ // Run substitutions. - // fmt.Printf("BEFORE: %s\n", message) message = regReplace(message, fmt.Sprintf(`^%s$`, qm), placeholder) message = regReplace(message, fmt.Sprintf(`^%s(\W+)`, qm), fmt.Sprintf("%s$1", placeholder)) message = regReplace(message, fmt.Sprintf(`(\W+)%s(\W+)`, qm), fmt.Sprintf("$1%s$2", placeholder)) message = regReplace(message, fmt.Sprintf(`(\W+)%s$`, qm), fmt.Sprintf("$1%s", placeholder)) - // fmt.Printf("AFTER: %s\n", message) } // Convert the placeholders back in. diff --git a/src/topic_test.go b/src/topic_test.go new file mode 100644 index 0000000..0282faa --- /dev/null +++ b/src/topic_test.go @@ -0,0 +1,120 @@ +package rivescript + +import "testing" + +func TestPunishmentTopic(t *testing.T) { + bot := NewTest(t) + bot.extend(` + + hello + - Hi there! + + + swear word + - How rude! Apologize or I won't talk to you again.{topic=sorry} + + + * + - Catch-all. + + > topic sorry + + sorry + - It's ok!{topic=random} + + + * + - Say you're sorry! + < topic + `) + bot.reply("hello", "Hi there!") + bot.reply("How are you?", "Catch-all.") + bot.reply("Swear word!", "How rude! Apologize or I won't talk to you again.") + bot.reply("hello", "Say you're sorry!") + bot.reply("How are you?", "Say you're sorry!") + bot.reply("Sorry!", "It's ok!") + bot.reply("hello", "Hi there!") + bot.reply("How are you?", "Catch-all.") +} + +func TestTopicInheritance(t *testing.T) { + bot := NewTest(t) + RS_ERR_MATCH := "ERR: No Reply Matched" + bot.extend(` + > topic colors + + what color is the sky + - Blue. + + what color is the sun + - Yellow. + < topic + + > topic linux + + name a red hat distro + - Fedora. + + name a debian distro + - Ubuntu. + < topic + + > topic stuff includes colors linux + + say stuff + - "Stuff." + < topic + + > topic override inherits colors + + what color is the sun + - Purple. + < topic + + > topic morecolors includes colors + + what color is grass + - Green. + < topic + + > topic evenmore inherits morecolors + + what color is grass + - Blue, sometimes. + < topic + `) + bot.bot.SetUservar(bot.username, "topic", "colors") + bot.reply("What color is the sky?", "Blue.") + bot.reply("What color is the sun?", "Yellow.") + bot.reply("What color is grass?", RS_ERR_MATCH) + bot.reply("Name a Red Hat distro.", RS_ERR_MATCH) + bot.reply("Name a Debian distro.", RS_ERR_MATCH) + bot.reply("Say stuff.", RS_ERR_MATCH) + + bot.bot.SetUservar(bot.username, "topic", "linux") + bot.reply("What color is the sky?", RS_ERR_MATCH) + bot.reply("What color is the sun?", RS_ERR_MATCH) + bot.reply("What color is grass?", RS_ERR_MATCH) + bot.reply("Name a Red Hat distro.", "Fedora.") + bot.reply("Name a Debian distro.", "Ubuntu.") + bot.reply("Say stuff.", RS_ERR_MATCH) + + bot.bot.SetUservar(bot.username, "topic", "stuff") + bot.reply("What color is the sky?", "Blue.") + bot.reply("What color is the sun?", "Yellow.") + bot.reply("What color is grass?", RS_ERR_MATCH) + bot.reply("Name a Red Hat distro.", "Fedora.") + bot.reply("Name a Debian distro.", "Ubuntu.") + bot.reply("Say stuff.", `"Stuff."`) + + bot.bot.SetUservar(bot.username, "topic", "override") + bot.reply("What color is the sky?", "Blue.") + bot.reply("What color is the sun?", "Purple.") + bot.reply("What color is grass?", RS_ERR_MATCH) + bot.reply("Name a Red Hat distro.", RS_ERR_MATCH) + bot.reply("Name a Debian distro.", RS_ERR_MATCH) + bot.reply("Say stuff.", RS_ERR_MATCH) + + bot.bot.SetUservar(bot.username, "topic", "morecolors") + bot.reply("What color is the sky?", "Blue.") + bot.reply("What color is the sun?", "Yellow.") + bot.reply("What color is grass?", "Green.") + bot.reply("Name a Red Hat distro.", RS_ERR_MATCH) + bot.reply("Name a Debian distro.", RS_ERR_MATCH) + bot.reply("Say stuff.", RS_ERR_MATCH) + + bot.bot.SetUservar(bot.username, "topic", "evenmore") + bot.reply("What color is the sky?", "Blue.") + bot.reply("What color is the sun?", "Yellow.") + bot.reply("What color is grass?", "Blue, sometimes.") + bot.reply("Name a Red Hat distro.", RS_ERR_MATCH) + bot.reply("Name a Debian distro.", RS_ERR_MATCH) + bot.reply("Say stuff.", RS_ERR_MATCH) +} diff --git a/src/trigger_test.go b/src/trigger_test.go new file mode 100644 index 0000000..2cdcf07 --- /dev/null +++ b/src/trigger_test.go @@ -0,0 +1,138 @@ +package rivescript + +import "testing" + +func TestAtomicTriggers(t *testing.T) { + bot := NewTest(t) + bot.extend(` + + hello bot + - Hello human. + + + what are you + - I am a RiveScript bot. + `) + bot.reply("Hello bot", "Hello human.") + bot.reply("What are you?", "I am a RiveScript bot.") +} + +func TestWildcardTriggers(t *testing.T) { + bot := NewTest(t) + bot.extend(` + + my name is * + - Nice to meet you, . + + + * told me to say * + - Why did tell you to say ? + + + i am # years old + - A lot of people are . + + + i am _ years old + - Say that with numbers. + + + i am * years old + - Say that with fewer words. + + // Verify that wildcards in optionals are not matchable. + + my favorite [_] is * + - Why is it ? + + + i have [#] questions about * + - Well I don't have any answers about . + `) + bot.reply("my name is Bob", "Nice to meet you, bob.") + bot.reply("bob told me to say hi", "Why did bob tell you to say hi?") + bot.reply("i am 5 years old", "A lot of people are 5.") + bot.reply("i am five years old", "Say that with numbers.") + bot.reply("i am twenty five years old", "Say that with fewer words.") + + bot.reply("my favorite color is red", "Why is it red?") + bot.reply("i have 2 questions about bots", "Well I don't have any answers about bots.") +} + +func TestAlternativesAndOptionals(t *testing.T) { + bot := NewTest(t) + bot.extend(` + + what (are|is) you + - I am a robot. + + + what is your (home|office|cell) [phone] number + - It is 555-1234. + + + [please|can you] ask me a question + - Why is the sky blue? + + + (aa|bb|cc) [bogus] + - Matched. + + + (yo|hi) [computer|bot] * + - Matched. + `) + bot.reply("What are you?", "I am a robot.") + bot.reply("What is you?", "I am a robot.") + + bot.reply("What is your home phone number?", "It is 555-1234.") + bot.reply("What is your home number?", "It is 555-1234.") + bot.reply("What is your cell phone number?", "It is 555-1234.") + bot.reply("What is your office number?", "It is 555-1234.") + + bot.reply("Can you ask me a question?", "Why is the sky blue?") + bot.reply("Please ask me a question?", "Why is the sky blue?") + bot.reply("Ask me a question.", "Why is the sky blue?") + + bot.reply("aa", "Matched.") + bot.reply("bb", "Matched.") + bot.reply("aa bogus", "Matched.") + bot.reply("aabogus", "ERR: No Reply Matched") + bot.reply("bogus", "ERR: No Reply Matched") + + bot.reply("hi Aiden", "Matched.") + bot.reply("hi bot how are you?", "Matched.") + bot.reply("yo computer what time is it?", "Matched.") + bot.reply("yoghurt is yummy", "ERR: No Reply Matched") + bot.reply("hide and seek is fun", "ERR: No Reply Matched") + bot.reply("hip hip hurrah", "ERR: No Reply Matched") +} + +func TestTriggerArrays(t *testing.T) { + bot := NewTest(t) + bot.extend(` + ! array colors = red blue green yellow white + ^ dark blue|light blue + + + what color is my (@colors) * + - Your is . + + + what color was * (@colors) * + - It was . + + + i have a @colors * + - Tell me more about your . + `) + bot.reply("What color is my red shirt?", "Your shirt is red.") + bot.reply("What color is my blue car?", "Your car is blue.") + bot.reply("What color is my pink house?", "ERR: No Reply Matched") + bot.reply("What color is my dark blue jacket?", "Your jacket is dark blue.") + bot.reply("What color was Napoleoan's white horse?", "It was white.") + bot.reply("What color was my red shirt?", "It was red.") + bot.reply("I have a blue car.", "Tell me more about your car.") + bot.reply("I have a cyan car.", "ERR: No Reply Matched") +} + +func TestWeightedTriggers(t *testing.T) { + bot := NewTest(t) + bot.extend(` + + * or something{weight=10} + - Or something. <@> + + + can you run a google search for * + - Sure! + + + hello *{weight=20} + - Hi there! + `) + bot.reply("Hello robot.", "Hi there!") + bot.reply("Hello or something.", "Hi there!") + bot.reply("Can you run a Google search for Node", "Sure!") + bot.reply("Can you run a Google search for Node or something", "Or something. Sure!") +} diff --git a/src/unicode_test.go b/src/unicode_test.go new file mode 100644 index 0000000..ea38330 --- /dev/null +++ b/src/unicode_test.go @@ -0,0 +1,70 @@ +package rivescript + +import "testing" + +func TestUnicode(t *testing.T) { + bot := NewTestWithUTF8(t) + bot.extend(` + ! sub who's = who is + + äh + - What's the matter? + + + ブラッキー + - エーフィ + + // Make sure %Previous continues working in UTF-8 mode. + + knock knock + - Who's there? + + + * + % who is there + - who? + + + * + % * who + - Haha! ! + + // And with UTF-8. + + tëll më ä pöëm + - Thërë öncë wäs ä män nämëd Tïm + + + more + % thërë öncë wäs ä män nämëd tïm + - Whö nëvër qüïtë lëärnëd höw tö swïm + + + more + % whö nëvër qüïtë lëärnëd höw tö swïm + - Hë fëll öff ä döck, änd sänk lïkë ä röck + + + more + % hë fëll öff ä döck änd sänk lïkë ä röck + - Änd thät wäs thë ënd öf hïm. + `) + + bot.reply("äh", "What's the matter?") + bot.reply("ブラッキー", "エーフィ") + bot.reply("knock knock", "Who's there?") + bot.reply("Orange", "Orange who?") + bot.reply("banana", "Haha! Banana!") + bot.reply("tëll më ä pöëm", "Thërë öncë wäs ä män nämëd Tïm") + bot.reply("more", "Whö nëvër qüïtë lëärnëd höw tö swïm") + bot.reply("more", "Hë fëll öff ä döck, änd sänk lïkë ä röck") + bot.reply("more", "Änd thät wäs thë ënd öf hïm.") +} + +func TestPunctuation(t *testing.T) { + bot := NewTestWithUTF8(t) + bot.extend(` + + hello bot + - Hello human! + `) + + bot.reply("Hello bot", "Hello human!") + bot.reply("Hello, bot!", "Hello human!") + bot.reply("Hello: Bot", "Hello human!") + bot.reply("Hello... bot?", "Hello human!") + + bot.bot.SetUnicodePunctuation(`xxx`) + bot.reply("Hello bot", "Hello human!") + bot.reply("Hello, bot!", "ERR: No Reply Matched") +} From 1460687a32af314babfad7c001bfb30168612fd9 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Thu, 2 Feb 2017 02:12:43 -0800 Subject: [PATCH 2/2] General code cleanup, updating public API This goes through all the code and cleans up variable names, deletes unused functions, and fixes `go vet` errors. This also adds `error` return values to Reply(), SortReplies() and CurrentUser(), and adds more helpful error conditions to LoadFile() and LoadDirectory() to catch common user mistakes. And this fixes RemoveHandler() to delete any object macros already loaded by that handler to protect against null pointer exceptions. --- Changes.md | 12 ++++++ cmd/rivescript/main.go | 8 +++- doc_test.go | 10 ++--- parser/parser.go | 74 +++++++++++++++++--------------- rivescript.go | 18 +++++--- sessions/memory/memory.go | 4 +- src/astmap.go | 4 +- src/base_test.go | 62 +++++++++++++++++++-------- src/begin_test.go | 47 ++++++++++++++++++++- src/bot_variable_test.go | 4 -- src/brain.go | 76 +++++++++++++++++++-------------- src/config.go | 69 ++++++++++++++++++++++-------- src/config_test.go | 84 ++++++++++++++++++++++++++++++++++++ src/debug.go | 6 ++- src/errors.go | 12 ++++++ src/inheritance.go | 8 ++-- src/loading.go | 49 +++++++-------------- src/object_test.go | 89 +++++++++++++++++++++++++++++++++++++++ src/parser.go | 16 +++---- src/regexp.go | 50 ++++++++++++---------- src/reply_test.go | 14 ++++-- src/rivescript.go | 27 ++++-------- src/rivescript_test.go | 29 +++++++++++++ src/sessions_test.go | 10 +++-- src/sorting.go | 74 +++++++++++++++++++++++++------- src/structs.go | 9 ++++ src/tags.go | 79 ++++++++++++++++++---------------- src/topic_test.go | 39 +++++++++-------- src/trigger_test.go | 14 +++--- src/unicode_test.go | 2 +- src/utils.go | 9 ++-- 31 files changed, 707 insertions(+), 301 deletions(-) create mode 100644 src/config_test.go create mode 100644 src/errors.go create mode 100644 src/object_test.go create mode 100644 src/rivescript_test.go diff --git a/Changes.md b/Changes.md index 4dc2824..e4b279e 100644 --- a/Changes.md +++ b/Changes.md @@ -31,6 +31,8 @@ This update focuses on bug fixes and code reorganization. bot = rivescript.New(rivescript.WithUTF8()) } ``` +* `Reply()`, `SortReplies()` and `CurrentUser()` now return an `error` value + in addition to what they already returned. ### Changes @@ -40,6 +42,7 @@ This update focuses on bug fixes and code reorganization. * Handle module configuration at the root package instead of in the `src` package. This enabled getting rid of the `rivescript/config` package and making the public API more sane. +* Code cleanup via `go vet` * Add more documentation and examples to the Go doc. * Fix `@Redirects` not working sometimes when tags like `` insert capital letters (bug #1) @@ -49,6 +52,15 @@ This update focuses on bug fixes and code reorganization. the optional makes its wildcard non-capturing (bug #15) * Fix the `` tag handling to support star numbers greater than ``: you can use as many star numbers as will be captured by your trigger (bug #16) +* Fix a probable bug within inheritance/includes: some parts of the code were + looking in one location for them, another in the other, so they probably + didn't work perfectly before. +* Fix `RemoveHandler()` to make it remove all known object macros that used that + handler, which protects against a possible null pointer exception. +* Fix `LoadDirectory()` to return an error when doesn't find any RiveScript + source files to load, which helps protect against the common error that you + gave it the wrong directory. +* New unit tests: object macros ## v0.1.0 - Dec 11, 2016 diff --git a/cmd/rivescript/main.go b/cmd/rivescript/main.go index 2f2f1af..e2e3f40 100644 --- a/cmd/rivescript/main.go +++ b/cmd/rivescript/main.go @@ -97,8 +97,12 @@ Type a message to the bot and press Return to send it. } else if strings.Index(text, "/quit") == 0 { os.Exit(0) } else { - reply := bot.Reply("localuser", text) - fmt.Printf("Bot> %s\n", reply) + reply, err := bot.Reply("localuser", text) + if err != nil { + fmt.Printf("Error> %s\n", err) + } else { + fmt.Printf("Bot> %s\n", reply) + } } } } diff --git a/doc_test.go b/doc_test.go index 8a4cb32..f009baf 100644 --- a/doc_test.go +++ b/doc_test.go @@ -40,7 +40,7 @@ func Example() { bot.SortReplies() // Get a reply. - reply := bot.Reply("local-user", "Hello, bot!") + reply, _ := bot.Reply("local-user", "Hello, bot!") fmt.Printf("The bot says: %s", reply) } @@ -61,7 +61,7 @@ func ExampleRiveScript_utf8() { // Without UTF-8 mode enabled, the user's message "comment ça va" would // have the ç symbol removed; but in UTF-8 mode it's preserved and can // match the trigger we defined. - reply := bot.Reply("local-user", "Comment ça va?") + reply, _ := bot.Reply("local-user", "Comment ça va?") fmt.Println(reply) // "ça va bien." } @@ -97,7 +97,7 @@ func ExampleRiveScript_javascript() { `) bot.SortReplies() - reply := bot.Reply("local-user", "Add 5 and 7") + reply, _ := bot.Reply("local-user", "Add 5 and 7") fmt.Printf("Bot: %s\n", reply) } @@ -123,7 +123,7 @@ func ExampleRiveScript_subroutine() { `) bot.SortReplies() - _ = bot.Reply("local-user", "my name is bob") - reply := bot.Reply("local-user", "What is my name?") + bot.Reply("local-user", "my name is bob") + reply, _ := bot.Reply("local-user", "What is my name?") fmt.Printf("Bot: %s\n", reply) } diff --git a/parser/parser.go b/parser/parser.go index 2a6ebe6..857f913 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -8,7 +8,6 @@ RiveScript source code and get an "abstract syntax tree" back from it. package parser import ( - "errors" "fmt" "strconv" "strings" @@ -90,19 +89,19 @@ func (self *Parser) Parse(filename string, code []string) (*ast.Root, error) { // NOTE: the all caps AST is the instance, and the lowercase ast is the // package that defines the types. AST := ast.New() - AST.Begin.Global["hi"] = "true" // Track temporary variables - topic := "random" // Default topic = random - lineno := 0 // Line numbers for syntax tracking - comment := false // In a multi-line comment - inobj := false // In an object macro - objName := "" // Name of the object we're in - objLang := "" // The programming language of the object - objBuf := []string{} // Source code buffer of the object - isThat := "" // Is a %Previous trigger - var curTrig *ast.Trigger // Pointer to the current trigger - curTrig = nil + var ( + topic = "random" // Default topic = random + lineno int // Line numbers for syntax tracking + comment bool // In a multi-line comment + inobj bool // In an object macro + objName string // Name of the object we're in + objLang string // The programming language of the object + objBuf = []string{} // Source code buffer of the object + isThat string // Is a %Previous trigger + curTrig *ast.Trigger // Pointer to the current trigger + ) // Local (file-scoped) parser options. localOptions := map[string]string{ @@ -251,16 +250,19 @@ func (self *Parser) Parse(filename string, code []string) (*ast.Root, error) { // Handle the types of RiveScript commands switch cmd { case "!": // ! Define - halves := strings.SplitN(line, "=", 2) - left := strings.Split(strings.TrimSpace(halves[0]), " ") - value := "" - type_ := "" - name := "" + var ( + halves = strings.SplitN(line, "=", 2) + left = strings.Split(strings.TrimSpace(halves[0]), " ") + value string + kind string // global, var, sub, ... + name string + ) + if len(halves) == 2 { value = strings.TrimSpace(halves[1]) } if len(left) >= 1 { - type_ = strings.TrimSpace(left[0]) + kind = strings.TrimSpace(left[0]) if len(left) >= 2 { left = left[1:] name = strings.TrimSpace(strings.Join(left, " ")) @@ -268,17 +270,18 @@ func (self *Parser) Parse(filename string, code []string) (*ast.Root, error) { } // Remove 'fake' line breaks unless this is an array. - if type_ != "array" { + if kind != "array" { crlfReplacer := strings.NewReplacer("", "") value = crlfReplacer.Replace(value) } // Handle version numbers - if type_ == "version" { + if kind == "version" { parsedVersion, _ := strconv.ParseFloat(value, 32) if parsedVersion > RS_VERSION { - return nil, errors.New( - fmt.Sprintf("Unsupported RiveScript version. We only support %f at %s line %d", RS_VERSION, filename, lineno), + return nil, fmt.Errorf( + "Unsupported RiveScript version. We only support %f at %s line %d", + RS_VERSION, filename, lineno, ) } continue @@ -295,7 +298,7 @@ func (self *Parser) Parse(filename string, code []string) (*ast.Root, error) { } // Handle the rest of the !Define types. - switch type_ { + switch kind { case "local": // Local file-scoped parser options self.say("\tSet local parser option %s = %s", name, value) @@ -326,7 +329,7 @@ func (self *Parser) Parse(filename string, code []string) (*ast.Root, error) { } // Convert any remaining \s's over. - for i, _ := range fields { + for i := range fields { spaceReplacer := strings.NewReplacer("\\s", " ") fields[i] = spaceReplacer.Replace(fields[i]) } @@ -341,11 +344,11 @@ func (self *Parser) Parse(filename string, code []string) (*ast.Root, error) { self.say("\tSet person substitution %s = %s", name, value) AST.Begin.Person[name] = value default: - self.warn("Unknown definition type '%s'", filename, lineno, type_) + self.warn("Unknown definition type '%s'", filename, lineno, kind) } case ">": // > Label temp := strings.Split(strings.TrimSpace(line), " ") - type_ := temp[0] + kind := temp[0] temp = temp[1:] name := "" fields := []string{} @@ -358,12 +361,12 @@ func (self *Parser) Parse(filename string, code []string) (*ast.Root, error) { } // Handle the label types. - if type_ == "begin" { + if kind == "begin" { self.say("Found the BEGIN block.") - type_ = "topic" + kind = "topic" name = "__begin__" } - if type_ == "topic" { + if kind == "topic" { self.say("Set topic to %s", name) curTrig = nil topic = name @@ -384,7 +387,7 @@ func (self *Parser) Parse(filename string, code []string) (*ast.Root, error) { } } } - } else if type_ == "object" { + } else if kind == "object" { // If a field was provided, it should be the programming language. lang := "" if len(fields) > 0 { @@ -394,6 +397,9 @@ func (self *Parser) Parse(filename string, code []string) (*ast.Root, error) { // Missing language? if lang == "" { self.warn("No programming language specified for object '%s'", filename, lineno, name) + inobj = true + objName = name + objLang = "__unknown__" continue } @@ -403,15 +409,15 @@ func (self *Parser) Parse(filename string, code []string) (*ast.Root, error) { objBuf = []string{} inobj = true } else { - self.warn("Unknown label type '%s'", filename, lineno, type_) + self.warn("Unknown label type '%s'", filename, lineno, kind) } case "<": // < Label - type_ := line + kind := line - if type_ == "begin" || type_ == "topic" { + if kind == "begin" || kind == "topic" { self.say("\tEnd the topic label.") topic = "random" // Go back to default topic - } else if type_ == "object" { + } else if kind == "object" { self.say("\tEnd the object label.") inobj = false } diff --git a/rivescript.go b/rivescript.go index c82de26..5154c9f 100644 --- a/rivescript.go +++ b/rivescript.go @@ -120,9 +120,13 @@ SortReplies sorts the reply structures in memory for optimal matching. After you have finished loading your RiveScript code, call this method to populate the various sort buffers. This is absolutely necessary for reply matching to work efficiently! + +If the bot has loaded no topics, or if it ends up with no sorted triggers at +the end, it will return an error saying such. This usually means the bot didn't +load any RiveScript code, for example because it looked in the wrong directory. */ -func (rs *RiveScript) SortReplies() { - rs.rs.SortReplies() +func (rs *RiveScript) SortReplies() error { + return rs.rs.SortReplies() } //////////////////////////////////////////////////////////////////////////////// @@ -144,6 +148,9 @@ func (rs *RiveScript) SetHandler(name string, handler macro.MacroInterface) { /* RemoveHandler removes an object macro language handler. +If the handler has already loaded object macros, they will be deleted from +the bot along with the handler. + Parameters lang: The programming language for the handler to remove. @@ -328,7 +335,7 @@ This is only useful from within an object macro, to get the ID of the user who invoked the macro. This value is set at the beginning of `Reply()` and unset at the end, so this function will return empty outside of a reply context. */ -func (rs *RiveScript) CurrentUser() string { +func (rs *RiveScript) CurrentUser() (string, error) { return rs.rs.CurrentUser() } @@ -344,8 +351,9 @@ Parameters username: The name of the user requesting a reply. message: The user's message. */ -func (rs *RiveScript) Reply(username, message string) string { - return rs.rs.Reply(username, message) +func (rs *RiveScript) Reply(username, message string) (string, error) { + reply, err := rs.rs.Reply(username, message) + return reply, err } //////////////////////////////////////////////////////////////////////////////// diff --git a/sessions/memory/memory.go b/sessions/memory/memory.go index ad4d800..459e698 100644 --- a/sessions/memory/memory.go +++ b/sessions/memory/memory.go @@ -68,7 +68,7 @@ func (s *MemoryStore) SetLastMatch(username, trigger string) { } // Get a user variable. -func (s *MemoryStore) Get(username string, name string) (string, error) { +func (s *MemoryStore) Get(username, name string) (string, error) { s.lock.Lock() defer s.lock.Unlock() @@ -78,7 +78,7 @@ func (s *MemoryStore) Get(username string, name string) (string, error) { value, ok := s.users[username].Variables[name] if !ok { - return "undefined", fmt.Errorf(`variable "%s" for user "%s" not set`, name, username) + return "", fmt.Errorf(`variable "%s" for user "%s" not set`, name, username) } return value, nil diff --git a/src/astmap.go b/src/astmap.go index acd3a51..d971b32 100644 --- a/src/astmap.go +++ b/src/astmap.go @@ -31,7 +31,7 @@ type astRoot struct { type astBegin struct { global map[string]string - var_ map[string]string + vars map[string]string sub map[string]string person map[string]string array map[string][]string // Map of string (names) to arrays-of-strings @@ -39,8 +39,6 @@ type astBegin struct { type astTopic struct { triggers []*astTrigger - includes map[string]bool - inherits map[string]bool } type astTrigger struct { diff --git a/src/base_test.go b/src/base_test.go index 662aff5..1267d52 100644 --- a/src/base_test.go +++ b/src/base_test.go @@ -4,7 +4,6 @@ package rivescript // public facing API from the root rivescript-go package. import ( - "fmt" "testing" "github.com/aichaos/rivescript-go/sessions" @@ -31,17 +30,10 @@ func NewTestWithConfig(t *testing.T, debug, utf8 bool, ses sessions.SessionManag t: t, username: "soandso", } - test.bot.Debug = debug - test.bot.UTF8 = utf8 - test.bot.sessions = ses + test.bot.Configure(debug, true, utf8, 50, ses) return test } -// RS exposes the underlying RiveScript API. -func (rst *RiveScriptTest) RS() *RiveScript { - return rst.bot -} - // extend updates the RiveScript source code. func (rst RiveScriptTest) extend(code string) { rst.bot.Stream(code) @@ -49,17 +41,53 @@ func (rst RiveScriptTest) extend(code string) { } // reply asserts that a given input gets the expected reply. -func (rst RiveScriptTest) reply(message string, expected string) { - reply := rst.bot.Reply(rst.username, message) - if reply != expected { - rst.t.Error(fmt.Sprintf("Expected %s, got %s", expected, reply)) +func (rst RiveScriptTest) reply(message, expected string) { + reply, err := rst.bot.Reply(rst.username, message) + if err != nil { + rst.t.Errorf("Got an error when checking a reply to '%s': %s", message, err) + } else if reply != expected { + rst.t.Errorf("Expected %s, got %s", expected, reply) + } +} + +// replyError asserts that a given input gives an error. +func (rst RiveScriptTest) replyError(message string, expected error) { + if reply, err := rst.bot.Reply(rst.username, message); err == nil { + rst.t.Errorf( + "Reply to '%s' was expected to error; but it returned %s", + message, + reply, + ) + } else if err != expected { + rst.t.Errorf( + "Reply to '%s' got different error than expected; wanted %s, got %s", + message, + expected, + err, + ) } } -// uservar asserts a user variable. +// assertEqual checks if two strings are equal. +func (rst RiveScriptTest) assertEqual(a, b string) { + if a != b { + rst.t.Errorf("assertEqual: %s != %s", a, b) + } +} + +// uservar asserts a user variable is defined and has the expected value. func (rst RiveScriptTest) uservar(name string, expected string) { - value, _ := rst.bot.GetUservar(rst.username, name) - if value != expected { - rst.t.Error(fmt.Sprintf("Uservar %s expected %s, got %s", name, expected, value)) + value, err := rst.bot.GetUservar(rst.username, name) + if err != nil { + rst.t.Errorf("Got an error when asserting variable %s: %s", name, err) + } else if value != expected { + rst.t.Errorf("Uservar %s expected %s, got %s", name, expected, value) + } +} + +// undefined asserts that a user variable is not set. +func (rst RiveScriptTest) undefined(name string) { + if value, err := rst.bot.GetUservar(rst.username, name); err == nil { + rst.t.Errorf("Uservar %s was expected to be undefined; but was %s", name, value) } } diff --git a/src/begin_test.go b/src/begin_test.go index 1c26126..c21fd42 100644 --- a/src/begin_test.go +++ b/src/begin_test.go @@ -43,8 +43,53 @@ func TestConditionalBeginBlock(t *testing.T) { `) bot.reply("Hello bot", "Hello human.") bot.uservar("met", "true") - bot.uservar("name", "undefined") + bot.undefined("name") bot.reply("My name is bob", "Hello, Bob.") bot.uservar("name", "Bob") bot.reply("Hello Bot", "Bob: Hello human.") } + +func TestDefinitions(t *testing.T) { + // A helper to assert a substitution exists. + assertIn := func(dict map[string]string, key string, expected bool) { + _, ok := dict[key] + if ok != expected { + t.Errorf( + "Expected key %s to exist: %v; but we got %v", + key, expected, ok, + ) + } + } + + bot := NewTest(t) + bot.extend(` + ! global g1 = one + ! global g2 = two + ! global g2 = + + ! var v1 = one + ! var v2 = two + ! var v2 = + + ! sub what's = what is + ! sub who's = who is + ! sub who's = + + ! person you = me + ! person me = you + ! person your = my + ! person my = your + ! person your = + ! person my = + `) + rs := bot.bot + + assertIn(rs.global, "g1", true) + assertIn(rs.global, "g2", false) + assertIn(rs.vars, "v1", true) + assertIn(rs.vars, "v2", false) + assertIn(rs.sub, "what's", true) + assertIn(rs.sub, "who's", false) + assertIn(rs.person, "you", true) + assertIn(rs.person, "your", false) +} diff --git a/src/bot_variable_test.go b/src/bot_variable_test.go index e389276..354005f 100644 --- a/src/bot_variable_test.go +++ b/src/bot_variable_test.go @@ -2,10 +2,6 @@ package rivescript import "testing" -//////////////////////////////////////////////////////////////////////////////// -// Bot Variable Tests -//////////////////////////////////////////////////////////////////////////////// - func TestBotVariables(t *testing.T) { bot := NewTest(t) bot.extend(` diff --git a/src/brain.go b/src/brain.go index 1c61d29..ca6b995 100644 --- a/src/brain.go +++ b/src/brain.go @@ -8,35 +8,46 @@ import ( "strings" ) -// Brain logic for RiveScript - -func (rs *RiveScript) Reply(username, message string) string { +// Reply gets a reply from the bot. +func (rs *RiveScript) Reply(username, message string) (string, error) { rs.say("Asked to reply to [%s] %s", username, message) + var err error // Initialize a user profile for this user? rs.sessions.Init(username) // Store the current user's ID. + rs.inReplyContext = true rs.currentUser = username // Format their message. message = rs.formatMessage(message, false) - reply := "" + var reply string // If the BEGIN block exists, consult it first. if _, ok := rs.topics["__begin__"]; ok { - begin := rs.getReply(username, "request", true, 0) + var begin string + begin, err = rs.getReply(username, "request", true, 0) + if err != nil { + return "", err + } // OK to continue? if strings.Index(begin, "{ok}") > -1 { - reply = rs.getReply(username, message, false, 0) + reply, err = rs.getReply(username, message, false, 0) + if err != nil { + return "", err + } begin = strings.NewReplacer("{ok}", reply).Replace(begin) } reply = begin reply = rs.processTags(username, message, reply, []string{}, []string{}, 0) } else { - reply = rs.getReply(username, message, false, 0) + reply, err = rs.getReply(username, message, false, 0) + if err != nil { + return "", err + } } // Save their message history. @@ -44,8 +55,9 @@ func (rs *RiveScript) Reply(username, message string) string { // Unset the current user's ID. rs.currentUser = "" + rs.inReplyContext = false - return reply + return reply, nil } /* @@ -58,11 +70,11 @@ Parameters isBegin: Whether this reply is for the "BEGIN Block" context or not. step: Recursion depth counter. */ -func (rs *RiveScript) getReply(username string, message string, isBegin bool, step uint) string { +func (rs *RiveScript) getReply(username string, message string, isBegin bool, step uint) (string, error) { // Needed to sort replies? if len(rs.sorted.topics) == 0 { rs.warn("You forgot to call SortReplies()!") - return "ERR: Replies Not Sorted" + return "", ErrRepliesNotSorted } // Collect data on this user. @@ -72,7 +84,7 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st } stars := []string{} thatStars := []string{} // For %Previous - reply := "" + var reply string // Avoid letting them fall into a missing topic. if _, ok := rs.topics[topic]; !ok { @@ -83,7 +95,7 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st // Avoid deep recursion. if step > rs.Depth { - return "ERR: Deep Recursion Detected" + return "", ErrDeepRecursion } // Are we in the BEGIN block? @@ -95,7 +107,7 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st if _, ok := rs.topics[topic]; !ok { // This was handled before, which would mean topic=random and it doesn't // exist. Serious issue! - return "ERR: No default topic 'random' was found!" + return "", ErrNoDefaultTopic } // Create a pointer for the matched data when we find it. @@ -109,7 +121,7 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st // be the same as it was the first time, resulting in an infinite loop! if step == 0 { allTopics := []string{topic} - if len(rs.topics[topic].includes) > 0 || len(rs.topics[topic].inherits) > 0 { + if len(rs.includes[topic]) > 0 || len(rs.inherits[topic]) > 0 { // Get ALL the topics! allTopics = rs.getTopicTree(topic, 0) } @@ -145,7 +157,7 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st // Collect the bot stars. thatStars = []string{} if len(match) > 1 { - for i, _ := range match[1:] { + for i := range match[1:] { thatStars = append(thatStars, match[i+1]) } } @@ -169,7 +181,7 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st // Get the user's message stars. if len(match) > 1 { - for i, _ := range match[1:] { + for i := range match[1:] { stars = append(stars, match[i+1]) } } @@ -212,7 +224,7 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st // Collect the stars. if len(match) > 1 { - for i, _ := range match[1:] { + for i := range match[1:] { stars = append(stars, match[i+1]) } } @@ -245,7 +257,10 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st redirect = rs.processTags(username, message, redirect, stars, thatStars, 0) redirect = strings.ToLower(redirect) rs.say("Pretend user said: %s", redirect) - reply = rs.getReply(username, redirect, isBegin, step+1) + reply, err = rs.getReply(username, redirect, isBegin, step+1) + if err != nil { + return "", err + } break } @@ -253,7 +268,7 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st for _, row := range matched.condition { halves := strings.Split(row, "=>") if len(halves) == 2 { - condition := re_condition.FindStringSubmatch(strings.TrimSpace(halves[0])) + condition := reCondition.FindStringSubmatch(strings.TrimSpace(halves[0])) if len(condition) > 0 { left := strings.TrimSpace(condition[1]) eq := condition[2] @@ -266,10 +281,10 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st // Defaults? if len(left) == 0 { - left = "undefined" + left = UNDEFINED } if len(right) == 0 { - right = "undefined" + right = UNDEFINED } rs.say("Check if %s %s %s", left, eq, right) @@ -319,10 +334,9 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st // Process weights in the replies. bucket := []string{} for _, rep := range matched.reply { - weight := 1 - match := re_weight.FindStringSubmatch(rep) + match := reWeight.FindStringSubmatch(rep) if len(match) > 0 { - weight, _ = strconv.Atoi(match[1]) + weight, _ := strconv.Atoi(match[1]) if weight <= 0 { rs.warn("Can't have a weight <= 0!") weight = 1 @@ -346,9 +360,9 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st // Still no reply?? Give up with the fallback error replies. if !foundMatch { - reply = "ERR: No Reply Matched" + return "", ErrNoTriggerMatched } else if len(reply) == 0 { - reply = "ERR: No Reply Found" + return "", ErrNoReplyFound } rs.say("Reply: %s", reply) @@ -358,7 +372,7 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st // The BEGIN block can set {topic} and user vars. // Topic setter - match := re_topic.FindStringSubmatch(reply) + match := reTopic.FindStringSubmatch(reply) var giveup uint for len(match) > 0 { giveup++ @@ -369,11 +383,11 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st name := match[1] rs.sessions.Set(username, map[string]string{"topic": name}) reply = strings.Replace(reply, fmt.Sprintf("{topic=%s}", name), "", -1) - match = re_topic.FindStringSubmatch(reply) + match = reTopic.FindStringSubmatch(reply) } // Set user vars - match = re_set.FindStringSubmatch(reply) + match = reSet.FindStringSubmatch(reply) giveup = 0 for len(match) > 0 { giveup++ @@ -385,11 +399,11 @@ func (rs *RiveScript) getReply(username string, message string, isBegin bool, st value := match[2] rs.sessions.Set(username, map[string]string{name: value}) reply = strings.Replace(reply, fmt.Sprintf("", name, value), "", -1) - match = re_set.FindStringSubmatch(reply) + match = reSet.FindStringSubmatch(reply) } } else { reply = rs.processTags(username, message, reply, stars, thatStars, 0) } - return reply + return reply, nil } diff --git a/src/config.go b/src/config.go index 33a9cba..93e574f 100644 --- a/src/config.go +++ b/src/config.go @@ -9,6 +9,7 @@ import ( "github.com/aichaos/rivescript-go/sessions" ) +// SetHandler registers a handler for foreign language object macros. func (rs *RiveScript) SetHandler(lang string, handler macro.MacroInterface) { rs.cLock.Lock() defer rs.cLock.Unlock() @@ -16,13 +17,23 @@ func (rs *RiveScript) SetHandler(lang string, handler macro.MacroInterface) { rs.handlers[lang] = handler } +// RemoveHandler deletes support for a foreign language object macro. func (rs *RiveScript) RemoveHandler(lang string) { rs.cLock.Lock() defer rs.cLock.Unlock() + // Purge all loaded objects for this handler. + for name, language := range rs.objlangs { + if language == lang { + delete(rs.objlangs, name) + } + } + + // And delete the handler itself. delete(rs.handlers, lang) } +// SetSubroutine defines a Go function to handle an object macro for RiveScript. func (rs *RiveScript) SetSubroutine(name string, fn Subroutine) { rs.cLock.Lock() defer rs.cLock.Unlock() @@ -30,6 +41,7 @@ func (rs *RiveScript) SetSubroutine(name string, fn Subroutine) { rs.subroutines[name] = fn } +// DeleteSubroutine deletes a Go object macro handler. func (rs *RiveScript) DeleteSubroutine(name string) { rs.cLock.Lock() defer rs.cLock.Unlock() @@ -37,60 +49,67 @@ func (rs *RiveScript) DeleteSubroutine(name string) { delete(rs.subroutines, name) } -func (rs *RiveScript) SetGlobal(name string, value string) { +// SetGlobal configures a global variable in RiveScript. +func (rs *RiveScript) SetGlobal(name, value string) { rs.cLock.Lock() defer rs.cLock.Unlock() - if value == "undefined" { + if value == UNDEFINED { delete(rs.global, name) } else { rs.global[name] = value } } -func (rs *RiveScript) SetVariable(name string, value string) { +// SetVariable configures a bot variable in RiveScript. +func (rs *RiveScript) SetVariable(name, value string) { rs.cLock.Lock() defer rs.cLock.Unlock() - if value == "undefined" { - delete(rs.var_, name) + if value == UNDEFINED { + delete(rs.vars, name) } else { - rs.var_[name] = value + rs.vars[name] = value } } -func (rs *RiveScript) SetSubstitution(name string, value string) { +// SetSubstitution sets a substitution pattern. +func (rs *RiveScript) SetSubstitution(name, value string) { rs.cLock.Lock() defer rs.cLock.Unlock() - if value == "undefined" { + if value == UNDEFINED { delete(rs.sub, name) } else { rs.sub[name] = value } } -func (rs *RiveScript) SetPerson(name string, value string) { +// SetPerson sets a person substitution. +func (rs *RiveScript) SetPerson(name, value string) { rs.cLock.Lock() defer rs.cLock.Unlock() - if value == "undefined" { + if value == UNDEFINED { delete(rs.person, name) } else { rs.person[name] = value } } -func (rs *RiveScript) SetUservar(username string, name string, value string) { +// SetUservar sets a user variable to a value. +func (rs *RiveScript) SetUservar(username, name, value string) { rs.sessions.Set(username, map[string]string{ name: value, }) } +// SetUservars sets many user variables at a time. func (rs *RiveScript) SetUservars(username string, data map[string]string) { rs.sessions.Set(username, data) } +// GetGlobal retrieves the value of a global variable. func (rs *RiveScript) GetGlobal(name string) (string, error) { rs.cLock.Lock() defer rs.cLock.Unlock() @@ -98,51 +117,65 @@ func (rs *RiveScript) GetGlobal(name string) (string, error) { if _, ok := rs.global[name]; ok { return rs.global[name], nil } - return "undefined", errors.New("Global variable not found.") + return UNDEFINED, errors.New("Global variable not found.") } +// GetVariable retrieves the value of a bot variable. func (rs *RiveScript) GetVariable(name string) (string, error) { rs.cLock.Lock() defer rs.cLock.Unlock() - if _, ok := rs.var_[name]; ok { - return rs.var_[name], nil + if _, ok := rs.vars[name]; ok { + return rs.vars[name], nil } - return "undefined", errors.New("Variable not found.") + return UNDEFINED, errors.New("Variable not found.") } -func (rs *RiveScript) GetUservar(username string, name string) (string, error) { +// GetUservar retrieves the value of a user variable. +func (rs *RiveScript) GetUservar(username, name string) (string, error) { return rs.sessions.Get(username, name) } +// GetUservars retrieves all variables about a user. func (rs *RiveScript) GetUservars(username string) (*sessions.UserData, error) { return rs.sessions.GetAny(username) } +// GetAllUservars gets all variables about all users. func (rs *RiveScript) GetAllUservars() map[string]*sessions.UserData { return rs.sessions.GetAll() } +// ClearUservars deletes all the variables that belong to a user. func (rs *RiveScript) ClearUservars(username string) { rs.sessions.Clear(username) } +// ClearAllUservars deletes all variables for all users. func (rs *RiveScript) ClearAllUservars() { rs.sessions.ClearAll() } +// FreezeUservars takes a snapshot of a user's variables. func (rs *RiveScript) FreezeUservars(username string) error { return rs.sessions.Freeze(username) } +// ThawUservars restores a snapshot of user variables. func (rs *RiveScript) ThawUservars(username string, action sessions.ThawAction) error { return rs.sessions.Thaw(username, action) } +// LastMatch returns the last match of the user. func (rs *RiveScript) LastMatch(username string) (string, error) { return rs.sessions.GetLastMatch(username) } -func (rs *RiveScript) CurrentUser() string { - return rs.currentUser +// CurrentUser returns the current user and can only be called from within an +// object macro context. +func (rs *RiveScript) CurrentUser() (string, error) { + if rs.inReplyContext { + return rs.currentUser, nil + } + return "", errors.New("CurrentUser() can only be called inside a reply context") } diff --git a/src/config_test.go b/src/config_test.go new file mode 100644 index 0000000..7a86523 --- /dev/null +++ b/src/config_test.go @@ -0,0 +1,84 @@ +package rivescript + +import ( + "testing" + + "github.com/aichaos/rivescript-go/sessions/memory" +) + +func TestConfigAPI(t *testing.T) { + // A helper function to handleVars error reporting when we do (or don't) expect errors. + handleVars := func(fn func(string) (string, error), param string, expected bool) { + _, err := fn(param) + if (err == nil && expected) || (err != nil && !expected) { + t.Errorf( + "Expected errors for var %s: %v; but we got as the error: [%s]", + param, + expected, + err, + ) + } + } + + // A helper to assert a substitution exists. + assertIn := func(dict map[string]string, key string, expected bool) { + _, ok := dict[key] + if ok != expected { + t.Errorf( + "Expected key %s to exist: %v; but we got %v", + key, expected, ok, + ) + } + } + + bot := NewTest(t) + bot.extend(` + + hello go + - hello-go + `) + rs := bot.bot + + // Setting a Go object macro. + rs.SetSubroutine("hello-go", func(rs *RiveScript, args []string) string { + return "Hello world" + }) + bot.reply("Hello go", "Hello world") + + // Deleting it. + rs.DeleteSubroutine("hello-go") + bot.reply("Hello go", "[ERR: Object Not Found]") + + // Global variables. + handleVars(rs.GetGlobal, "global test", true) + rs.SetGlobal("global test", "on") + handleVars(rs.GetGlobal, "global test", false) + rs.SetGlobal("global test", "undefined") + handleVars(rs.GetGlobal, "global test", true) + + // Bot variables. + handleVars(rs.GetVariable, "var test", true) + rs.SetVariable("var test", "on") + handleVars(rs.GetVariable, "var test", false) + rs.SetVariable("var test", "undefined") + handleVars(rs.GetVariable, "var test", true) + + // Substitutions. + assertIn(rs.sub, "what's", false) + rs.SetSubstitution("what's", "what is") + assertIn(rs.sub, "what's", true) + rs.SetSubstitution("what's", "undefined") + assertIn(rs.sub, "what's", false) + + // Person substitutions. + assertIn(rs.person, "you", false) + rs.SetPerson("you", "me") + assertIn(rs.person, "you", true) + rs.SetPerson("you", "undefined") + assertIn(rs.person, "you", false) +} + +func TestDebug(t *testing.T) { + bot := NewTestWithConfig(t, true, false, memory.New()) + bot.bot.say("Debug line.") + bot.bot.warn("Warning line.") +} diff --git a/src/debug.go b/src/debug.go index 4ad4fb8..a0ace3a 100644 --- a/src/debug.go +++ b/src/debug.go @@ -15,7 +15,9 @@ func (rs *RiveScript) say(message string, a ...interface{}) { // warn prints a warning message for non-fatal errors func (rs *RiveScript) warn(message string, a ...interface{}) { - fmt.Printf("[WARN] "+message+"\n", a...) + if !rs.Quiet { + fmt.Printf("[WARN] "+message+"\n", a...) + } } // warnSyntax is like warn but takes a filename and line number. @@ -24,6 +26,7 @@ func (rs *RiveScript) warnSyntax(message string, filename string, lineno int, a rs.warn(message, a...) } +// DumpTopics prints the topic structure to the terminal. func (rs *RiveScript) DumpTopics() { for topic, data := range rs.topics { fmt.Printf("Topic: %s\n", topic) @@ -45,6 +48,7 @@ func (rs *RiveScript) DumpTopics() { } } +// DumpSorted prints the sorted structure to the terminal. func (rs *RiveScript) DumpSorted() { rs._dumpSorted(rs.sorted.topics, "Topics") rs._dumpSorted(rs.sorted.thats, "Thats") diff --git a/src/errors.go b/src/errors.go new file mode 100644 index 0000000..49a0640 --- /dev/null +++ b/src/errors.go @@ -0,0 +1,12 @@ +package rivescript + +import "errors" + +// The types of errors returned by RiveScript. +var ( + ErrDeepRecursion = errors.New("Deep Recursion Detected") + ErrRepliesNotSorted = errors.New("Replies Not Sorted") + ErrNoDefaultTopic = errors.New("No default topic 'random' was found") + ErrNoTriggerMatched = errors.New("No Trigger Matched") + ErrNoReplyFound = errors.New("The trigger matched but yielded no reply") +) diff --git a/src/inheritance.go b/src/inheritance.go index f504e37..85b8f6e 100644 --- a/src/inheritance.go +++ b/src/inheritance.go @@ -104,7 +104,7 @@ func (rs *RiveScript) _getTopicTriggers(topic string, topics map[string]*astTopi // Does this topic include others? if _, ok := rs.includes[topic]; ok { - for includes, _ := range rs.includes[topic] { + 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)...) } @@ -112,7 +112,7 @@ func (rs *RiveScript) _getTopicTriggers(topic string, topics map[string]*astTopi // Does this topic inherit others? if _, ok := rs.inherits[topic]; ok { - for inherits, _ := range rs.inherits[topic] { + 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)...) } @@ -151,10 +151,10 @@ func (rs *RiveScript) getTopicTree(topic string, depth uint) []string { // Collect an array of all topics. topics := []string{topic} - for includes, _ := range rs.topics[topic].includes { + for includes := range rs.includes[topic] { topics = append(topics, rs.getTopicTree(includes, depth+1)...) } - for inherits, _ := range rs.topics[topic].inherits { + for inherits := range rs.inherits[topic] { topics = append(topics, rs.getTopicTree(inherits, depth+1)...) } diff --git a/src/loading.go b/src/loading.go index e108317..c9caed7 100644 --- a/src/loading.go +++ b/src/loading.go @@ -4,19 +4,19 @@ package rivescript import ( "bufio" - "errors" "fmt" "os" "path/filepath" "strings" ) +// LoadFile loads a single file into memory. func (rs *RiveScript) LoadFile(path string) error { rs.say("Load RiveScript file: %s", path) fh, err := os.Open(path) if err != nil { - return errors.New(fmt.Sprintf("Failed to open file %s: %s", path, err)) + return fmt.Errorf("Failed to open file %s: %s", path, err) } defer fh.Close() @@ -31,6 +31,7 @@ func (rs *RiveScript) LoadFile(path string) error { return rs.parse(path, lines) } +// LoadDirectory loads a directory of files into memory. func (rs *RiveScript) LoadDirectory(path string, extensions ...string) error { if len(extensions) == 0 { extensions = []string{".rive", ".rs"} @@ -38,9 +39,15 @@ func (rs *RiveScript) LoadDirectory(path string, extensions ...string) error { files, err := filepath.Glob(fmt.Sprintf("%s/*", path)) if err != nil { - return errors.New(fmt.Sprintf("Failed to open folder %s: %s", path, err)) + return fmt.Errorf("Failed to open folder %s: %s", path, err) } + // No files matched? + if len(files) == 0 { + return fmt.Errorf("No RiveScript source files were found in %s", path) + } + + var anyValid bool for _, f := range files { // Restrict file extensions. validExtension := false @@ -52,6 +59,7 @@ func (rs *RiveScript) LoadDirectory(path string, extensions ...string) error { } if validExtension { + anyValid = true err := rs.LoadFile(f) if err != nil { return err @@ -59,40 +67,15 @@ func (rs *RiveScript) LoadDirectory(path string, extensions ...string) error { } } + if !anyValid { + return fmt.Errorf("No RiveScript source files were found in %s", path) + } + return nil } +// Stream dynamically loads RiveScript source from a string. func (rs *RiveScript) Stream(code string) error { lines := strings.Split(code, "\n") return rs.parse("Stream()", lines) } - -func (rs *RiveScript) SortReplies() { - // (Re)initialize the sort cache. - rs.sorted.topics = map[string][]sortedTriggerEntry{} - rs.sorted.thats = map[string][]sortedTriggerEntry{} - rs.say("Sorting triggers...") - - // Loop through all the topics. - for topic, _ := range rs.topics { - rs.say("Analyzing topic %s", topic) - - // 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) - - // 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) - - // And sort them, too. - rs.sorted.thats[topic] = rs.sortTriggerSet(thatTriggers, false) - } - - // Sort the substitution lists. - rs.sorted.sub = sortList(rs.sub) - rs.sorted.person = sortList(rs.person) -} diff --git a/src/object_test.go b/src/object_test.go new file mode 100644 index 0000000..19bf4bb --- /dev/null +++ b/src/object_test.go @@ -0,0 +1,89 @@ +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) { + bot := NewTest(t) + bot.bot.SetHandler("text", &MockHandler{ + codes: map[string]string{}, + }) + bot.extend(` + > object hello text + Hello world! + < object + + > object goodbye javascript + return "Goodbye"; + < object + + + hello + - hello + + + goodbye + - goodbye + `) + bot.reply("hello", "Hello world!") + bot.reply("goodbye", "[ERR: Object Not Found]") +} + +// Mock macro handler. +type MockHandler struct { + codes map[string]string +} + +func (m *MockHandler) Load(name string, code []string) { + m.codes[name] = strings.Join(code, "\n") +} + +func (m *MockHandler) Call(name string, fields []string) string { + return m.codes[name] +} diff --git a/src/parser.go b/src/parser.go index f1dafd4..d43a650 100644 --- a/src/parser.go +++ b/src/parser.go @@ -12,28 +12,28 @@ func (rs *RiveScript) parse(path string, lines []string) error { // Get all of the "begin" type variables for k, v := range AST.Begin.Global { - if v == "" { + if v == UNDEFTAG { delete(rs.global, k) } else { rs.global[k] = v } } for k, v := range AST.Begin.Var { - if v == "" { - delete(rs.var_, k) + if v == UNDEFTAG { + delete(rs.vars, k) } else { - rs.var_[k] = v + rs.vars[k] = v } } for k, v := range AST.Begin.Sub { - if v == "" { + if v == UNDEFTAG { delete(rs.sub, k) } else { rs.sub[k] = v } } for k, v := range AST.Begin.Person { - if v == "" { + if v == UNDEFTAG { delete(rs.person, k) } else { rs.person[k] = v @@ -54,10 +54,10 @@ func (rs *RiveScript) parse(path string, lines []string) error { } // Merge in the topic inclusions/inherits. - for included, _ := range data.Includes { + for included := range data.Includes { rs.includes[topic][included] = true } - for inherited, _ := range data.Inherits { + for inherited := range data.Inherits { rs.inherits[topic][inherited] = true } diff --git a/src/regexp.go b/src/regexp.go index 0740f69..119dda1 100644 --- a/src/regexp.go +++ b/src/regexp.go @@ -1,26 +1,32 @@ package rivescript -// Common regular expressions. - import "regexp" -var re_weight = regexp.MustCompile(`\{weight=(\d+)\}`) -var re_inherits = regexp.MustCompile(`\{inherits=(\d+)\}`) -var re_meta = regexp.MustCompile(`[\<>]+`) -var re_symbols = regexp.MustCompile(`[.?,!;:@#$%^&*()]+`) -var re_nasties = regexp.MustCompile(`[^A-Za-z0-9 ]`) -var re_zerowidthstar = regexp.MustCompile(`^\*$`) -var re_optional = regexp.MustCompile(`\[(.+?)\]`) -var re_array = regexp.MustCompile(`@(.+?)\b`) -var re_botvar = regexp.MustCompile(``) -var re_uservar = regexp.MustCompile(``) -var re_input = regexp.MustCompile(``) -var re_reply = regexp.MustCompile(``) -var re_random = regexp.MustCompile(`\{random\}(.+?)\{/random\}`) -var re_anytag = regexp.MustCompile(`<([^<]+?)>`) -var re_topic = regexp.MustCompile(`\{topic=(.+?)\}`) -var re_redirect = regexp.MustCompile(`\{@(.+?)\}`) -var re_call = regexp.MustCompile(`(.+?)`) -var re_condition = regexp.MustCompile(`^(.+?)\s+(==|eq|!=|ne|<>|<|<=|>|>=)\s+(.*?)$`) -var re_set = regexp.MustCompile(``) -var re_placeholder = regexp.MustCompile(`\x00(\d+)\x00`) +// Commonly used regular expressions. +var ( + reWeight = regexp.MustCompile(`\{weight=(\d+)\}`) + reInherits = regexp.MustCompile(`\{inherits=(\d+)\}`) + reMeta = regexp.MustCompile(`[\<>]+`) + reSymbols = regexp.MustCompile(`[.?,!;:@#$%^&*()]+`) + reNasties = regexp.MustCompile(`[^A-Za-z0-9 ]`) + reZerowidthstar = regexp.MustCompile(`^\*$`) + reOptional = regexp.MustCompile(`\[(.+?)\]`) + reArray = regexp.MustCompile(`@(.+?)\b`) + reBotvars = regexp.MustCompile(``) + reUservars = regexp.MustCompile(``) + reInput = regexp.MustCompile(``) + reReply = regexp.MustCompile(``) + reRandom = regexp.MustCompile(`\{random\}(.+?)\{/random\}`) + + // Self-contained tags like that contain no nested tag. + reAnytag = regexp.MustCompile(`<([^<]+?)>`) + + reTopic = regexp.MustCompile(`\{topic=(.+?)\}`) + reRedirect = regexp.MustCompile(`\{@(.+?)\}`) + reCall = regexp.MustCompile(`(.+?)`) + reCondition = regexp.MustCompile(`^(.+?)\s+(==|eq|!=|ne|<>|<|<=|>|>=)\s+(.*?)$`) + reSet = regexp.MustCompile(``) + + // Placeholders used during substitutions. + rePlaceholder = regexp.MustCompile(`\x00(\d+)\x00`) +) diff --git a/src/reply_test.go b/src/reply_test.go index b1892ec..7a765ef 100644 --- a/src/reply_test.go +++ b/src/reply_test.go @@ -78,6 +78,12 @@ func TestRedirects(t *testing.T) { + hi there - {@hello} + // Infinite recursion between these two. + + one + @ two + + two + @ one + // Variables can throw off redirects with their capitalizations, // so make sure redirects handle this properly. ! var master = Kirsle @@ -94,6 +100,8 @@ func TestRedirects(t *testing.T) { bot.reply("my name is Kirsle", "That's my botmaster's name too.") bot.reply("call me kirsle", "That's my botmaster's name too.") + + bot.replyError("one", ErrDeepRecursion) } func TestConditionals(t *testing.T) { @@ -114,8 +122,8 @@ func TestConditionals(t *testing.T) { * == true => Yes. - No. `) - age_q := "What can I do?" - bot.reply(age_q, "I don't know.") + ageQ := "What can I do?" + bot.reply(ageQ, "I don't know.") ages := map[string]string{ "16": "Not much of anything.", @@ -128,7 +136,7 @@ func TestConditionals(t *testing.T) { } for age, expect := range ages { bot.reply(fmt.Sprintf("I am %s years old.", age), "OK.") - bot.reply(age_q, expect) + bot.reply(ageQ, expect) } bot.reply("Am I your master?", "No.") diff --git a/src/rivescript.go b/src/rivescript.go index 71aae27..28a0f97 100644 --- a/src/rivescript.go +++ b/src/rivescript.go @@ -28,12 +28,14 @@ import ( "github.com/aichaos/rivescript-go/sessions/memory" ) +// RiveScript is the bot instance. type RiveScript struct { // Parameters Debug bool // Debug mode Strict bool // Strictly enforce RiveScript syntax Depth uint // Max depth for recursion UTF8 bool // Support UTF-8 RiveScript code + Quiet bool // Suppress all warnings from being emitted UnicodePunctuation *regexp.Regexp // Internal helpers @@ -42,7 +44,7 @@ type RiveScript struct { // Internal data structures cLock sync.Mutex // Lock for config variables. global map[string]string // 'global' variables - var_ map[string]string // 'var' bot variables + vars map[string]string // 'var' bot variables sub map[string]string // 'sub' substitutions person map[string]string // 'person' substitutions array map[string][]string // 'array' @@ -57,13 +59,15 @@ type RiveScript struct { sorted *sortBuffer // Sorted data from SortReplies() // State information. - currentUser string + inReplyContext bool + currentUser string } /****************************************************************************** * Constructor and Debug Methods * ******************************************************************************/ +// New creates a new RiveScript instance with the default configuration. func New() *RiveScript { rs := new(RiveScript) @@ -84,7 +88,7 @@ func New() *RiveScript { // Initialize all the data structures. rs.global = map[string]string{} - rs.var_ = map[string]string{} + rs.vars = map[string]string{} rs.sub = map[string]string{} rs.person = map[string]string{} rs.array = map[string][]string{} @@ -111,22 +115,7 @@ func (rs *RiveScript) Configure(debug, strict, utf8 bool, depth uint, rs.sessions = sessions } -func (rs *RiveScript) SetDebug(value bool) { - rs.Debug = value -} - -func (rs *RiveScript) SetUTF8(value bool) { - rs.UTF8 = value -} - -func (rs *RiveScript) SetStrict(value bool) { - rs.Strict = value -} - -func (rs *RiveScript) SetDepth(value uint) { - rs.Depth = value -} - +// SetUnicodePunctuation allows for overriding the regexp for punctuation. func (rs *RiveScript) SetUnicodePunctuation(value string) { rs.UnicodePunctuation = regexp.MustCompile(value) } diff --git a/src/rivescript_test.go b/src/rivescript_test.go new file mode 100644 index 0000000..668add4 --- /dev/null +++ b/src/rivescript_test.go @@ -0,0 +1,29 @@ +package rivescript + +import "testing" + +// Test that you get an error if you didn't call SortReplies(). +func TestNoSorting(t *testing.T) { + bot := NewTest(t) + bot.bot.Quiet = true // Suppress warnings + bot.replyError("hello bot", ErrRepliesNotSorted) +} + +// Test failing to load replies. +func TestFailedLoading(t *testing.T) { + bot := New() + err := bot.LoadFile("/root/notexist345613123098") + if err == nil { + t.Errorf("I tried to load an obviously missing file, but I succeeded unexpectedly") + } + + err = bot.LoadDirectory("/root/notexist412901890281") + if err == nil { + t.Errorf("I tried to load an obviously missing directory, but I succeeded unexpectedly") + } + + err = bot.SortReplies() + if err == nil { + t.Errorf("I was expecting an error from SortReplies, but I didn't get one") + } +} diff --git a/src/sessions_test.go b/src/sessions_test.go index 10cea5d..7678704 100644 --- a/src/sessions_test.go +++ b/src/sessions_test.go @@ -31,6 +31,8 @@ var commonSessionTest = ` func TestNullSession(t *testing.T) { bot := NewTestWithConfig(t, false, false, null.New()) + bot.bot.Quiet = true // Suppress warnings + bot.extend(commonSessionTest) bot.reply("My name is Aiden", "Nice to meet you, undefined.") bot.reply("Who am I?", "Aren't you undefined?") @@ -63,16 +65,16 @@ func TestFreezeThaw(t *testing.T) { bot.reply("My name is Aiden", "Nice to meet you, Aiden.") bot.reply("Who am I?", "Aren't you Aiden?") - bot.RS().FreezeUservars(bot.username) + bot.bot.FreezeUservars(bot.username) bot.reply("My name is Bob", "Nice to meet you, Bob.") bot.reply("Who am I?", "Aren't you Bob?") - bot.RS().ThawUservars(bot.username, sessions.Thaw) + bot.bot.ThawUservars(bot.username, sessions.Thaw) bot.reply("Who am I?", "Aren't you Aiden?") - bot.RS().FreezeUservars(bot.username) + bot.bot.FreezeUservars(bot.username) bot.reply("My name is Bob", "Nice to meet you, Bob.") bot.reply("Who am I?", "Aren't you Bob?") - bot.RS().ThawUservars(bot.username, sessions.Discard) + bot.bot.ThawUservars(bot.username, sessions.Discard) bot.reply("Who am I?", "Aren't you Bob?") } diff --git a/src/sorting.go b/src/sorting.go index a0b0d4d..80f5f8d 100644 --- a/src/sorting.go +++ b/src/sorting.go @@ -3,11 +3,55 @@ package rivescript // Data sorting functions import ( + "errors" "sort" "strconv" "strings" ) +// SortReplies prepares the internal sort buffers to get ready for replying. +func (rs *RiveScript) SortReplies() error { + // (Re)initialize the sort cache. + rs.sorted.topics = map[string][]sortedTriggerEntry{} + rs.sorted.thats = map[string][]sortedTriggerEntry{} + rs.say("Sorting triggers...") + + // If there are no topics, give an error. + if len(rs.topics) == 0 { + return errors.New("SortReplies: no topics were found; did you load any RiveScript code?") + } + + // Loop through all the topics. + for topic := range rs.topics { + rs.say("Analyzing topic %s", topic) + + // 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) + + // 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) + + // And sort them, too. + rs.sorted.thats[topic] = rs.sortTriggerSet(thatTriggers, false) + } + + // Sort the substitution lists. + rs.sorted.sub = sortList(rs.sub) + rs.sorted.person = sortList(rs.person) + + // Did we sort anything at all? + if len(rs.sorted.topics) == 0 && len(rs.sorted.thats) == 0 { + return errors.New("SortReplies: ended up with empty trigger lists; did you load any RiveScript code?") + } + + return nil +} + /* sortTriggerSet sorts a group of triggers in an optimal sorting order. @@ -33,7 +77,7 @@ func (rs *RiveScript) sortTriggerSet(triggers []sortedTriggerEntry, excludePrevi } // Check the trigger text for any {weight} tags, default being 0 - match := re_weight.FindStringSubmatch(trig.trigger) + match := reWeight.FindStringSubmatch(trig.trigger) weight := 0 if len(match) > 0 { weight, _ = strconv.Atoi(match[1]) @@ -79,7 +123,7 @@ func (rs *RiveScript) sortTriggerSet(triggers []sortedTriggerEntry, excludePrevi rs.say("Looking at trigger: %s", pattern) // See if the trigger has an {inherits} tag. - match := re_inherits.FindStringSubmatch(pattern) + match := reInherits.FindStringSubmatch(pattern) if len(match) > 0 { inherits, _ = strconv.Atoi(match[1]) if inherits > highestInherits { @@ -87,7 +131,7 @@ func (rs *RiveScript) sortTriggerSet(triggers []sortedTriggerEntry, excludePrevi } rs.say("Trigger belongs to a topic that inherits other topics. "+ "Level=%d", inherits) - pattern = re_inherits.ReplaceAllString(pattern, "") + pattern = reInherits.ReplaceAllString(pattern, "") } else { inherits = -1 } @@ -194,7 +238,7 @@ func sortList(dict map[string]string) []string { track := map[int][]string{} // Loop through each item. - for item, _ := range dict { + for item := range dict { cnt := wordCount(item, true) if _, ok := track[cnt]; !ok { track[cnt] = []string{} @@ -204,7 +248,7 @@ func sortList(dict map[string]string) []string { // Sort them by word count, descending. sortedCounts := []int{} - for cnt, _ := range track { + for cnt := range track { sortedCounts = append(sortedCounts, cnt) } sort.Sort(sort.Reverse(sort.IntSlice(sortedCounts))) @@ -301,14 +345,14 @@ func sortByLength(running []sortedTriggerEntry, triggers []sortedTriggerEntry) [ // initSortTrack initializes a new, empty sortTrack object. func initSortTrack() *sortTrack { - track := new(sortTrack) - track.atomic = map[int][]sortedTriggerEntry{} - track.option = map[int][]sortedTriggerEntry{} - track.alpha = map[int][]sortedTriggerEntry{} - track.number = map[int][]sortedTriggerEntry{} - track.wild = map[int][]sortedTriggerEntry{} - track.pound = []sortedTriggerEntry{} - track.under = []sortedTriggerEntry{} - track.star = []sortedTriggerEntry{} - return track + return &sortTrack{ + atomic: map[int][]sortedTriggerEntry{}, + option: map[int][]sortedTriggerEntry{}, + alpha: map[int][]sortedTriggerEntry{}, + number: map[int][]sortedTriggerEntry{}, + wild: map[int][]sortedTriggerEntry{}, + pound: []sortedTriggerEntry{}, + under: []sortedTriggerEntry{}, + star: []sortedTriggerEntry{}, + } } diff --git a/src/structs.go b/src/structs.go index bd57c22..df98746 100644 --- a/src/structs.go +++ b/src/structs.go @@ -2,6 +2,15 @@ package rivescript // Miscellaneous structures +// Forms of undefined. +const ( + // UNDEFINED is the text "undefined", the default text for variable getters. + UNDEFINED = "undefined" + + // UNDEFTAG is the "" tag for unsetting variables in !Definitions. + UNDEFTAG = "" +) + // Subroutine is a Golang function type for defining an object macro in Go. // TODO: get this exportable to third party devs somehow type Subroutine func(*RiveScript, []string) string diff --git a/src/tags.go b/src/tags.go index ec744f9..020a631 100644 --- a/src/tags.go +++ b/src/tags.go @@ -23,12 +23,12 @@ func (rs *RiveScript) formatMessage(msg string, botReply bool) string { // In UTF-8 mode, only strip metacharacters and HTML brackets (to protect // against obvious XSS attacks). if rs.UTF8 { - msg = re_meta.ReplaceAllString(msg, "") + msg = reMeta.ReplaceAllString(msg, "") msg = rs.UnicodePunctuation.ReplaceAllString(msg, "") // For the bot's reply, also strip common punctuation. if botReply { - msg = re_symbols.ReplaceAllString(msg, "") + msg = reSymbols.ReplaceAllString(msg, "") } } else { // For everything else, strip all non-alphanumerics. @@ -42,14 +42,14 @@ func (rs *RiveScript) formatMessage(msg string, botReply bool) string { func (rs *RiveScript) triggerRegexp(username string, pattern string) string { // If the trigger is simply '*' then the * needs to become (.*?) // to match the blank string too. - pattern = re_zerowidthstar.ReplaceAllString(pattern, "") + pattern = reZerowidthstar.ReplaceAllString(pattern, "") // Simple replacements. pattern = strings.Replace(pattern, "*", `(.+?)`, -1) pattern = strings.Replace(pattern, "#", `(\d+?)`, -1) pattern = strings.Replace(pattern, "_", `(\w+?)`, -1) - pattern = re_weight.ReplaceAllString(pattern, "") // Remove {weight} tags - pattern = re_inherits.ReplaceAllString(pattern, "") // Remove {inherits} tags + pattern = reWeight.ReplaceAllString(pattern, "") // Remove {weight} tags + pattern = reInherits.ReplaceAllString(pattern, "") // Remove {inherits} tags pattern = strings.Replace(pattern, "", `(.*?)`, -1) // UTF-8 mode special characters. @@ -59,7 +59,7 @@ func (rs *RiveScript) triggerRegexp(username string, pattern string) string { } // Optionals. - match := re_optional.FindStringSubmatch(pattern) + match := reOptional.FindStringSubmatch(pattern) var giveup uint for len(match) > 0 { giveup++ @@ -83,7 +83,7 @@ func (rs *RiveScript) triggerRegexp(username string, pattern string) string { pattern = regReplace(pattern, fmt.Sprintf(`\s*\[%s\]\s*`, quotemeta(match[1])), fmt.Sprintf(`(?:%s|(?:\s|\b)+)`, pipes)) - match = re_optional.FindStringSubmatch(pattern) + match = reOptional.FindStringSubmatch(pattern) } // _ wildcards can't match numbers! Quick note on why I did it this way: @@ -100,7 +100,7 @@ func (rs *RiveScript) triggerRegexp(username string, pattern string) string { break } - match := re_array.FindStringSubmatch(pattern) + match := reArray.FindStringSubmatch(pattern) if len(match) > 0 { name := match[1] rep := "" @@ -119,12 +119,12 @@ func (rs *RiveScript) triggerRegexp(username string, pattern string) string { break } - match := re_botvar.FindStringSubmatch(pattern) + match := reBotvars.FindStringSubmatch(pattern) if len(match) > 0 { name := match[1] rep := "" - if _, ok := rs.var_[name]; ok { - rep = stripNasties(rs.var_[name]) + if _, ok := rs.vars[name]; ok { + rep = stripNasties(rs.vars[name]) } pattern = strings.Replace(pattern, fmt.Sprintf(``, name), strings.ToLower(rep), -1) } @@ -138,14 +138,16 @@ func (rs *RiveScript) triggerRegexp(username string, pattern string) string { break } - match := re_uservar.FindStringSubmatch(pattern) + match := reUservars.FindStringSubmatch(pattern) if len(match) > 0 { name := match[1] - rep := "undefined" - if value, err := rs.sessions.Get(username, name); err == nil { - rep = value + + value, err := rs.sessions.Get(username, name) + if err != nil { + value = UNDEFINED } - pattern = strings.Replace(pattern, fmt.Sprintf(``, name), strings.ToLower(rep), -1) + + pattern = strings.Replace(pattern, fmt.Sprintf(``, name), strings.ToLower(value), -1) } } @@ -167,8 +169,8 @@ func (rs *RiveScript) triggerRegexp(username string, pattern string) string { pattern = strings.Replace(pattern, inputPattern, history.Input[i-1], -1) pattern = strings.Replace(pattern, replyPattern, history.Reply[i-1], -1) } else { - pattern = strings.Replace(pattern, inputPattern, "undefined", -1) - pattern = strings.Replace(pattern, replyPattern, "undefined", -1) + pattern = strings.Replace(pattern, inputPattern, UNDEFINED, -1) + pattern = strings.Replace(pattern, replyPattern, UNDEFINED, -1) } } } @@ -201,10 +203,10 @@ func (rs *RiveScript) processTags(username string, message string, reply string, botstars := []string{""} botstars = append(botstars, bst...) if len(stars) == 1 { - stars = append(stars, "undefined") + stars = append(stars, UNDEFINED) } if len(botstars) == 1 { - botstars = append(botstars, "undefined") + botstars = append(botstars, UNDEFINED) } // Tag shortcuts. @@ -216,7 +218,7 @@ func (rs *RiveScript) processTags(username string, message string, reply string, reply = strings.Replace(reply, "", "{lowercase}{/lowercase}", -1) // Weight and star tags. - reply = re_weight.ReplaceAllString(reply, "") // Remove {weight} tags. + reply = reWeight.ReplaceAllString(reply, "") // Remove {weight} tags. reply = strings.Replace(reply, "", stars[1], -1) reply = strings.Replace(reply, "", botstars[1], -1) for i := 1; i < len(stars); i++ { @@ -244,7 +246,7 @@ func (rs *RiveScript) processTags(username string, message string, reply string, reply = strings.Replace(reply, `\#`, "#", -1) // {random} - match := re_random.FindStringSubmatch(reply) + match := reRandom.FindStringSubmatch(reply) var giveup uint for len(match) > 0 { giveup++ @@ -253,7 +255,7 @@ func (rs *RiveScript) processTags(username string, message string, reply string, break } - random := []string{} + var random []string text := match[1] if strings.Index(text, "|") > -1 { random = strings.Split(text, "|") @@ -267,7 +269,7 @@ func (rs *RiveScript) processTags(username string, message string, reply string, } reply = strings.Replace(reply, fmt.Sprintf("{random}%s{/random}", text), output, -1) - match = re_random.FindStringSubmatch(reply) + match = reRandom.FindStringSubmatch(reply) } // Person substitution and string formatting. @@ -284,7 +286,7 @@ func (rs *RiveScript) processTags(username string, message string, reply string, } content := match[1] - replace := "" + var replace string if format == "person" { replace = rs.substitute(content, rs.person, rs.sorted.person) } else { @@ -303,7 +305,7 @@ func (rs *RiveScript) processTags(username string, message string, reply string, reply = strings.Replace(reply, "", "{/__call__}", -1) for { // Look for tags that don't contain any other tags inside them. - matcher := re_anytag.FindStringSubmatch(reply) + matcher := reAnytag.FindStringSubmatch(reply) if len(matcher) == 0 { break // No tags left! } @@ -322,7 +324,7 @@ func (rs *RiveScript) processTags(username string, message string, reply string, // and work similarly var target map[string]string if tag == "bot" { - target = rs.var_ + target = rs.vars } else { target = rs.global } @@ -337,7 +339,7 @@ func (rs *RiveScript) processTags(username string, message string, reply string, if _, ok := target[data]; ok { insert = target[data] } else { - insert = "undefined" + insert = UNDEFINED } } } else if tag == "set" { @@ -403,7 +405,7 @@ func (rs *RiveScript) processTags(username string, message string, reply string, // user vars insert, err = rs.sessions.Get(username, data) if err != nil { - insert = "undefined" + insert = UNDEFINED } } else { // Unrecognized tag; preserve it. @@ -418,7 +420,7 @@ func (rs *RiveScript) processTags(username string, message string, reply string, reply = strings.Replace(reply, "\x01", ">", -1) // Topic setter. - match = re_topic.FindStringSubmatch(reply) + match = reTopic.FindStringSubmatch(reply) giveup = 0 for len(match) > 0 { giveup++ @@ -430,11 +432,11 @@ func (rs *RiveScript) processTags(username string, message string, reply string, name := match[1] rs.sessions.Set(username, map[string]string{"topic": name}) reply = strings.Replace(reply, fmt.Sprintf("{topic=%s}", name), "", -1) - match = re_topic.FindStringSubmatch(reply) + match = reTopic.FindStringSubmatch(reply) } // Inline redirector. - match = re_redirect.FindStringSubmatch(reply) + match = reRedirect.FindStringSubmatch(reply) giveup = 0 for len(match) > 0 { giveup++ @@ -445,15 +447,18 @@ func (rs *RiveScript) processTags(username string, message string, reply string, target := match[1] rs.say("Inline redirection to: %s", target) - subreply := rs.getReply(username, strings.TrimSpace(target), false, step+1) + subreply, err := rs.getReply(username, strings.TrimSpace(target), false, step+1) + if err != nil { + subreply = err.Error() + } reply = strings.Replace(reply, fmt.Sprintf("{@%s}", target), subreply, -1) - match = re_redirect.FindStringSubmatch(reply) + match = reRedirect.FindStringSubmatch(reply) } // Object caller. reply = strings.Replace(reply, "{__call__}", "", -1) reply = strings.Replace(reply, "{/__call__}", "", -1) - match = re_call.FindStringSubmatch(reply) + match = reCall.FindStringSubmatch(reply) giveup = 0 for len(match) > 0 { giveup++ @@ -483,7 +488,7 @@ func (rs *RiveScript) processTags(username string, message string, reply string, } reply = strings.Replace(reply, fmt.Sprintf("%s", match[1]), output, -1) - match = re_call.FindStringSubmatch(reply) + match = reCall.FindStringSubmatch(reply) } return reply @@ -528,7 +533,7 @@ func (rs *RiveScript) substitute(message string, subs map[string]string, sorted break } - match := re_placeholder.FindStringSubmatch(message) + match := rePlaceholder.FindStringSubmatch(message) if len(match) > 0 { i, _ := strconv.Atoi(match[1]) result := ph[i] diff --git a/src/topic_test.go b/src/topic_test.go index 0282faa..3a8e1eb 100644 --- a/src/topic_test.go +++ b/src/topic_test.go @@ -34,7 +34,6 @@ func TestPunishmentTopic(t *testing.T) { func TestTopicInheritance(t *testing.T) { bot := NewTest(t) - RS_ERR_MATCH := "ERR: No Reply Matched" bot.extend(` > topic colors + what color is the sky @@ -73,23 +72,23 @@ func TestTopicInheritance(t *testing.T) { bot.bot.SetUservar(bot.username, "topic", "colors") bot.reply("What color is the sky?", "Blue.") bot.reply("What color is the sun?", "Yellow.") - bot.reply("What color is grass?", RS_ERR_MATCH) - bot.reply("Name a Red Hat distro.", RS_ERR_MATCH) - bot.reply("Name a Debian distro.", RS_ERR_MATCH) - bot.reply("Say stuff.", RS_ERR_MATCH) + bot.replyError("What color is grass?", ErrNoTriggerMatched) + bot.replyError("Name a Red Hat distro.", ErrNoTriggerMatched) + bot.replyError("Name a Debian distro.", ErrNoTriggerMatched) + bot.replyError("Say stuff.", ErrNoTriggerMatched) bot.bot.SetUservar(bot.username, "topic", "linux") - bot.reply("What color is the sky?", RS_ERR_MATCH) - bot.reply("What color is the sun?", RS_ERR_MATCH) - bot.reply("What color is grass?", RS_ERR_MATCH) + bot.replyError("What color is the sky?", ErrNoTriggerMatched) + bot.replyError("What color is the sun?", ErrNoTriggerMatched) + bot.replyError("What color is grass?", ErrNoTriggerMatched) bot.reply("Name a Red Hat distro.", "Fedora.") bot.reply("Name a Debian distro.", "Ubuntu.") - bot.reply("Say stuff.", RS_ERR_MATCH) + bot.replyError("Say stuff.", ErrNoTriggerMatched) bot.bot.SetUservar(bot.username, "topic", "stuff") bot.reply("What color is the sky?", "Blue.") bot.reply("What color is the sun?", "Yellow.") - bot.reply("What color is grass?", RS_ERR_MATCH) + bot.replyError("What color is grass?", ErrNoTriggerMatched) bot.reply("Name a Red Hat distro.", "Fedora.") bot.reply("Name a Debian distro.", "Ubuntu.") bot.reply("Say stuff.", `"Stuff."`) @@ -97,24 +96,24 @@ func TestTopicInheritance(t *testing.T) { bot.bot.SetUservar(bot.username, "topic", "override") bot.reply("What color is the sky?", "Blue.") bot.reply("What color is the sun?", "Purple.") - bot.reply("What color is grass?", RS_ERR_MATCH) - bot.reply("Name a Red Hat distro.", RS_ERR_MATCH) - bot.reply("Name a Debian distro.", RS_ERR_MATCH) - bot.reply("Say stuff.", RS_ERR_MATCH) + bot.replyError("What color is grass?", ErrNoTriggerMatched) + bot.replyError("Name a Red Hat distro.", ErrNoTriggerMatched) + bot.replyError("Name a Debian distro.", ErrNoTriggerMatched) + bot.replyError("Say stuff.", ErrNoTriggerMatched) bot.bot.SetUservar(bot.username, "topic", "morecolors") bot.reply("What color is the sky?", "Blue.") bot.reply("What color is the sun?", "Yellow.") bot.reply("What color is grass?", "Green.") - bot.reply("Name a Red Hat distro.", RS_ERR_MATCH) - bot.reply("Name a Debian distro.", RS_ERR_MATCH) - bot.reply("Say stuff.", RS_ERR_MATCH) + bot.replyError("Name a Red Hat distro.", ErrNoTriggerMatched) + bot.replyError("Name a Debian distro.", ErrNoTriggerMatched) + bot.replyError("Say stuff.", ErrNoTriggerMatched) bot.bot.SetUservar(bot.username, "topic", "evenmore") bot.reply("What color is the sky?", "Blue.") bot.reply("What color is the sun?", "Yellow.") bot.reply("What color is grass?", "Blue, sometimes.") - bot.reply("Name a Red Hat distro.", RS_ERR_MATCH) - bot.reply("Name a Debian distro.", RS_ERR_MATCH) - bot.reply("Say stuff.", RS_ERR_MATCH) + bot.replyError("Name a Red Hat distro.", ErrNoTriggerMatched) + bot.replyError("Name a Debian distro.", ErrNoTriggerMatched) + bot.replyError("Say stuff.", ErrNoTriggerMatched) } diff --git a/src/trigger_test.go b/src/trigger_test.go index 2cdcf07..571199d 100644 --- a/src/trigger_test.go +++ b/src/trigger_test.go @@ -83,15 +83,15 @@ func TestAlternativesAndOptionals(t *testing.T) { bot.reply("aa", "Matched.") bot.reply("bb", "Matched.") bot.reply("aa bogus", "Matched.") - bot.reply("aabogus", "ERR: No Reply Matched") - bot.reply("bogus", "ERR: No Reply Matched") + bot.replyError("aabogus", ErrNoTriggerMatched) + bot.replyError("bogus", ErrNoTriggerMatched) bot.reply("hi Aiden", "Matched.") bot.reply("hi bot how are you?", "Matched.") bot.reply("yo computer what time is it?", "Matched.") - bot.reply("yoghurt is yummy", "ERR: No Reply Matched") - bot.reply("hide and seek is fun", "ERR: No Reply Matched") - bot.reply("hip hip hurrah", "ERR: No Reply Matched") + bot.replyError("yoghurt is yummy", ErrNoTriggerMatched) + bot.replyError("hide and seek is fun", ErrNoTriggerMatched) + bot.replyError("hip hip hurrah", ErrNoTriggerMatched) } func TestTriggerArrays(t *testing.T) { @@ -111,12 +111,12 @@ func TestTriggerArrays(t *testing.T) { `) bot.reply("What color is my red shirt?", "Your shirt is red.") bot.reply("What color is my blue car?", "Your car is blue.") - bot.reply("What color is my pink house?", "ERR: No Reply Matched") + bot.replyError("What color is my pink house?", ErrNoTriggerMatched) bot.reply("What color is my dark blue jacket?", "Your jacket is dark blue.") bot.reply("What color was Napoleoan's white horse?", "It was white.") bot.reply("What color was my red shirt?", "It was red.") bot.reply("I have a blue car.", "Tell me more about your car.") - bot.reply("I have a cyan car.", "ERR: No Reply Matched") + bot.replyError("I have a cyan car.", ErrNoTriggerMatched) } func TestWeightedTriggers(t *testing.T) { diff --git a/src/unicode_test.go b/src/unicode_test.go index ea38330..5e34ce1 100644 --- a/src/unicode_test.go +++ b/src/unicode_test.go @@ -66,5 +66,5 @@ func TestPunctuation(t *testing.T) { bot.bot.SetUnicodePunctuation(`xxx`) bot.reply("Hello bot", "Hello human!") - bot.reply("Hello, bot!", "ERR: No Reply Matched") + bot.replyError("Hello, bot!", ErrNoTriggerMatched) } diff --git a/src/utils.go b/src/utils.go index 9fccdfe..06bccfa 100644 --- a/src/utils.go +++ b/src/utils.go @@ -20,7 +20,7 @@ func wordCount(pattern string, all bool) int { wc := 0 for _, word := range words { if len(word) > 0 { - wc += 1 + wc++ } } @@ -29,7 +29,7 @@ func wordCount(pattern string, all bool) int { // stripNasties strips special characters out of a string. func stripNasties(pattern string) string { - return re_nasties.ReplaceAllString(pattern, "") + return reNasties.ReplaceAllString(pattern, "") } // isAtomic tells you whether a string is atomic or not. @@ -55,9 +55,8 @@ func stringFormat(format string, text string) string { } else if format == "sentence" { if len(text) > 1 { return strings.ToUpper(text[0:1]) + strings.ToLower(text[1:]) - } else { - return strings.ToUpper(text) } + return strings.ToUpper(text) } else if format == "formal" { words := strings.Split(text, " ") result := []string{} @@ -132,7 +131,7 @@ func regReplace(input string, pattern string, result string) string { match := reg.FindStringSubmatch(input) input = reg.ReplaceAllString(input, result) if len(match) > 1 { - for i, _ := range match[1:] { + for i := range match[1:] { input = strings.Replace(input, fmt.Sprintf("$%d", i), match[i], -1) } }