Plugins

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.

What is an Adhearsion 2.0 Plugin?

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.

Anatomy of a Plugin

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

Gem Plugin Structure

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.

Plugin Files

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:

  • version.rb contains the current version number for the plugin, and is used during packaging.
  • plugin.rb contains the hooks into the Adhearsion framework that are called when the plugin is loaded by the Adhearsion application.
  • controller_methods.rb contains a module that gets mixed into the base CallController class, making its methods available to all calls running in Adhearsion.
# 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.

  • The first is the init block which is invoked by Adhearsion when the plugin is first loaded. In this case, all this does is write an informational message to the log showing that the plugin was, in fact, loaded.
  • The second is the #config block that registers configuration options with the Adhearsion framework.
  • The third block is the #tasks block, which registers Rake tasks to be available within the Adhearsion application. In this case it adds a Rake task called greet_plugin:info that prints the version number of the plugin.

Plugin Initialization

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.

Plugin Configuration

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.

Plugin Rake Tasks

The #tasks method allows the plugin developer to define Rake tasks to be made available inside an Adhearsion application. Task definitions follow Rake conventions.

Making a plugin useful

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.

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

Testing your code

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.

Using the plugin

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.

Adding an entire CallController

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.

Setup

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.

Tests first!

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

The CallController

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

Routes

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
Back to Logging Continue to Best Practices
blog comments powered by Disqus