witch 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/README.md +146 -0
- data/Rakefile +2 -0
- data/lib/version.rb +3 -0
- data/lib/witch.rb +154 -0
- data/sandbox/api.rb +25 -0
- data/sandbox/mailer.rb +17 -0
- data/sandbox/test.rb +17 -0
- data/sandbox/web.rb +17 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/witch/witch_spec.rb +57 -0
- data/witch.gemspec +24 -0
- metadata +104 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f4aec34f53b5eefdb9e1ef701556c0b9834f23ae
|
4
|
+
data.tar.gz: 99bf4a228960cc0e830136b57e00562e4254b5fc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3b6c2d058d1fc8c524756b0662b65f7d7d4a601902e96c699b886b96348a47e9e2291f2514df161e95e13d4368d411b3ae3828c7ccad8726e5fcc6e3952bdcd2
|
7
|
+
data.tar.gz: a55381d307d1c6cea404640a135da9d553730074b39807d5397e5f35c6b13cba06e3c87b8f0c80ef554bcb08878ab58654fcb3df1567d936f1f7ff5ecc810ad8
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
Tuktuk - SMTP client for Ruby
|
2
|
+
=============================
|
3
|
+
|
4
|
+
Unlike famous ol' Pony gem (which is friggin' awesome by the way), Tuktuk does not rely on
|
5
|
+
`sendmail` or a separate SMTP server in order to deliver email. Tuktuk looks up the
|
6
|
+
MX servers of the destination address and connects directly using Net::SMTP.
|
7
|
+
This way you don't need to install Exim or Postfix and you can actually handle
|
8
|
+
response status codes -- like bounces, 5xx -- within your application.
|
9
|
+
|
10
|
+
Plus, it supports DKIM out of the box.
|
11
|
+
|
12
|
+
Delivering mail
|
13
|
+
---------------
|
14
|
+
|
15
|
+
``` ruby
|
16
|
+
require 'tuktuk'
|
17
|
+
|
18
|
+
message = {
|
19
|
+
:from => 'you@username.com',
|
20
|
+
:to => 'user@yoursite.com',
|
21
|
+
:body => 'Hello there',
|
22
|
+
:subject => 'Hiya'
|
23
|
+
}
|
24
|
+
|
25
|
+
response, email = Tuktuk.deliver(message)
|
26
|
+
```
|
27
|
+
|
28
|
+
The `response` is either a Net::SMTP::Response object, or a Bounce exception (HardBounce or SoftBounce, depending on the cause). `email` is a [mail](https://github.com/mikel/mail) object. So, to handle bounces you'd do:
|
29
|
+
|
30
|
+
``` ruby
|
31
|
+
[...]
|
32
|
+
|
33
|
+
response, email = Tuktuk.deliver(message)
|
34
|
+
|
35
|
+
if response.is_a?(Tuktuk::Bounce)
|
36
|
+
puts 'Email bounced. Type: ' + response.class.name # => HardBounce or SoftBounce
|
37
|
+
else
|
38
|
+
puts 'Email delivered!'
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
Delivering multiple
|
43
|
+
-------------------
|
44
|
+
|
45
|
+
With Tuktuk, you can also deliver multiple messages at once. Depending on the `max_workers` config parameter, Tuktuk will either connect sequentially to the target domain's MX servers, or do it in parallel by spawning threads.
|
46
|
+
|
47
|
+
Tuktuk will try to send all emails targeted for a specific domain on the same SMTP session. If a MX server is not responding -- or times out in the middle --, Tuktuk will try to deliver the remaining messages to next MX server, and so on.
|
48
|
+
|
49
|
+
To #deliver_many, you need to pass an array of messages, and you'll receive an array of [response, email] elements, just as above.
|
50
|
+
|
51
|
+
``` ruby
|
52
|
+
messages = [ { ... }, { ... }, { ... }, { ... } ] # array of messages
|
53
|
+
|
54
|
+
result = Tuktuk.deliver_many(messages)
|
55
|
+
|
56
|
+
result.each do |response, email|
|
57
|
+
|
58
|
+
if response.is_a?(Tuktuk::Bounce)
|
59
|
+
puts 'Email bounced. Type: ' + response.class.name
|
60
|
+
else
|
61
|
+
puts 'Email delivered!'
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
Options & DKIM
|
68
|
+
--------------
|
69
|
+
|
70
|
+
Now, if you want to enable DKIM (and you should):
|
71
|
+
|
72
|
+
``` ruby
|
73
|
+
require 'tuktuk'
|
74
|
+
|
75
|
+
Tuktuk.options = {
|
76
|
+
:dkim => {
|
77
|
+
:domain => 'yoursite.com',
|
78
|
+
:selector => 'mailer',
|
79
|
+
:private_key => IO.read('ssl/yoursite.com.key')
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
message = { ... }
|
84
|
+
|
85
|
+
response, email = Tuktuk.deliver(message)
|
86
|
+
```
|
87
|
+
|
88
|
+
For DKIM to work, you need to set up some TXT records in your domain's DNS. You can use [this tool](http://www.socketlabs.com/domainkey-dkim-generation-wizard/) to generate the key. You should also create [SPF records](http://www.spfwizard.net/) if you haven't.
|
89
|
+
|
90
|
+
All available options, with their defaults:
|
91
|
+
|
92
|
+
``` ruby
|
93
|
+
Tuktuk.options = {
|
94
|
+
:log_to => nil, # e.g. log/mailer.log or STDOUT
|
95
|
+
:helo_domain => nil, # your server's domain goes here
|
96
|
+
:max_workers => 0, # controls number of threads for delivering_many emails (read below)
|
97
|
+
:open_timeout => 20, # max seconds to wait for opening a connection
|
98
|
+
:read_timeout => 20, # 20 seconds to wait for a response, once connected
|
99
|
+
:verify_ssl => true, # whether to skip SSL keys verification or not
|
100
|
+
:debug => false, # connects and delivers email to localhost, instead of real target server. CAUTION!
|
101
|
+
:dkim => { ... }
|
102
|
+
}
|
103
|
+
```
|
104
|
+
|
105
|
+
You can set the `max_threads` option to `auto`, which will spawn the necessary threads to connect in paralell to all target MX servers when delivering multiple messages. When set to `0`, these batches will be delivered sequentially.
|
106
|
+
|
107
|
+
In other words, if you have three emails targeted to Gmail users and two for Hotmail users, using `auto` Tuktuk will spawn two threads and connect to both servers at once. Using `0` will have email deliveried to one host and then the other.
|
108
|
+
|
109
|
+
Using with Rails
|
110
|
+
----------------
|
111
|
+
|
112
|
+
Tuktuk comes with ActionMailer support out of the box. In your environment.rb or environments/{env}.rb:
|
113
|
+
|
114
|
+
``` ruby
|
115
|
+
require 'tuktuk/rails'
|
116
|
+
|
117
|
+
[...]
|
118
|
+
|
119
|
+
config.action_mailer.delivery_method = :tuktuk
|
120
|
+
```
|
121
|
+
|
122
|
+
Since Tuktuk delivers email directly to the user's MX servers, it's probably a good idea to set `config.action_mailer.raise_delivery_errors` to true. That way you can actually know if an email couldn't make it to its destination.
|
123
|
+
|
124
|
+
When used with ActionMailer, you can pass options using ActionMailer's interface, like this:
|
125
|
+
|
126
|
+
``` ruby
|
127
|
+
|
128
|
+
config.action_mailer.delivery_method = :tuktuk
|
129
|
+
|
130
|
+
config.action_mailer.tuktuk_settings = {
|
131
|
+
:log_to => 'log/mailer.log', # when not set, Tuktuk will use Rails.logger
|
132
|
+
:dkim => {
|
133
|
+
:domain => 'yoursite.com',
|
134
|
+
:selector => 'mailer',
|
135
|
+
:private_key => IO.read('ssl/yoursite.com.key')
|
136
|
+
}
|
137
|
+
}
|
138
|
+
```
|
139
|
+
|
140
|
+
# Contributions
|
141
|
+
|
142
|
+
You're more than welcome. Send a pull request, including tests, and make sure you don't break anything. That's it.
|
143
|
+
|
144
|
+
# Copyright
|
145
|
+
|
146
|
+
(c) Fork Limited. MIT license.
|
data/Rakefile
ADDED
data/lib/version.rb
ADDED
data/lib/witch.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Witch
|
5
|
+
|
6
|
+
class ConsumerDisconnected < Exception; end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
def init(options = {})
|
11
|
+
raise 'Already initialized!' if @queue
|
12
|
+
@queue = options.delete(:queue) || 'eventstream'
|
13
|
+
@options = options
|
14
|
+
@callbacks = {:once => {}, :on => {}}
|
15
|
+
@channels = []
|
16
|
+
@id = Socket.gethostname + '-' + $$.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def once(*events, &block)
|
20
|
+
raise 'Not initialized' unless @callbacks
|
21
|
+
events.each do |event|
|
22
|
+
@callbacks[:once][event.to_sym] ||= []
|
23
|
+
@callbacks[:once][event.to_sym].push(block)
|
24
|
+
register_for(event)
|
25
|
+
end
|
26
|
+
listen(:once, @id) unless listening_on?(@id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def on(*events, &block)
|
30
|
+
raise 'Not initialized' unless @callbacks
|
31
|
+
events.each do |event|
|
32
|
+
@callbacks[:on][event.to_sym] ||= []
|
33
|
+
@callbacks[:on][event.to_sym].push(block)
|
34
|
+
end
|
35
|
+
listen(:on, @queue) unless listening_on?(@queue)
|
36
|
+
end
|
37
|
+
|
38
|
+
def off(*events)
|
39
|
+
raise 'Not initialized' unless @callbacks
|
40
|
+
events.each do |event|
|
41
|
+
@callbacks[:on].delete(event.to_sym)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def publish(event, data = nil)
|
46
|
+
str = event + '___' + data.to_s
|
47
|
+
|
48
|
+
lock.synchronize do
|
49
|
+
# first, send to everyone on the main queue
|
50
|
+
puts '[publisher] Publishing on queue ' + @queue
|
51
|
+
connection.publish(@queue, str)
|
52
|
+
|
53
|
+
# if one or more clients are subscribed to the 'once' queue, choose one and notify.
|
54
|
+
if consumer = connection.smembers('subscribers:' + event.to_s).shuffle.first
|
55
|
+
# connection.publish(['events', event, subscriber].join(':'), data)
|
56
|
+
puts "[publisher] Found consumer for #{event}: #{consumer}"
|
57
|
+
count = connection.publish(consumer, str)
|
58
|
+
|
59
|
+
if count == 0
|
60
|
+
puts "[publisher] Consumer #{consumer} disconnected! Removing and retrying..."
|
61
|
+
connection.srem('subscribers:' + event.to_s, consumer)
|
62
|
+
raise ConsumerDisconnected, "name: #{consumer}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end # lock
|
67
|
+
|
68
|
+
rescue ConsumerDisconnected => e
|
69
|
+
retry
|
70
|
+
end
|
71
|
+
|
72
|
+
def disconnect
|
73
|
+
return unless @id
|
74
|
+
@callbacks[:once].keys.each do |event|
|
75
|
+
unregister_from(event)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def register_for(event)
|
82
|
+
lock.synchronize do
|
83
|
+
count = connection.sadd('subscribers:' + event.to_s, @id)
|
84
|
+
puts "[consumer] Registered. Queue contains #{count} listeners."
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def unregister_from(event)
|
89
|
+
lock.synchronize do
|
90
|
+
puts "[consumer] Unregistering from #{event} processing queue."
|
91
|
+
connection.srem('subscribers:' + event.to_s, @id)
|
92
|
+
end
|
93
|
+
rescue => e
|
94
|
+
puts e.inspect
|
95
|
+
end
|
96
|
+
|
97
|
+
def listen(type, queue)
|
98
|
+
lock.synchronize do
|
99
|
+
puts "[consumer] Subscribing to queue #{queue} on new thread..."
|
100
|
+
@channels.push(queue)
|
101
|
+
Thread.new do
|
102
|
+
# connection.subscribe(['events' + event + id].join(':')) do |on|
|
103
|
+
connection.subscribe(queue) do |on|
|
104
|
+
on.subscribe do |channel, subscriptions|
|
105
|
+
|
106
|
+
puts "[consumer] Subscribed to #{channel} (#{subscriptions} subscriptions)"
|
107
|
+
raise "Existing subscribers!" if subscriptions > 1
|
108
|
+
end
|
109
|
+
on.message do |channel, msg|
|
110
|
+
process(type, msg)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
rescue => e
|
116
|
+
@channels.delete(queue)
|
117
|
+
puts e.message
|
118
|
+
end
|
119
|
+
|
120
|
+
def process(type, msg)
|
121
|
+
parts = msg.split('___')
|
122
|
+
data = parts[1] ? JSON.parse(parts[1]) : nil
|
123
|
+
fire_callbacks(type, parts[0], data)
|
124
|
+
rescue => e
|
125
|
+
puts "[consumer] Error parsing message: #{e.message}"
|
126
|
+
end
|
127
|
+
|
128
|
+
def fire_callbacks(type, event, data)
|
129
|
+
return if @callbacks.nil?
|
130
|
+
lock.synchronize do
|
131
|
+
(@callbacks[type][event.to_sym] || []).each do |callback|
|
132
|
+
callback.call(data)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
rescue => e
|
136
|
+
puts "[consumer] Error firing callback: #{e.message}"
|
137
|
+
end
|
138
|
+
|
139
|
+
def lock
|
140
|
+
@lock ||= Mutex.new
|
141
|
+
end
|
142
|
+
|
143
|
+
def connection
|
144
|
+
raise 'Not initialized!' unless @options
|
145
|
+
Thread.current[:redis] ||= Redis.new(@options)
|
146
|
+
end
|
147
|
+
|
148
|
+
def listening_on?(queue)
|
149
|
+
@channels.include?(queue)
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
data/sandbox/api.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
$LOAD_PATH.push File.expand_path(File.dirname(__FILE__) + '/../lib')
|
2
|
+
|
3
|
+
require 'witch'
|
4
|
+
|
5
|
+
Witch.init
|
6
|
+
|
7
|
+
puts 'API server...'
|
8
|
+
|
9
|
+
Signal.trap 'EXIT' do
|
10
|
+
Thread.new { Witch.disconnect }.join
|
11
|
+
end
|
12
|
+
|
13
|
+
module Events
|
14
|
+
def self.device_dead
|
15
|
+
puts Time.now.to_s + ' - marking device dead'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
Redis.new.subscribe('test') do |on|
|
20
|
+
on.message do |channel, message|
|
21
|
+
puts 'got message: ' + message
|
22
|
+
Events.send(message) if Events.respond_to?(message)
|
23
|
+
Witch.publish(message)
|
24
|
+
end
|
25
|
+
end
|
data/sandbox/mailer.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
$LOAD_PATH.push File.expand_path(File.dirname(__FILE__) + '/../lib')
|
2
|
+
|
3
|
+
require 'witch'
|
4
|
+
|
5
|
+
Witch.init
|
6
|
+
|
7
|
+
puts 'mailer server...'
|
8
|
+
|
9
|
+
Witch.once 'device_dead' do |data|
|
10
|
+
puts Time.now.to_s + ' sending email to user'
|
11
|
+
end
|
12
|
+
|
13
|
+
Signal.trap 'EXIT' do
|
14
|
+
Thread.new { Witch.disconnect }.join
|
15
|
+
end
|
16
|
+
|
17
|
+
loop { sleep 1; }
|
data/sandbox/test.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
$LOAD_PATH.push File.expand_path(File.dirname(__FILE__) + '/../lib')
|
2
|
+
|
3
|
+
require 'witch'
|
4
|
+
|
5
|
+
Witch.init
|
6
|
+
|
7
|
+
puts 'API server...'
|
8
|
+
|
9
|
+
Witch.once 'foo' do |data|
|
10
|
+
puts 'got message: ' + data.inspect
|
11
|
+
end
|
12
|
+
|
13
|
+
Signal.trap 'EXIT' do
|
14
|
+
Thread.new { Witch.disconnect }.join
|
15
|
+
end
|
16
|
+
|
17
|
+
loop { sleep 1; }
|
data/sandbox/web.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
$LOAD_PATH.push File.expand_path(File.dirname(__FILE__) + '/../lib')
|
2
|
+
|
3
|
+
require 'witch'
|
4
|
+
|
5
|
+
Witch.init
|
6
|
+
|
7
|
+
puts 'web server...'
|
8
|
+
|
9
|
+
Witch.on 'device_dead', 'device_away' do |data|
|
10
|
+
puts Time.now.to_s + ' - showing device dead message in UI '
|
11
|
+
end
|
12
|
+
|
13
|
+
Signal.trap 'EXIT' do
|
14
|
+
Thread.new { Witch.disconnect }.join
|
15
|
+
end
|
16
|
+
|
17
|
+
loop { sleep 1; }
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe 'witch' do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@called = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
after(:all) do
|
10
|
+
# Witch.disconnect
|
11
|
+
end
|
12
|
+
|
13
|
+
def register(type)
|
14
|
+
Witch.init
|
15
|
+
Witch.send(type, 'some_event') do
|
16
|
+
# puts 'calledxxxxxxxxx'
|
17
|
+
@called[$$] = true
|
18
|
+
end
|
19
|
+
sleep 0.1
|
20
|
+
end
|
21
|
+
|
22
|
+
describe 'once' do
|
23
|
+
|
24
|
+
describe 'single process' do
|
25
|
+
|
26
|
+
before do
|
27
|
+
register :once
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'fires event once' do
|
31
|
+
puts 'publishing'
|
32
|
+
Witch.publish 'some_event'
|
33
|
+
puts @called.inspect
|
34
|
+
# @called.keys.should == $$
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
describe 'multi process' do
|
40
|
+
|
41
|
+
it 'fires event once' do
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'on' do
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
describe 'off' do
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
data/witch.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path("../lib/version", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "witch"
|
6
|
+
s.version = Witch::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ['Tomás Pollak']
|
9
|
+
s.email = ['tomas@forkhq.com']
|
10
|
+
s.homepage = "https://github.com/tomas/witch"
|
11
|
+
s.summary = "Reliable pub/sub for multi-process architectures."
|
12
|
+
s.description = "Reliable pub/sub for multi-process architectures."
|
13
|
+
|
14
|
+
s.required_rubygems_version = ">= 1.3.6"
|
15
|
+
s.rubyforge_project = "tuktuk"
|
16
|
+
|
17
|
+
s.add_development_dependency "bundler", ">= 1.0.0"
|
18
|
+
s.add_development_dependency "rspec", '~> 3.0', '>= 3.0.0'
|
19
|
+
s.add_runtime_dependency "redis", ">= 3.0"
|
20
|
+
|
21
|
+
s.files = `git ls-files`.split("\n")
|
22
|
+
s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
|
23
|
+
s.require_path = 'lib'
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: witch
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tomás Pollak
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-04-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.0.0
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 3.0.0
|
37
|
+
type: :development
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '3.0'
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 3.0.0
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: redis
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '3.0'
|
61
|
+
description: Reliable pub/sub for multi-process architectures.
|
62
|
+
email:
|
63
|
+
- tomas@forkhq.com
|
64
|
+
executables: []
|
65
|
+
extensions: []
|
66
|
+
extra_rdoc_files: []
|
67
|
+
files:
|
68
|
+
- Gemfile
|
69
|
+
- README.md
|
70
|
+
- Rakefile
|
71
|
+
- lib/version.rb
|
72
|
+
- lib/witch.rb
|
73
|
+
- sandbox/api.rb
|
74
|
+
- sandbox/mailer.rb
|
75
|
+
- sandbox/test.rb
|
76
|
+
- sandbox/web.rb
|
77
|
+
- spec/spec_helper.rb
|
78
|
+
- spec/witch/witch_spec.rb
|
79
|
+
- witch.gemspec
|
80
|
+
homepage: https://github.com/tomas/witch
|
81
|
+
licenses: []
|
82
|
+
metadata: {}
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.3.6
|
97
|
+
requirements: []
|
98
|
+
rubyforge_project: tuktuk
|
99
|
+
rubygems_version: 2.2.0
|
100
|
+
signing_key:
|
101
|
+
specification_version: 4
|
102
|
+
summary: Reliable pub/sub for multi-process architectures.
|
103
|
+
test_files: []
|
104
|
+
has_rdoc:
|