The ability to easily add reusable functionality to a framework is one of its most important features. The Plugin system in Adhearsion 2.0 provides a wide range of integration points.
A plugin in Adhearsion, as in many other Ruby frameworks, simply represents a collection of functionality. Most often plugins add new functionality to your calls in the form of modules used as mixins to the base CallController class. This functionality is packaged as a gem to facilitate its installation, reuse, and sharing with the community. CallController methods, initializer code, integrated configuration, rake tasks and code generators all are possible with the plugin classes.
The easiest way to create a skeleton plugin is to use the Adhearsion command "ahn generate". By running the following ahn generate plugin GreetPlugin a directory named greet_plugin will be created in the current working directory. The plugin itself, being a gem, can reside anywhere, though it is recommended to keep it outside any particular Adhearsion application to make packaging easier. The output from this command should show the files being created, like this:
$ ahn generate plugin GreetPlugin create greet_plugin create greet_plugin/lib create greet_plugin/lib/greet_plugin create greet_plugin/spec create greet_plugin/greet_plugin.gemspec create greet_plugin/Rakefile create greet_plugin/README.md create greet_plugin/Gemfile create greet_plugin/lib/greet_plugin.rb create greet_plugin/lib/greet_plugin/version.rb create greet_plugin/lib/greet_plugin/plugin.rb create greet_plugin/lib/greet_plugin/controller_methods.rb create greet_plugin/spec/spec_helper.rb create greet_plugin/spec/greet_plugin/controller_methods_spec.rb
The greet_plugin.gemspec file contains information on your plugin, including required dependencies, contact information and other metadata. Edit the gemspec with contact information, the name and description of your plugin and any development and runtime dependencies to have a fully functional gem.
The README is customarily formatted in Markdown. Please take the time to write a brief description of your plugin, and especially how to use it!
The Rakefile contains tasks that pertain to the plugin gem itself, such as running unit tests. Note that it is separate from adding tasks or generators to Adhearsion applications.
The entry point for the plugin, as with most gems, resides in lib/greet_plugin.rb. It is mainly composed of requires for the plugin classes and modules. When adding functionality to a plugin, it will need to be require'd here to be available. Plugins are namespaced by package name to avoid conflicts.
lib/greet_plugin.rb:
GreetPlugin = Module.new
require "greet_plugin/version"
require "greet_plugin/plugin"
require "greet_plugin/controller_methods"
In this example Adhearsion plugin:
# lib/greet_plugin/plugin.rb
module GreetPlugin
class Plugin < Adhearsion::Plugin
# Actions to perform when the plugin is loaded
#
init :greet_plugin do
logger.info "GreetPlugin has been loaded"
end
# Basic configuration for the plugin
#
config :greet_plugin do
greeting "Hello", desc: "What to use to greet users"
end
# Defining a Rake task is easy
# The following can be invoked with:
# rake plugin_demo:info
#
tasks do
namespace :greet_plugin do
desc "Prints the PluginTemplate information"
task :info do
STDOUT.puts "GreetPlugin plugin v. #{VERSION}"
end
end
end
end
end
plugin.rb contains directives that pertain to various aspects of plugin functionality. Code above shows three examples.
Every plugin goes through two separate phases before it is ready to run. While Adhearsion is starting up, and prior to taking any calls, the plugin first gets initialized through a supplied init block. This block may be used to set up any basic requirements or validate the configuration. Later, after the Adhearsion framework has booted, the optional run block is called to start the plugin. An example of using this two step startup of init and run blocks might be an IRC plugin. In the init block, the IRC class is instantiated and configured, but no connection to the server is made. Then in run the actual connection is opened and the service begins. Both blocks are optional. An optional symbol representing the plugin name may be provided as the first argument. A plugin can also request to be initialized before or after another plugin by name, using the :before and :after options passed as an hash to init and/or run.
Note that your run block must not block indefinitely! If necessary, place the contents of your run block within a thread so that Adhearsion can continue to start the other plugins:
module GreetPlugin
class Plugin < Adhearsion::Plugin
run :greet_plugin do
Thread.new do
catching_standard_errors { my_blocking_runner_method }
end
end
end
end
Note the use of catchingstandarderrors. This ensures that any exceptions raised within your plugin are routed through Adhearsion's exception handling event system. More information on this can be found in the best practices guide.
The #config block allows a plugin to define configuration values in a customizable and self-documenting way. Every configuration key has a name followed by its default value, and then by a :desc key to allow for a description. By allowing your plugin to be configured this way, its options will be exposed via rake config:show in an application directory. Additionally, you will be able to set configuration options via the shell environment, which is handy for services like Heroku.
A config line can also validate supplied values with a transform:
max_connections 5, desc: "Maximum number of connections to make",
transform: lambda { |v| v.to_i }
The :transform will be used to modify the configuration value after it is read from the end-user's setting.
The #tasks method allows the plugin developer to define Rake tasks to be made available inside an Adhearsion application. Task definitions follow Rake conventions.
So far, the facilities Adhearsion provides to hook into the framework and your application have been shown. While simple plugins that are nothing more than rake tasks and generators have their place, you probably want to go further. This section will discuss how to add funtionality to Adhearsion calls.
A plugin is at its heart simply a Ruby gem, and bundled code needs to be loaded through requiring the proper files. The generated plugin has a single business logic file in lib/controller_methods.rb. Neither the file name nor the module name are mandatory, this is just normal Ruby code.
The generated plugin has a single business logic file in lib/controller_methods.rb. Neither the file name nor the module name are mandatory Content is as follows:
lib/controller_methods.rb
module GreetPlugin
module ControllerMethods
# The methods are defined in a normal method the user will then
# mix in their CallControllers.
# The following also contains an example of configuration usage.
#
def greet(name)
play "#{Adhearsion.config[:greet_plugin].greeting}, #{name}"
end
end
end
The module is intended to be used as a mixin in call controllers.
Module usage can be seen in action in the generated test file, which also illustrates how the call controller methods can be easily tested.
spec/greetplugin/controllermethods_spec.rb
require 'spec_helper'
module GreetPlugin
describe Plugin do
describe "mixed in to a CallController" do
class TestController < Adhearsion::CallController
include GreetPlugin::ControllerMethods
end
let(:mock_call) { mock 'Call' }
subject do
TestController.new mock_call
end
describe "#greet" do
it "greets with the correct parameter" do
subject.should_receive(:play).once.with("Hello, Luca")
subject.greet "Luca"
end
end
end
end
end
Since plugin code is a normal Ruby library, it can be tested using familiar tools like Test::Unit or RSpec.
Note that, at first, Adhearsion Routes or XMPP handlers may seem difficult to test. However, a good practice is to put all your business logic into classes and methods, and then simply invoke the methods from the routes or handlers. In this way you can maintain good code test coverage and keep your route definitions small and readable.
You have generated your new plugin, built tests and are ready to use it. Now what? The plugin is a gem, so you might eventually publish it. In the meantime you can simply use it locally by adding a path line to your application's Gemfile.
Gemfile
gem 'adhearsion', '>= 2.0.0'
gem 'greet_plugin', path: '/home/user/projects/greet_plugin'
# ... whatever other gems you need
Do not forget to run 'bundle install' after adding the gem.
It is also possible to provide a full CallController implementation that can be used out-of-the-box by your application. There is an entire section of the documentation dedicated to CallControllers. Please refer to that section for full details on available methods within CallControllers. Below you will see how to create a new controller in the plugin, complete with new configuration keys and test. Our goal is to have a controller that dials a SIP device during office hours and plays a message when the office is closed.
First, add configuration variables to allow controlling the time-of-day routing:
lib/greet_plugin/plugin.rb
config :greet_plugin do
greeting "Hello", desc: "What to use to greet users"
office_hours_start 8, desc: "Start of office hours, integer, 24 hour format"
office_hours_end 18, desc: "End of office hours, integer, 24 hour format"
end
One way to make testing time-based features easy is to use the Timecop gem. Just add it to your gemspec under the development group and add "require 'timecop'" at the top of spec/spec_helper.rb.
Now add a few tests, taking advantage of Adhearsion's testing facilities and the generated helpers. The tests describe what will be implemented in the controller.
spec/hourscontrollerspec.rb
require 'spec_helper'
module GreetPlugin
describe HoursController do
let(:mock_call) { mock 'Call' }
subject do
HoursController.new mock_call
end
it 'dials out when inside office hours' do
Timecop.freeze Time.utc(2012, 3, 8, 12, 0, 0)
subject.should_receive(:dial).once
subject.run
end
it 'plays a message when outside office hours' do
Timecop.freeze Time.utc(2012, 3, 8, 22, 0, 0)
subject.should_receive(:play).once
subject.run
end
end
end
Last but not least, the actual CallController.
lib/greetplugin/hourscontroller.rb
module GreetPlugin
class HoursController < Adhearsion::CallController
def run
if in_office_hours
dial "SIP/101"
else
say "Our office is open between #{config.office_hours_start} and #{config.office_hours_end}. Please call back later."
end
end
private
def in_office_hours
Time.now.hour.between? config.office_hours_start, config.office_hours_end
end
def config
Adhearsion.config[:greet_plugin]
end
end
end
To make calls in the application reach this controller, you will need to create a route. The example below uses a generic route that matches all calls (no filters).
myapplicationdir/config/adhearsion.rb
Adhearsion.router do
route 'default', GreetPlugin::HoursController
end