Pushr refactored with external notifiers
Refactoring
After couple of weeks in use, Pushr really needed some refactoring. There were ugly things, there was forgotten stuff. This had to go away. Beyond that, the Twitter notifier was soldered into the code and there was no way to easily add and configure more. And that really did suck, because some friends over at XNet are much more used to plain old IRC.
You can download notifiers from their repository, currently there are ones for IRC and Jabber. As will be evident, they are modeled after Integrity notifiers, so it should be trivial to adapt these.
External notifiers
So. All the notifying logic had to be ripped out and a foundation in form of Pushr::Notifier::Base had to be laid. This class provides shared logic for separate notifiers, which use plain old inheritance to get it:
# Inherit from this class in your notifiers
# See eg. http://github.com/karmi/pushr_notifiers/blob/master/irc.rb
class Base
attr_reader :config
def initialize(config={})
@config = config
log.fatal("#{self.class.name}") { "Notifier not configured!" } unless configured?
end
# Implement this method for your particular notification method
def deliver!
raise NoMethodError, "You need to implement 'deliver!' method in your notifier"
end
private
# Over-ride this method to send diferent message
def message(notification)
if notification.success
"Deployed #{notification.application} with revision ... etc"
else
"FAIL! Deploying #{notification.application} failed. Check deploy.log for details."
end
end
# Implement this method to check for notifier configuration
def configured?
raise NoMethodError, "You need to implement 'configured?' method in your notifier"
end
end
The architecture for this setup was heavily inspired by solution in Nicolás Sanguinetti's & Simon Rozet's Integrity application, which you should check out.
You can setup notifiers in Pushr's config.yml like this:
notifiers:
# You'll probably want to protect the updates for this Twitter account :)
- twitter:
username: hello
password: kitty
# For IRC see http://github.com/karmi/pushr_notifiers/blob/master/irc.rb
- irc:
uri: 'irc://irc.freenode.net:6667/test'
# For XMPP see http://github.com/karmi/pushr_notifiers/blob/master/jabber.rb
- jabber:
username: he110
password: g33k
These notifiers are then loaded like this:
def load_notifiers
@notifiers_path = CONFIG['notifiers_path'] || '../pushr_notifiers'
@notifiers = []
CONFIG['notifiers'].each do |notifier|
notifier_name, notifier_config = notifier.to_a.flatten
unless Pushr::Notifier::const_defined?(notifier_name.to_s.camelize)
begin
require File.join( File.dirname(__FILE__), @notifiers_path, notifier_name )
rescue Exception => e
raise "Notifier #{notifier_name} not found! (#{e.message})"
end
end
@notifiers << Pushr::Notifier::const_get(notifier_name.to_s.camelize).new(notifier_config)
end
end
Several things are goin' on in here. First, using the something || something_else Ruby convention, we don't force anybody to configure the obvious in config.yml. You'll most probably put notifier classes one level above Pushr's directory in a pushr_notifiers directory. So, why set this.
We then traverse the notifiers array from config.yml and look if we have something like this already declared in our app –– using ported ActiveSupport's camelize method. As is the case with Twitter notifier, which is the default notifier.
If not, we'll try to load a file with the same name as the notifier, and put an informative Exception if it couldn't be found.
After we put all notifiers into the @notifiers array, it's super-easy to call them all in one swoop:
def send_notifications
@notifiers.each { |n| n.deliver!(self) }
end
And thanks to shared logic in Pushr::Notifier::Base it's again super easy to implement (let's say) an IRC notifier, using Simon Rozet's wonderful shout-bot gem:
require 'rubygems'
require 'shout-bot' # $ sudo gem install sr-shout-bot --source=http://gems.github.com
class Pushr::Notifier::Irc < Pushr::Notifier::Base
def deliver!(notification)
return unless configured?
ShoutBot.shout(config['uri'], :as => "Pushr") do |channel|
channel.say message(notification)
end
end
private
def configured?
!config['uri'].nil?
end
end
If you're interested in this kind of programming, you should definitely check Russ Olsen's book Design Patterns in Ruby. It's last chapter, Convention over Configuration, will guide you through this magic Rails-like stuff. When you think you know Ruby rather good, this is the book which will bring your skillset to another level.
Join the discussion