-
Notifications
You must be signed in to change notification settings - Fork 16
Migrating from v1.x to v2.x
Ever since go-sarah's initial release two years ago, this has been a huge help to the author's ChatOps. During these years, some minor improvements and bug fixes mostly without interface changes were introduced. However, a desire to add some improvements that involve drastic interface changes is still not fulfilled, leaving the author in a dilemma of either maintaining the API or adding breaking changes. Version 2 development is one such project to introduce API changes to improve go-sarah as a whole.
Previous v1.x focused on providing a group of components with minimal interfaces to maximize customizability. Dividing core features into components, defining the interface for each of them, and letting them interact with each other helped a lot to increase customizability and maintainability. In terms of handiness, developers were encouraged to define their own utility code to minimize their works. However, it was also true that many projects had to implement their own boilerplates that were quite similar to each other. Version 2 more focuses on providing some utilities to ease developers' tasks while still maintaining its customizability and maintainability while some feature improvements are also added.
Previously, developers had to call sarah.NewRunner()
to initialize sarah.Runner
and call Runner.Run()
to start bot interaction, which did not make good sense; Developers had to initialize an instance just to call Run()
method one time. To simplify such interaction, now a global function -- sarah.Run()
-- is provided. This initializes internal runner
and then call its Run()
method.
Also, some global functions such as sarah.CurrentStatus()
and sarah.RegisterXxx
are added to access such internal state so developers no longer have to pass around some instances to modify behavior or access internal state. This not only provides easier APIs, but also boosts the handiness of each package's init()
function.
func main() {
// A helper to stash sarah.RunnerOptions for later use.
options := sarah.NewRunnerOptions()
// Setup Slack bot.
// *RunnerOptions had be passed around to append desired option.
setupSlack(options)
// Setup properties to setup .guess command on the fly
options.Append(sarah.WithCommandProps(GuessProps))
// Setup sarah.Runner.
runnerConfig := sarah.NewConfig()
runner, err := sarah.NewRunner(runnerConfig, options.Arg())
if err != nil {
panic(fmt.Errorf("failed to initialize Runner: %s", err.Error()))
}
// Run sarah.Runner.
// This blocks.
go func() {
// Run sarah.Runner.
// This blocks.
runner.Run(context.TODO())
}
// Get current status
status := runner.Status()
}
func setupSlack(options *sarah.RunnerOptions) {
// Setup slack adapter.
slackConfig := slack.NewConfig()
slackConfig.Token = "REPLACE THIS"
adapter, err := slack.NewAdapter(slackConfig)
if err != nil {
panic(fmt.Errorf("faileld to setup Slack Adapter: %s", err.Error()))
}
// Setup storage.
cacheConfig := sarah.NewCacheConfig()
storage := sarah.NewUserContextStorage(cacheConfig)
// Setup Bot with slack adapter and default storage.
bot, err := sarah.NewBot(adapter, sarah.BotWithStorage(storage))
if err != nil {
panic(fmt.Errorf("faileld to setup Slack Bot: %s", err.Error()))
}
options.Append(sarah.WithBot(bot))
}
import (
// Simply import some packages to register commands.
_ "github.com/oklahomer/go-sarah/examples/simple/plugins/guess"
_ "github.com/oklahomer/go-sarah/examples/simple/plugins/hello"
)
func main() {
// Setup Slack bot.
setupSlack()
// Run.
// This does not block.
config := sarah.NewConfig()
err = sarah.Run(context.TODO(), config)
if err != nil {
panic(fmt.Errorf("failed to run: %s", err.Error()))
}
// Get current status
status := sarah.CurrentStatus()
}
func setupSlack() {
// Setup slack adapter.
slackConfig := slack.NewConfig()
slackConfig.Token = "REPLACE THIS"
adapter, err := slack.NewAdapter(slackConfig)
if err != nil {
panic(fmt.Errorf("faileld to setup Slack Adapter: %s", err.Error()))
}
// Setup storage.
cacheConfig := sarah.NewCacheConfig()
storage := sarah.NewUserContextStorage(cacheConfig)
// Setup Bot with slack adapter and default storage.
bot, err := sarah.NewBot(adapter, sarah.BotWithStorage(storage))
if err != nil {
panic(fmt.Errorf("faileld to setup Slack Bot: %s", err.Error()))
}
sarah.RegisterBot(bot)
}
See https://github.com/oklahomer/go-sarah/issues/72 for details.
Runner.Status()
was the way to fetch currently running bot status, but sarah.Runner
is now removed. Instead, global function sarah.CurrentStatus()
is given to return internal runner's status.
func main() {
// Some initialization comes here.
runner := ....
// Run.
go func() {
runner.Run(context.TODO())
}
// Supervise status. Runner instance had to be passed around.
go supervise(runner)
// The rest comes here.
}
func supervise(runner sarah.Runner) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case t := <-ticker.C:
status := runner.Status()
// Do something with this.
// Return if runner is completely stopped.
}
}
}
func main() {
// Setup and run bot.
go runBot()
// Supervise status.
// Runner instance does not have to be passed.
// sarah.CurrentStatus() can be called even if the bot is not yet fully setup.
go supervise()
// The rest comes here.
}
func supervise() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case t := <-ticker.C:
status := sarah.CurrentStatus()
// Do something with this.
// Return if runner is completely stopped.
}
}
}
With v1.x, developers had two options to change bot behavior:
- One way was to call
sarah.WithXxx()
to createsarah.RunnerOption
and pass each of them tosarah.NewRunner()
as a functional option. - Another way was to instantiate
sarah.RunnerOptions
that holds zero or moresarah.RunnerOption
s, add as manysarah.RunnerOption
to it and then pass this tosarah.NewRunner()
. The benefit of this is that developers can passsarah.RunnerOptions
around to append the desired number ofsarah.RunnerOption
s. However, developers still had to initializesarah.RunnerOptions
.
From v2.x, those global functions -- sarah.WithXxx
-- that return sarah.RunnerOption
are replaced by global functions -- sarah.RegisterXxxto register desired options. When
sarah.Run()is called, registered options are activated. In this way, a package can register its option within its
init()` function. Packages under examples/simple/plugins/ such as examples/simple/plugins/hello/props.go show such examples. With this option, the main package only has to import those packages to register options. When a manual configuration is still needed, that can be ignited from main package. e.g. O/R mapper has to be initialized and then passed to a plugin constructor. See Todo command initialization for such example.
func main() {
// A helper to stash sarah.RunnerOptions for later use.
options := sarah.NewRunnerOptions()
// All cumbersome setup flows come here.
// Setup .hello command
bot.AppendCommand(hello.Command)
// Setup properties to setup .guess command on the fly
options.Append(sarah.WithCommandProps(guess.Props))
// Setup sarah.Runner.
runnerConfig := sarah.NewConfig()
runner, err := sarah.NewRunner(runnerConfig, options.Arg())
}
package guess
var Props = sarah.NewCommandPropsBuilder().
// Some method calls follow.
MustBuild()
package hello
var HelloCommand = &command{}
type command struct {
}
var _ sarah.Command = (*command)(nil)
package main
import (
// Simply import some packages to register commands.
_ "github.com/oklahomer/go-sarah/examples/simple/plugins/guess"
_ "github.com/oklahomer/go-sarah/examples/simple/plugins/hello"
)
func main() {
// Run.
cfg := sarah.NewConfig()
sarah.Run(cfg)
}
package guess
func init() {
// Register sarah.CommandProps.
sarah.RegisterCommandProps(props)
}
var props = sarah.NewCommandPropsBuilder().
// Some method calls follow.
MustBuild()
package hello
func init() {
// Register sarah.Command instance.
sarah.RegisterCommand(slack.SLACK, &command{})
}
type command struct {
}
var _ sarah.Command = (*command)(nil)
In v1, Command.Match()
is called to see if the user input is trying to ignite its belonging sarah.Command
.
/examples/simple/plugins/morning/props.go shows such example. In a similar way, a command can check if the input is sent in a specific group. When an input is sent from a different group, then simply return false
so the users do not notice the existence of such command. However, when a user inputs a help command, all registered sarah.Command
s' Command. InputExample()
were called and results were sent back to the user. Therefore, the command for specific groups was not really hidden.
In v2, Command.InputExample()
is replaced with Command.Instruction()
. This method receives sarah.Input
just like Command.Match()
. When this returns an empty string, the usage instruction of such command is excluded from the help command's result.
See https://github.com/oklahomer/go-sarah/issues/71 for details.
Errors can be escalated from bot to sarah.Runner
through a designated function. Returned error is checked and if its type is *sarah.BotNonContinuableError
, such state is notified to administrators via registered sarah.Alerter
implementations. In v2, developers can register additional error handler to judge escalated error is worth being notified to administrators or the bot should be stopped.
See https://github.com/oklahomer/go-sarah/pull/81 for details.
Live Configuration Update is a signature feature of go-sarah
. This, however, only supported file system monitoring to notify config file update events and rebuild sarah.Command
/sarah.ScheduledTask
. Watcher mechanism is more generalized in v2 release so other configuration management mechanism can be supported.
This would help keep synced with centralized configuration management systems such as HashiCorp's Consul, LINE's Central Dogma or other similar systems.
See https://github.com/oklahomer/go-sarah/issues/82 for details.
From v2, this project uses xerrors instead of standard errors package. https://github.com/oklahomer/go-sarah/issues/74
Version 2 employs updated golack package so its slack adapter can manipulate thread message interactions. In the course of this modification, some functions to generate sarah.CommandResponse
are unified.
See https://github.com/oklahomer/go-sarah/issues?utf8=%E2%9C%93&q=+label%3Av2+ to see all updates.
To have a grasp of overall architecture, have a look at Components.