Skip to content

Migrating from v1.x to v2.x

Go Hagiwara edited this page Aug 11, 2019 · 1 revision

v2.0.0 Development Motives

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.

Major Interface Improvements

Removal of sarah.Runner

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.

Old Runner Usage

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))
}

New Usage Without Runner

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.

A New way to access sarah.Status

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.

Old Status Retrieval

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.
		}
	}
}

New Status Retrieval

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.
		}
	}
}

Introducing sarah.RegisterXxx to replace old sarah.WithXxx

With v1.x, developers had two options to change bot behavior:

  • One way was to call sarah.WithXxx() to create sarah.RunnerOption and pass each of them to sarah.NewRunner() as a functional option.
  • Another way was to instantiate sarah.RunnerOptions that holds zero or more sarah.RunnerOptions, add as many sarah.RunnerOption to it and then pass this to sarah.NewRunner(). The benefit of this is that developers can pass sarah.RunnerOptions around to append the desired number of sarah.RunnerOptions. However, developers still had to initialize sarah.RunnerOptions.

From v2.x, those global functions -- sarah.WithXxx -- that return sarah.RunnerOption are replaced by global functions -- sarah.RegisterXxxto register desired options. Whensarah.Run()is called, registered options are activated. In this way, a package can register its option within itsinit()` 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.

Old Usage of Functional Options

In main package

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())
}

In each plugin package

package guess

var Props = sarah.NewCommandPropsBuilder().
	// Some method calls follow.
        MustBuild()
package hello

var HelloCommand = &command{}

type command struct {
}

var _ sarah.Command = (*command)(nil)

New Usage of Register Functions

In main package

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)
}

In each plugin package

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)

Selectively Return Command Usage Instruction

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.Commands' 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.

New Features and Major Improvements

Cusmozing Handler for Escalated Errors

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.

Generalized Watcher Mechanism

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.

Minor Improvements

Employing xerrors

From v2, this project uses xerrors instead of standard errors package. https://github.com/oklahomer/go-sarah/issues/74

Updating golack

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.

Other improvements

See https://github.com/oklahomer/go-sarah/issues?utf8=%E2%9C%93&q=+label%3Av2+ to see all updates.