Plugins

  • 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