brightbox-warren 0.5 → 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,37 +1,59 @@
1
- class Warren::Connection
2
-
3
- # Creates a new connection with the options passed in.
4
- # Requires at least a :user, :pass and :vhost else will raise
5
- # InvalidConnectionDetails.
6
- def initialize opts = {}
7
- # Check they've passed in the stuff without a default on it
8
- unless opts.has_key?(:user) && opts.has_key?(:pass) && opts.has_key?(:vhost)
9
- raise InvalidConnectionDetails, "Missing a username, password or vhost."
1
+ require "yaml"
2
+ module Warren
3
+ class Connection
4
+
5
+ attr_reader :options
6
+
7
+ #
8
+ # Creates a new connection by reading the options from
9
+ # WARREN_ROOT/config/warren.yml or specify the file to read as an
10
+ # argument or by passing a hash of connection details in.
11
+ #
12
+ # Reads WARREN_ENV out of the yaml'd hash (just like ActiveRecord)
13
+ # "development" by default (and RAILS_ENV if running under rails)
14
+ #
15
+ # Raises InvalidConnectionDetails if no params are found for the current
16
+ # environment.
17
+ #
18
+ def initialize params = nil
19
+ if params.nil? || !params.is_a?(Hash)
20
+ file ||= WARREN_ROOT << "/config" << "/warren.yml"
21
+ raise InvalidConnectionDetails, "Config file not found: #{file}" unless File.exists?(file)
22
+ opts = YAML.load(file)
23
+ end
24
+ opts ||= params
25
+
26
+ opts = symbolize_keys(opts[WARREN_ENV])
27
+ check_connection_details(opts)
28
+ @options = opts
10
29
  end
11
- @opts = opts
12
- end
13
-
14
- # Returns the default queue name or returns InvalidConnectionDetails
15
- # if no default queue is defined
16
- def queue_name
17
- raise InvalidConnectionDetails, "Missing a default queue name." unless @opts.has_key?(:default_queue)
18
- @opts[:default_queue]
19
- end
20
-
21
- # Returns a hash of the connection options
22
- def options
23
- {
24
- :user => @opts[:user],
25
- :pass => @opts[:pass],
26
- :vhost => @opts[:vhost],
27
- :host => (@opts[:host] || "localhost"),
28
- :port => (@opts[:port] || ::AMQP::PORT.to_i),
29
- :logging => (@opts[:logging] || false)
30
- }
31
- end
32
-
33
- # Raised if connection details are missing or invalid
34
- # Check the error message for more details
35
- class InvalidConnectionDetails < Exception
30
+
31
+ #
32
+ # Raised if connection details are missing or invalid
33
+ # Check the error message for more details
34
+ #
35
+ InvalidConnectionDetails = Class.new(Exception)
36
+
37
+ private
38
+
39
+ #
40
+ # Changes all keys into symbols
41
+ #
42
+ def symbolize_keys(hash)
43
+ hash.each do |key, value|
44
+ hash.delete(key)
45
+ hash[key.to_sym] = value
46
+ end
47
+ end
48
+
49
+ #
50
+ # Calls the adapter to check the connection details
51
+ # Returns true or raises InvalidConnectionDetails
52
+ #
53
+ def check_connection_details params
54
+ return true unless Warren::Queue.adapter.respond_to?(:check_connection_details)
55
+ Warren::Queue.adapter.send(:check_connection_details, params)
56
+ end
57
+
36
58
  end
37
59
  end
@@ -10,41 +10,41 @@ module Warren
10
10
  # Hashes the message using a secret salt, stores the hash
11
11
  # in the message and then checks its the same when pulled
12
12
  # off the other end.
13
- #
13
+ #
14
14
  # Basic trust implementation to make sure the message
15
15
  # hasn't been tampered with in transit and came from
16
16
  # an "authorised" app.
17
- #
17
+ #
18
18
  # Make sure both the publisher and subscriber use the same
19
19
  # key else you'll get KeyValidationError error raised.
20
- #
20
+ #
21
21
  class SharedSecret
22
22
  # Raised when no key (salt) is provided
23
23
  class NoKeyError < Exception; end
24
24
  # Raised when there is a key mismatch error
25
25
  class KeyValidationError < Exception; end
26
-
26
+
27
27
  # Sets the key to use
28
28
  def self.key= key
29
29
  @@key = key
30
30
  end
31
-
31
+
32
32
  # Returns the current key
33
33
  # Raises NoKeyError if no key has been assigned yet
34
34
  def self.key
35
35
  raise NoKeyError if @@key.nil?
36
36
  @@key
37
37
  end
38
-
38
+
39
39
  # Returns the hashed message
40
- #
40
+ #
41
41
  # Expects that msg#to_s returns a string
42
42
  # to hash against.
43
- #
43
+ #
44
44
  def self.secret msg
45
45
  HMAC::SHA256.hexdigest(self.key, msg.to_s)
46
46
  end
47
-
47
+
48
48
  # Called when the message is being packed for
49
49
  # transit. Returns a hash.
50
50
  def self.pack msg
@@ -54,7 +54,7 @@ module Warren
54
54
  msg[:secret] = self.secret(msg.to_s)
55
55
  msg
56
56
  end
57
-
57
+
58
58
  # Called when unpacking the message from transit.
59
59
  # Returns the original object.
60
60
  def self.unpack msg
@@ -62,9 +62,9 @@ module Warren
62
62
  raise KeyValidationError unless msg.delete(:secret) == self.secret(msg)
63
63
  # see if its a hash we created, it'll only contain the key "secret_msg" if it is
64
64
  msg = msg[:secret_msg] if msg.keys == [:secret_msg]
65
- msg
65
+ msg
66
66
  end
67
-
67
+
68
68
  end
69
69
  end
70
70
  end
@@ -4,13 +4,13 @@ module Warren
4
4
  class MessageFilter
5
5
  # Packs the message into a YAML string
6
6
  # for transferring safely across the wire
7
- class Yaml
8
-
7
+ class Yaml < MessageFilter
8
+
9
9
  # Returns a YAML string
10
10
  def self.pack msg
11
11
  YAML.dump(msg)
12
12
  end
13
-
13
+
14
14
  # Returns original message
15
15
  def self.unpack msg
16
16
  YAML.load(msg)
@@ -1,46 +1,49 @@
1
- require File.expand_path(File.dirname(__FILE__) + "/message_filters/yaml")
2
-
3
1
  module Warren
4
2
  # Handles filtering messages going onto/coming off the queue
5
3
  class MessageFilter
6
4
  # Array of filters to be run on the message before its
7
5
  # pushed to rabbit.
8
- #
9
- # NB: These get called in reverse order from the array -
6
+ #
7
+ # NB: These get called in reverse order from the array -
10
8
  # the last filter to be added gets called first.
11
- @@filters = [Warren::MessageFilter::Yaml]
9
+ @@filters = []
12
10
 
13
11
  class << self
14
12
  # Adds a filter to the list
15
- #
13
+ #
16
14
  # A valid filter is just a class that defines
17
- # <tt>self.pack</tt> and <tt>self.unpack</tt>
15
+ # <tt>self.pack</tt> and <tt>self.unpack</tt>
18
16
  # methods, which both accept a single argument,
19
17
  # act upon it, and return the output.
20
- #
18
+ #
21
19
  # Example filter class (See also message_filters/*.rb)
22
- #
20
+ #
23
21
  # class Foo
24
22
  # def self.pack msg
25
23
  # msg.reverse # Assumes msg responds to reverse
26
24
  # end
27
- #
25
+ #
28
26
  # def self.unpack msg
29
27
  # msg.reverse # Does the opposite of Foo#pack
30
28
  # end
31
29
  # end
32
- #
30
+ #
33
31
  def << filter
34
32
  @@filters << filter
35
33
  end
36
34
  alias :add_filter :<<
37
35
  end
38
-
36
+
37
+ # Called when a subclass is created, adds the subclass to the queue
38
+ def self.inherited klass
39
+ add_filter klass
40
+ end
41
+
39
42
  # Returns current array of filters
40
43
  def self.filters
41
44
  @@filters
42
45
  end
43
-
46
+
44
47
  # Resets the filters to default
45
48
  def self.reset_filters
46
49
  @@filters = [Warren::MessageFilter::Yaml]
@@ -65,6 +68,9 @@ module Warren
65
68
  end
66
69
  msg
67
70
  end
68
-
71
+
69
72
  end
70
- end
73
+ end
74
+
75
+ # Make sure the YAML filter is added first
76
+ require File.expand_path(File.dirname(__FILE__) + "/filters/yaml")
data/lib/warren/queue.rb CHANGED
@@ -1,85 +1,69 @@
1
1
  class Warren::Queue
2
2
  @@connection = nil
3
-
3
+ @@adapter = nil
4
+
5
+ #
4
6
  # Raised if no connection has been defined yet.
5
- class NoConnectionDetails < Exception
6
- end
7
-
7
+ #
8
+ NoConnectionDetails = Class.new(Exception)
9
+
10
+ #
8
11
  # Raised if a block is expected by the method but none is given.
9
- class NoBlockGiven < Exception
10
- end
11
-
12
- # Sets the connection details
13
- def self.connection= params
14
- @@connection = params.is_a?(Warren::Connection) ? params : Warren::Connection.new(params)
12
+ #
13
+ NoBlockGiven = Class.new(Exception)
14
+
15
+ #
16
+ # Raised if an adapter isn't set
17
+ #
18
+ NoAdapterSet = Class.new(Exception)
19
+
20
+ #
21
+ # Sets the current connection
22
+ #
23
+ def self.connection= conn
24
+ @@connection = (conn.is_a?(Warren::Connection) ? conn : Warren::Connection.new(conn) )
15
25
  end
16
26
 
27
+ #
17
28
  # Returns the current connection details
18
- # Raises NoConnectionDetails if no connection details have been
19
- # assigned yet.
29
+ #
20
30
  def self.connection
21
- if @@connection.nil?
22
- raise NoConnectionDetails, "You need to set the connection details."
23
- end
24
- @@connection
31
+ @@connection ||= Warren::Connection.new
32
+ end
33
+
34
+ #
35
+ # Sets the adapter when this class is subclassed.
36
+ #
37
+ def self.inherited klass
38
+ @@adapter = klass
25
39
  end
26
40
 
27
- #
28
- # Sends a message to a queue. If successfully sent it returns
29
- # true, unless callback block is passed (see below)
30
41
  #
31
- # Warren::Queue.publish(:queue_name, {:foo => "name"})
42
+ # Sets the adapter manually
32
43
  #
33
- # Can also pass a block which is fired after the message
34
- # is sent. If a block is passed, then the return value of the block
35
- # is returned from this method.
44
+ def self.adapter= klass
45
+ @@adapter = klass
46
+ end
47
+
36
48
  #
37
- # Warren::Queue.publish(:queue_name, {:foo => "name"}) { puts "foo" }
49
+ # Returns the current adapter or raises NoAdapterSet exception
38
50
  #
39
- def self.publish queue_name, payload, &blk
40
- queue_name = self.connection.queue_name if queue_name == :default
41
- # Create a message object if it isn't one already
42
- msg = Warren::MessageFilter.pack(payload)
43
-
44
- do_connect(true, blk) do
45
- queue = MQ::Queue.new(MQ.new, queue_name)
46
- queue.publish msg.to_s
47
- end
51
+ def self.adapter
52
+ @@adapter || raise(NoAdapterSet)
53
+ end
48
54
 
55
+ #
56
+ # Publishes the message to the queue
57
+ #
58
+ def self.publish *args, &blk
59
+ self.adapter.publish(*args, &blk)
49
60
  end
50
61
 
51
- #
52
- # Subscribes to a queue and runs the block
53
- # for each message received
54
- #
55
- # Warren::Queue.subscribe("example") {|msg| puts msg }
56
62
  #
57
- # Expects a block and raises NoBlockGiven if no block is given.
63
+ # Sends the subscribe message to the adapter class
58
64
  #
59
- def self.subscribe queue_name, &block
60
- raise NoBlockGiven unless block_given?
61
- queue_name = self.connection.queue_name if queue_name == :default
62
- # todo: check if its a valid queue?
63
- do_connect(false) do
64
- queue = MQ::Queue.new(MQ.new, queue_name)
65
- queue.subscribe do |msg|
66
- msg = Warren::MessageFilter.unpack(msg)
67
- block.call(msg)
68
- end
69
- end
65
+ def self.subscribe *args, &blk
66
+ self.adapter.subscribe(*args, &blk)
70
67
  end
71
68
 
72
-
73
- private
74
-
75
- # Connects and does the stuff its told to!
76
- def self.do_connect should_stop = true, callback = nil, &block
77
- AMQP.start(self.connection.options) do
78
- block.call
79
- AMQP.stop { EM.stop_event_loop } if should_stop
80
- end
81
- # Returns the block return value or true
82
- callback.nil? ? true : callback.call
83
- end
84
-
85
69
  end
data/readme.rdoc CHANGED
@@ -1,19 +1,26 @@
1
1
  = Warren
2
2
 
3
- Library for pushing messages onto RabbitMQ queues, and receiving them at the other end.
3
+ Library for sending and receiving messages, complete with en/decrypting messages on either side of the transport.
4
4
 
5
- It handles authentication + filtering messages with custom classes if needed.
5
+ It was written to handle sending messages between two nodes using RabbitMQ, which is why the two default adapters are for synchronous and asynchronous rabbitmq client libraries. You can delegate the sending & receiving to any custom class you want, simply by subclassing Warren::Queue. (Isn't ruby magic marvelous!)
6
6
 
7
- Start with Warren::Queue for details and see also examples/
7
+ The filtering works in much the same way as the adapter class. There is a default YAML filter that is always called last before sending the message and first when receiving the message, simply to make sure the message is a string when sent + received. You can then add custom classes onto the stack in any order you want, simply by subclassing Warren::MessageFilter. Add them in the same order on the receiving side and warren takes care of calling them in reverse order.
8
8
 
9
- == Released under the MIT Licence
9
+ Start by looking at examples/ to see how to use it, and then lib/warren/adapters/ to see how to implement your own adapter class and lib/warren/filters to see how to implement your own filters.
10
10
 
11
- Copyright (c) 2008 Brightbox Systems Ltd
11
+ == Installation
12
12
 
13
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
13
+ gem install brightbox-warren
14
14
 
15
- * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
15
+ == Usage
16
16
 
17
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ require "rubygems"
18
+ require "warren"
19
+ # Pull in the bunny adapter
20
+ require "warren/adapters/bunny_adapter"
21
+
22
+ # See examples/ for more
18
23
 
19
- See http://www.brightbox.co.uk/ for contact details.
24
+ == License
25
+
26
+ Licensed under the MIT license. See LICENSE for more details.
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1 @@
1
1
  require File.join(File.dirname(__FILE__) + "/../lib/warren")
2
-
3
- # Hash#only and Hash#except
4
- require File.join(File.dirname(__FILE__) + "/hash_extend")
@@ -3,68 +3,87 @@ require File.dirname(__FILE__) + '/../spec_helper'
3
3
  describe Warren::Connection do
4
4
 
5
5
  before(:each) do
6
- @c = Warren::Connection.new(details)
6
+ @file = stub 'io', :read => yaml_data
7
+ setup_adapter
7
8
  end
8
-
9
- it "should require a username" do
10
- lambda {
11
- Warren::Connection.new(details.except(:user))
12
- }.should raise_error(Warren::Connection::InvalidConnectionDetails)
13
- end
14
-
15
- it "should require a password" do
16
- lambda {
17
- Warren::Connection.new(details.except(:pass))
18
- }.should raise_error(Warren::Connection::InvalidConnectionDetails)
19
- end
20
-
21
- it "should require a queue name if none specified" do
22
- lambda {
23
- conn = Warren::Connection.new(details)
24
- conn.queue_name
25
- }.should raise_error(Warren::Connection::InvalidConnectionDetails)
9
+
10
+ it "should read from a config file" do
11
+ YAML.should_receive(:load).with("#{File.dirname($0)}/config/warren.yml").and_return({"development" => {}})
12
+
13
+ Warren::Connection.new
26
14
  end
27
-
28
- it "should return a default host" do
29
- @c.options[:host].should == "localhost"
15
+
16
+ it "should parse the right config out" do
17
+ conn = Warren::Connection.new(@file)
18
+ conn.instance_variable_get("@opts").should == {
19
+ :host => "localhost",
20
+ :user => "rspec",
21
+ :pass => "password",
22
+ :logging => false
23
+ }
30
24
  end
31
25
 
32
- it "should return a given host" do
33
- c = Warren::Connection.new(details.merge({:host => "caius"}))
34
- c.options[:host].should == "caius"
26
+ it "should symbolize keys in a hash" do
27
+ conn = Warren::Connection.new(@file)
28
+ hash = {"one" => "two", "three" => "four", :five => "six"}
29
+ conn.send(:symbolize_keys, hash).should == {
30
+ :one => "two",
31
+ :three => "four",
32
+ :five => "six"
33
+ }
35
34
  end
36
35
 
37
- it "should return a default port" do
38
- @c.options[:port].should == 5672
36
+ it "should raise if no adapter set to check against" do
37
+ Warren::Queue.adapter = nil
38
+ lambda {
39
+ Warren::Connection.new(@file)
40
+ }.should raise_error(Warren::Queue::NoAdapterSet)
39
41
  end
40
42
 
41
- it "should return a given port" do
42
- c = Warren::Connection.new(details.merge({:port => 1}))
43
- c.options[:port].should == 1
43
+ it "should successfully check against adapter" do
44
+ _adapter = stub 'queue', :check_connection_details => true
45
+
46
+ Warren::Connection.new(@file)
44
47
  end
45
48
 
46
- it "should return a default logging param" do
47
- @c.options[:logging].should == false
49
+ it "should raise errors for missing connection details" do
50
+ _adapter = stub 'queue', :check_connection_details => ["one", "two"]
51
+
52
+ Warren::Connection.new(@file)
48
53
  end
49
54
 
50
- it "should return a given logging param" do
51
- c = Warren::Connection.new(details.merge({:logging => true}))
52
- c.options[:logging].should == true
55
+ it "should raise errors for other prerequisits in the adapter" do
56
+ Adapter = Class.new(Warren::Queue) do
57
+ def self.check_connection_details params
58
+ raise Warren::Connection::InvalidConnectionDetails, "Missing prerequisites"
59
+ end
60
+ end
61
+
62
+ lambda {
63
+ Warren::Connection.new(@file)
64
+ }.should raise_error(Warren::Connection::InvalidConnectionDetails, "Missing prerequisites")
53
65
  end
54
66
 
55
- it "should return the given queue name" do
56
- conn = Warren::Connection.new(details.merge({:default_queue => "queue"}))
57
- conn.queue_name.should == "queue"
67
+ def setup_adapter
68
+ _adapter = stub 'queue'
69
+ Warren::Queue.adapter = _adapter
58
70
  end
59
71
 
60
- private
72
+ def yaml_data
73
+ a = <<-EOF
74
+ ---
75
+ development:
76
+ host: localhost
77
+ user: rspec
78
+ pass: password
79
+ logging: false
61
80
 
62
- def details
63
- {
64
- :user => "user",
65
- :pass => "pass",
66
- :vhost => "main",
67
- }
81
+ test:
82
+ host: localhost
83
+ user: rspec
84
+ pass: password
85
+ logging: true
86
+ EOF
68
87
  end
69
-
88
+
70
89
  end