schoefmax-warren 0.8.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ require "rubygems"
2
+
3
+ class DummyAdapter < Warren::Queue
4
+ def self.publish queue_name, payload, &blk
5
+ puts "publishing #{payload.inspect} to #{queue_name}"
6
+ end
7
+
8
+ def self.subscribe queue_name, &block
9
+ puts "subscribing to #{queue_name}"
10
+ end
11
+ end
12
+
@@ -0,0 +1,27 @@
1
+ require "rubygems"
2
+
3
+ class TestAdapter < Warren::Queue
4
+ ConnectionFailed = Class.new(Exception)
5
+ #
6
+ def self.publish queue_name, payload, &blk
7
+ raise TestAdapter::ConnectionFailed if fail?
8
+ true
9
+ end
10
+
11
+ #
12
+ # def self.subscribe queue_name, &block
13
+ # puts "subscribing to #{queue_name}"
14
+ # end
15
+ #
16
+
17
+ private
18
+
19
+ def self.fail?
20
+ File.exists?(file_name) && (File.read(file_name, 4) == "FAIL")
21
+ end
22
+
23
+ def self.file_name
24
+ "#{WARREN_ROOT}/tmp/warren.txt"
25
+ end
26
+ end
27
+
@@ -0,0 +1,140 @@
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
+ # Warren::Connection.new # reads WARREN_ROOT/config/warren.yml
13
+ # Warren::Connection.new("file.yml") # reads file.yml
14
+ # Warren::Connection.new({"foo" => "bar"}) # uses the hash
15
+ #
16
+ # Reads WARREN_ENV out of the yaml'd hash (just like ActiveRecord)
17
+ # "development" by default (and RAILS_ENV if running under rails)
18
+ #
19
+ # Raises InvalidConnectionDetails if no params are found for the current
20
+ # environment.
21
+ #
22
+ # todo: fail nicely if the env isn't defined
23
+ #
24
+ def initialize params = nil
25
+ get_connection_details(params)
26
+ # Make the details publically accessible
27
+ @options = @opts
28
+ end
29
+
30
+ #
31
+ # Raised if connection details are missing or invalid
32
+ # Check the error message for more details
33
+ #
34
+ InvalidConnectionDetails = Class.new(Exception)
35
+
36
+ private
37
+
38
+ #
39
+ # Short version: This is the brain of this class and sets everything up.
40
+ #
41
+ # Long version: Works out where the connection details need to come from
42
+ # (params or yml) and parses them from there. Then figures out if the details
43
+ # are there for the current env and raises a nice error if there aren't any
44
+ # detail for the current env. Then we symblize all the keys and get the adapter
45
+ # to check its happy with the connection details we have.
46
+ #
47
+ def get_connection_details params
48
+ case params
49
+ when NilClass
50
+ # Read from config/warren.yml
51
+ read_config_file
52
+
53
+ when String
54
+ # See if it exists as a file
55
+ parse_config_file(params)
56
+
57
+ when Hash
58
+ # Use it as-is
59
+ @opts = params
60
+
61
+ else
62
+ # See if it responds to :read
63
+ if respond_to?(:read)
64
+ # Parse it as yaml
65
+ parse_config(params)
66
+ else
67
+ # Have no idea what to do with it
68
+ raise InvalidConnectionDetails, "Don't know what to do with the params passed. Please pass a hash, filename or nothing."
69
+ end
70
+ end
71
+
72
+ # Make sure the hash keys are symbols
73
+ @opts = symbolize_keys(@opts)
74
+ # Parse out the current environment
75
+ parse_environment
76
+ # Call the adapter to figure out if the details are alright
77
+ check_connection_details
78
+ end
79
+
80
+ #
81
+ # Reads in either config/warren.yml or the passed argument as a YAML file
82
+ #
83
+ def read_config_file filename=nil
84
+ filename ||= "#{WARREN_ROOT}/config/warren.yml"
85
+ parse_config_file(filename)
86
+ end
87
+
88
+ #
89
+ # Parses the config file into a hash from yaml.
90
+ #
91
+ def parse_config_file filename
92
+ if File.exists?(filename)
93
+ @opts = YAML.load_file(filename)
94
+ else
95
+ raise InvalidConnectionDetails, "File not found: '#{filename}'"
96
+ end
97
+ end
98
+
99
+ #
100
+ # Parses the object using YAML#load
101
+ #
102
+ def parse_config obj
103
+ @opts = YAML.load(obj)
104
+ end
105
+
106
+ #
107
+ # Modifies the hash into just the details for the
108
+ # current environment, or raises InvalidConnectionDetails
109
+ #
110
+ def parse_environment
111
+ # keys are symbolified already
112
+ unless @opts.has_key?(WARREN_ENV.to_sym)
113
+ raise InvalidConnectionDetails, "No details for current environment '#{WARREN_ENV}'"
114
+ end
115
+ @opts = @opts[WARREN_ENV.to_sym]
116
+ end
117
+
118
+ #
119
+ # Changes all keys into symbols
120
+ #
121
+ def symbolize_keys(hash)
122
+ hash.each do |key, value|
123
+ hash.delete(key)
124
+ # Make it recursive
125
+ hash[key.to_sym] = (value.is_a?(Hash) ? symbolize_keys(value) : value)
126
+ end
127
+ hash
128
+ end
129
+
130
+ #
131
+ # Calls the adapter to check the connection details
132
+ # Returns true or raises InvalidConnectionDetails
133
+ #
134
+ def check_connection_details
135
+ return true unless Warren::Queue.adapter.respond_to?(:check_connection_details)
136
+ Warren::Queue.adapter.send(:check_connection_details, @opts)
137
+ end
138
+
139
+ end
140
+ end
@@ -0,0 +1,70 @@
1
+ begin
2
+ require "hmac-sha2"
3
+ rescue LoadError => e
4
+ puts "Error loading the `ruby-hmac` gem."
5
+ exit!
6
+ end
7
+
8
+ module Warren
9
+ class MessageFilter
10
+ # Hashes the message using a secret salt, stores the hash
11
+ # in the message and then checks its the same when pulled
12
+ # off the other end.
13
+ #
14
+ # Basic trust implementation to make sure the message
15
+ # hasn't been tampered with in transit and came from
16
+ # an "authorised" app.
17
+ #
18
+ # Make sure both the publisher and subscriber use the same
19
+ # key else you'll get KeyValidationError error raised.
20
+ #
21
+ class SharedSecret
22
+ # Raised when no key (salt) is provided
23
+ class NoKeyError < Exception; end
24
+ # Raised when there is a key mismatch error
25
+ class KeyValidationError < Exception; end
26
+
27
+ # Sets the key to use
28
+ def self.key= key
29
+ @@key = key
30
+ end
31
+
32
+ # Returns the current key
33
+ # Raises NoKeyError if no key has been assigned yet
34
+ def self.key
35
+ raise NoKeyError if @@key.nil?
36
+ @@key
37
+ end
38
+
39
+ # Returns the hashed message
40
+ #
41
+ # Expects that msg#to_s returns a string
42
+ # to hash against.
43
+ #
44
+ def self.secret msg
45
+ HMAC::SHA256.hexdigest(self.key, msg.to_s)
46
+ end
47
+
48
+ # Called when the message is being packed for
49
+ # transit. Returns a hash.
50
+ def self.pack msg
51
+ # Make sure its a hash
52
+ msg = {:secret_msg => msg} unless msg.is_a? Hash
53
+ # And add our secret into the hash
54
+ msg[:secret] = self.secret(msg.to_s)
55
+ msg
56
+ end
57
+
58
+ # Called when unpacking the message from transit.
59
+ # Returns the original object.
60
+ def self.unpack msg
61
+ # Check the secret exists in the msg and matches the secret_string
62
+ raise KeyValidationError unless msg.delete(:secret) == self.secret(msg)
63
+ # see if its a hash we created, it'll only contain the key "secret_msg" if it is
64
+ msg = msg[:secret_msg] if msg.keys == [:secret_msg]
65
+ msg
66
+ end
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,20 @@
1
+ require "yaml"
2
+
3
+ module Warren
4
+ class MessageFilter
5
+ # Packs the message into a YAML string
6
+ # for transferring safely across the wire
7
+ class Yaml < MessageFilter
8
+
9
+ # Returns a YAML string
10
+ def self.pack msg
11
+ YAML.dump(msg)
12
+ end
13
+
14
+ # Returns original message
15
+ def self.unpack msg
16
+ YAML.load(msg)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,76 @@
1
+ module Warren
2
+ # Handles filtering messages going onto/coming off the queue
3
+ class MessageFilter
4
+ # Array of filters to be run on the message before its
5
+ # pushed to rabbit.
6
+ #
7
+ # NB: These get called in reverse order from the array -
8
+ # the last filter to be added gets called first.
9
+ @@filters = []
10
+
11
+ class << self
12
+ # Adds a filter to the list
13
+ #
14
+ # A valid filter is just a class that defines
15
+ # <tt>self.pack</tt> and <tt>self.unpack</tt>
16
+ # methods, which both accept a single argument,
17
+ # act upon it, and return the output.
18
+ #
19
+ # Example filter class (See also message_filters/*.rb)
20
+ #
21
+ # class Foo
22
+ # def self.pack msg
23
+ # msg.reverse # Assumes msg responds to reverse
24
+ # end
25
+ #
26
+ # def self.unpack msg
27
+ # msg.reverse # Does the opposite of Foo#pack
28
+ # end
29
+ # end
30
+ #
31
+ def << filter
32
+ @@filters << filter
33
+ end
34
+ alias :add_filter :<<
35
+ end
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
+
42
+ # Returns current array of filters
43
+ def self.filters
44
+ @@filters
45
+ end
46
+
47
+ # Resets the filters to default
48
+ def self.reset_filters
49
+ @@filters = [Warren::MessageFilter::Yaml]
50
+ end
51
+
52
+ # Runs the raw message through all the filters
53
+ # and returns the filtered version
54
+ def self.pack msg
55
+ @@filters.reverse.each do |f|
56
+ # puts "Packing with #{f}"
57
+ msg = f.send(:pack, msg)
58
+ end
59
+ msg
60
+ end
61
+
62
+ # Runs the filtered message through all the
63
+ # filters and returns the raw version
64
+ def self.unpack msg
65
+ @@filters.each do |f|
66
+ # puts "Unpacking with #{f}"
67
+ msg = f.unpack(msg)
68
+ end
69
+ msg
70
+ end
71
+
72
+ end
73
+ end
74
+
75
+ # Make sure the YAML filter is added first
76
+ require File.expand_path(File.dirname(__FILE__) + "/filters/yaml")
@@ -0,0 +1,71 @@
1
+ module Warren
2
+ class Queue
3
+ @@connection = nil
4
+ @@adapter = nil
5
+
6
+ #
7
+ # Raised if no connection has been defined yet.
8
+ #
9
+ NoConnectionDetails = Class.new(Exception)
10
+
11
+ #
12
+ # Raised if a block is expected by the method but none is given.
13
+ #
14
+ NoBlockGiven = Class.new(Exception)
15
+
16
+ #
17
+ # Raised if an adapter isn't set
18
+ #
19
+ NoAdapterSet = Class.new(Exception)
20
+
21
+ #
22
+ # Sets the current connection
23
+ #
24
+ def self.connection= conn
25
+ @@connection = (conn.is_a?(Warren::Connection) ? conn : Warren::Connection.new(conn) )
26
+ end
27
+
28
+ #
29
+ # Returns the current connection details
30
+ #
31
+ def self.connection
32
+ @@connection ||= Warren::Connection.new
33
+ end
34
+
35
+ #
36
+ # Sets the adapter when this class is subclassed.
37
+ #
38
+ def self.inherited klass
39
+ @@adapter = klass
40
+ end
41
+
42
+ #
43
+ # Sets the adapter manually
44
+ #
45
+ def self.adapter= klass
46
+ @@adapter = klass
47
+ end
48
+
49
+ #
50
+ # Returns the current adapter or raises NoAdapterSet exception
51
+ #
52
+ def self.adapter
53
+ @@adapter || raise(NoAdapterSet)
54
+ end
55
+
56
+ #
57
+ # Publishes the message to the queue
58
+ #
59
+ def self.publish *args, &blk
60
+ self.adapter.publish(*args, &blk)
61
+ end
62
+
63
+ #
64
+ # Sends the subscribe message to the adapter class
65
+ #
66
+ def self.subscribe *args, &blk
67
+ self.adapter.subscribe(*args, &blk)
68
+ end
69
+
70
+ end
71
+ end
data/readme.rdoc ADDED
@@ -0,0 +1,46 @@
1
+ = Warren
2
+
3
+ Library for sending and receiving messages, complete with en/decrypting messages on either side of the transport.
4
+
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
+
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
+
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
+
11
+ == Installation
12
+
13
+ gem install brightbox-warren
14
+
15
+ == Usage
16
+
17
+ require "rubygems"
18
+ require "warren"
19
+ # Use the bunny adapter to connect to RabbitMQ (Bunny is an AMQP client that works with Rails/Passenger apps)
20
+ require "warren/adapters/bunny_adapter"
21
+ # If you're running in development and don't want to actually push messages onto the queue then instead of loading the bunny adapter use the dummy adapter
22
+ require "warren/adapters/dummy_adapter"
23
+
24
+ # See examples/ for more
25
+
26
+ == Rails
27
+
28
+ Add this to your environment.rb
29
+
30
+ config.gem "brightbox-warren", :lib => "warren", :version => ">= 0.8"
31
+
32
+ Add the config into config/warren.yml with the details for each environment. Works just the same as database.yml:
33
+
34
+ development:
35
+ user: rabbit
36
+ pass: carrots53
37
+ host: rabbit.warren
38
+ logging: false
39
+
40
+ And then in an initializer file (or bottom of environment.rb) require the adapter you want to use (for rabbitmq I suggest bunny - amqp uses eventmachine and was giving me issues under passenger.) And then any filters you want to use.
41
+
42
+ require "warren/adapters/bunny_adapter"
43
+
44
+ == License
45
+
46
+ Licensed under the MIT license. See LICENSE for more details.