pigeons 0.0.1pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,429 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'mocha'
5
+
6
+ # To test:
7
+ # 1) Bases (check)
8
+ # 2) Conditions
9
+ # 3) Flights (Cohorts)
10
+ # 4) Bit more complexity?
11
+ # 5) Send
12
+ # 6) "signup", aye!
13
+
14
+ class PigeonExtensions < Pigeons::Extension
15
+
16
+ # Match an ownership
17
+ condition /^own \s+ (?<property>\w+)/ix do |scope, match|
18
+ # p [ "Pigeon Extension::Condition", "#{item} matched conditional" ]
19
+
20
+ scope.where([ "property LIKE ?", "%#{match[:property].singularize}%" ])
21
+ end
22
+
23
+ condition /eaten/i do |scope, match|
24
+ scope.where(eaten: true)
25
+ end
26
+
27
+ condition /slept/i do |scope, match|
28
+ scope.where(slept: true)
29
+ end
30
+
31
+ base /^(?<color>\w+) \s+ dragon[s]?$/ix do |match|
32
+ Dragon.scoped.where(color: match[:color])
33
+ end
34
+
35
+ base /^the pixies$/i do |match|
36
+ Pixie.scoped
37
+ end
38
+
39
+ event /^hatching$/ do |scope, time, match|
40
+ scope.where("hatched_at < ?", time)
41
+ end
42
+
43
+ event /^defeating (?<orc_name>.*)$/ do |scope, time, match|
44
+
45
+ source_arel = scope.arel_table
46
+ battle_arel = Battle.arel_table
47
+ orc_arel = Orc.arel_table
48
+
49
+ scope.where(
50
+ Battle.joins(:orc).where(
51
+ # match to dragon
52
+ battle_arel[:dragon_id].eq(source_arel[:id]).and(
53
+ orc_arel[:name].eq(match[:orc_name])
54
+ ).and(
55
+ battle_arel[:created_at].lt(time)
56
+ ).and(
57
+ battle_arel[:is_dragon_victor].eq(true)
58
+ )
59
+ ).exists
60
+ )
61
+ end
62
+
63
+ event /^leveling up$/ do |scope, time, match|
64
+
65
+ source_arel = scope.arel_table
66
+ level_arel = Level.arel_table
67
+
68
+ scope.where(
69
+ Level.where(
70
+ level_arel[:pixie_id].eq(source_arel[:id]).and(
71
+ level_arel[:created_at].lt(time)
72
+ )
73
+ ).exists
74
+ )
75
+ end
76
+
77
+ end
78
+
79
+ class TestPigeons < Test::Unit::TestCase
80
+ include Mocha
81
+
82
+ context "when the config has issues" do
83
+ should "raise unknown letter type" do
84
+ Pigeons::Settings.pigeon_config_file = "ignored"
85
+ File.stubs(:exists? => true)
86
+
87
+ # Nonexistant
88
+ PigeonMailer.stubs(respond_to?: false)
89
+ File.stubs(:read => { flights: { aflight: [ "dragon gets a nonexistent letter" ] } }.to_json)
90
+
91
+ assert_raise Pigeons::PigeonError::PigeonFlightConfigError do
92
+ Pigeons.assemble
93
+ end
94
+
95
+ # No idea what class
96
+ PigeonMailer.stubs(respond_to?: true)
97
+ File.stubs(:read => { flights: { aflight: [ "rhinos gets a welcome letter" ] } }.to_json)
98
+ end
99
+ end
100
+
101
+ # These test flights will get run and we'll check the resultant SQL
102
+ # These are, obstensibly end-to-end tests
103
+ test_flights = [
104
+ # Test basic letter
105
+ {
106
+ name: "all dragons get welcome",
107
+ config: { flights: { aflight: [ "dragons gets a welcome letter" ] } },
108
+ # Simply a check to make sure we didn't send this letter type
109
+ expected: [ letter_not_exists(simple_scope(Dragon.scoped), "welcome") ]
110
+ },
111
+ # Test a different base
112
+ {
113
+ name: "all orcs get goodbye",
114
+ config: { flights: { aflight: [ "orcs get a goodbye letter" ] } },
115
+ expected: [ letter_not_exists(simple_scope(Orc.scoped), "goodbye") ]
116
+ },
117
+ # Test relative time (after hours)
118
+ {
119
+ name: "all dragons get welcome after signup, hours",
120
+ config: { flights: { aflight: [ "dragons gets a welcome letter 24 hours after signup" ] } },
121
+ expected: [ letter_not_exists(simple_scope(Dragon.scoped), "welcome").where("created_at < ?", 24.hours.ago) ]
122
+ },
123
+ # Test relative time (after days)
124
+ {
125
+ name: "all dragons get welcome after signup, days",
126
+ config: { flights: { aflight: [ "dragons gets a welcome letter 2 days after signup" ] } },
127
+ expected: [ letter_not_exists(simple_scope(Dragon.scoped), "welcome").where("created_at < ?", 2.days.ago) ]
128
+ },
129
+ # Test recurring (every)
130
+ {
131
+ name: "all dragons get welcome after signup every weeks",
132
+ config: { flights: { aflight: [ "dragons get a welcome letter every 3 weeks after signup" ] } },
133
+ expected: [ simple_scope(Dragon.scoped).where(
134
+ PigeonLetter.where(
135
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
136
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
137
+ ).and(
138
+ PigeonLetter.arel_table[:created_at].gt(3.weeks.ago)
139
+ ).and(
140
+ PigeonLetter.arel_table[:letter_type].eq("welcome")
141
+ )
142
+ ).exists.not
143
+ ).where("created_at < ?", 3.weeks.ago) ]
144
+ },
145
+ # Now, let's try an after
146
+ {
147
+ name: "all dragons get welcomed then fired",
148
+ config: { flights: { aflight: [ "dragons get a welcome letter 2 seconds after signup",
149
+ "then get a fired letter 2 hours after that" ] } },
150
+ expected: [ letter_not_exists(simple_scope(Dragon.scoped), "welcome").where("created_at < ?", 2.seconds.ago),
151
+ letter_not_exists(simple_scope(Dragon.scoped), "fired").where(
152
+ PigeonLetter.where(
153
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
154
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
155
+ ).and(
156
+ PigeonLetter.arel_table[:letter_type].in(["welcome"])
157
+ )
158
+ ).exists
159
+ ).where(
160
+ PigeonLetter.where(
161
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
162
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
163
+ ).and(
164
+ PigeonLetter.arel_table[:letter_type].in(["welcome"])
165
+ ).and(
166
+ PigeonLetter.arel_table[:created_at].gt(2.hours.ago)
167
+ )
168
+ ).exists.not
169
+ )
170
+ ]
171
+ },
172
+ # Let's add a condition
173
+ {
174
+ name: "all dragons who own lairs get taxed",
175
+ config: { flights: { aflight: [ "dragons who own lairs get a tax letter every year" ] } },
176
+ expected: [ simple_scope(Dragon.scoped).where(
177
+ PigeonLetter.where(
178
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
179
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
180
+ ).and(
181
+ PigeonLetter.arel_table[:created_at].gt(1.year.ago)
182
+ ).and(
183
+ PigeonLetter.arel_table[:letter_type].eq("tax")
184
+ )
185
+ ).exists.not
186
+ ).where("property LIKE '%lair%'") ]
187
+ },
188
+ # Let's add a simple base
189
+ {
190
+ name: "all pixies",
191
+ config: { flights: { aflight: [ "the pixies get a punk rock letter" ] } },
192
+ expected: [ letter_not_exists(simple_scope(Pixie.scoped), "punk_rock") ]
193
+ },
194
+ # Let's add a complex base
195
+ {
196
+ name: "red dragon sadness",
197
+ config: { flights: { aflight: [ "red dragons get a hate letter every day after signup" ] } },
198
+ expected: [ simple_scope(Dragon.scoped.where(color: 'red')).where(
199
+ PigeonLetter.where(
200
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
201
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
202
+ ).and(
203
+ PigeonLetter.arel_table[:created_at].gt(1.day.ago)
204
+ ).and(
205
+ PigeonLetter.arel_table[:letter_type].eq("hate")
206
+ )
207
+ ).exists.not
208
+ ).where(["created_at < ?", 1.day.ago]) ]
209
+ },
210
+ # Now let's test complex running conditions
211
+ {
212
+ name: "running conditions",
213
+ config: { flights: { aflight: [ "dragons get a hello letter after signup",
214
+ "then who've eaten get a food letter 1 hour after that",
215
+ "then who've slept get a sleep letter after that",
216
+ "then get a goodnight letter 30 minutes after that",
217
+ "then gets a nightcap letter" ] } }, # Note, these running conditions are complicated
218
+ expected: [ letter_not_exists(simple_scope(Dragon.scoped), "hello").where("created_at < ?", Time.now),
219
+ letter_not_exists(simple_scope(Dragon.scoped).where(eaten: true), "food").where(
220
+ PigeonLetter.where(
221
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
222
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
223
+ ).and(
224
+ PigeonLetter.arel_table[:letter_type].in(["hello"])
225
+ )
226
+ ).exists
227
+ ).where(
228
+ PigeonLetter.where(
229
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
230
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
231
+ ).and(
232
+ PigeonLetter.arel_table[:letter_type].in(["hello"])
233
+ ).and(
234
+ PigeonLetter.arel_table[:created_at].gt(1.hour.ago)
235
+ )
236
+ ).exists.not
237
+ ),
238
+ letter_not_exists(simple_scope(Dragon.scoped).where(slept: true), "sleep").where(
239
+ PigeonLetter.where(
240
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
241
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
242
+ ).and(
243
+ PigeonLetter.arel_table[:letter_type].in(["hello","food"])
244
+ )
245
+ ).exists
246
+ ).where(
247
+ PigeonLetter.where(
248
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
249
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
250
+ ).and(
251
+ PigeonLetter.arel_table[:letter_type].in(["hello","food"])
252
+ ).and(
253
+ PigeonLetter.arel_table[:created_at].gt(Time.now)
254
+ )
255
+ ).exists.not
256
+ ),
257
+ letter_not_exists(simple_scope(Dragon.scoped), "goodnight").where(
258
+ PigeonLetter.where(
259
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
260
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
261
+ ).and(
262
+ PigeonLetter.arel_table[:letter_type].in(["hello","food","sleep"])
263
+ )
264
+ ).exists
265
+ ).where(
266
+ PigeonLetter.where(
267
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
268
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
269
+ ).and(
270
+ PigeonLetter.arel_table[:letter_type].in(["hello","food","sleep"])
271
+ ).and(
272
+ PigeonLetter.arel_table[:created_at].gt(30.minutes.ago)
273
+ )
274
+ ).exists.not
275
+ ),
276
+ letter_not_exists(simple_scope(Dragon.scoped), "nightcap").where(
277
+ PigeonLetter.where(
278
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
279
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
280
+ ).and(
281
+ PigeonLetter.arel_table[:letter_type].in(["goodnight"])
282
+ )
283
+ ).exists
284
+ ).where(
285
+ PigeonLetter.where(
286
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
287
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
288
+ ).and(
289
+ PigeonLetter.arel_table[:letter_type].in(["goodnight"])
290
+ ).and(
291
+ PigeonLetter.arel_table[:created_at].gt(Time.now)
292
+ )
293
+ ).exists.not
294
+ ) ]
295
+ },
296
+ # Finally, we're going to test events
297
+ # First, simply
298
+ {
299
+ name: "all dragons get a birth certificate letter after hatching",
300
+ config: { flights: { aflight: [ "all dragons get a birth certificate letter after hatching" ] } },
301
+ expected: [ letter_not_exists(simple_scope(Dragon.scoped), "birth_certificate").where("hatched_at < ?", Time.now) ]
302
+ },
303
+ # Now, we're test a more complex event
304
+ {
305
+ name: "all dragons get a congratulations letter after defeating Hodor.",
306
+ config: { flights: { aflight: [ "all dragons get a congratulations letter after defeating Hodor." ] } },
307
+ expected: [ letter_not_exists(simple_scope(Dragon.scoped), "congratulations").where(
308
+ Battle.joins(:orc).where(
309
+ Battle.arel_table[:dragon_id].eq(Dragon.arel_table[:id]).and(
310
+ Orc.arel_table[:name].eq("Hodor")
311
+ ).and(
312
+ Battle.arel_table[:created_at].lt(Time.now)
313
+ ).and(
314
+ Battle.arel_table[:is_dragon_victor].eq(true)
315
+ )
316
+ ).exists
317
+ ) ]
318
+ },
319
+ # A recurring event
320
+ {
321
+ name: "pixies getting a level up email",
322
+ config: { flights: { aflight: [ "pixies get a level up letter every time after leveling up" ] } },
323
+ expected: [ simple_scope(Pixie.scoped).where(
324
+ PigeonLetter.where(
325
+ PigeonLetter.arel_table[:cargo_id].eq(Pixie.arel_table[:id]).and(
326
+ PigeonLetter.arel_table[:cargo_type].eq(Pixie.arel_table.name.classify)
327
+ ).and(
328
+ PigeonLetter.arel_table[:created_at].gt(Time.now)
329
+ ).and(
330
+ PigeonLetter.arel_table[:letter_type].eq("level_up")
331
+ )
332
+ ).exists.not
333
+ ).where(
334
+ Level.where(
335
+ Level.arel_table[:pixie_id].eq(Pixie.arel_table[:id]).and(
336
+ Level.arel_table[:created_at].lt(Time.now)
337
+ )
338
+ ).exists
339
+ ) ]
340
+ },
341
+ # Finally, we're going to test base changes
342
+ {
343
+ name: "some dragons, but then other dragons",
344
+ config: { flights: { aflight: [ "red dragons get a red letter",
345
+ "then get a redder letter 1 hour after that",
346
+ "blue dragons get a blue letter",
347
+ "then get a bluer letter 1 fortnight" ] } },
348
+ expected: [ letter_not_exists(simple_scope(Dragon.where(color: 'red')), "red"),
349
+ letter_not_exists(simple_scope(Dragon.where(color: 'red')), "redder").where(
350
+ PigeonLetter.where(
351
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
352
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
353
+ ).and(
354
+ PigeonLetter.arel_table[:letter_type].in(["red"])
355
+ )
356
+ ).exists
357
+ ).where(
358
+ PigeonLetter.where(
359
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
360
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
361
+ ).and(
362
+ PigeonLetter.arel_table[:letter_type].in(["red"])
363
+ ).and(
364
+ PigeonLetter.arel_table[:created_at].gt(1.hour.ago)
365
+ )
366
+ ).exists.not
367
+ ),
368
+ letter_not_exists(simple_scope(Dragon.where(color: 'blue')), "blue"),
369
+ letter_not_exists(simple_scope(Dragon.where(color: 'blue')), "bluer").where(
370
+ PigeonLetter.where(
371
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
372
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
373
+ ).and(
374
+ PigeonLetter.arel_table[:letter_type].in(["blue"])
375
+ )
376
+ ).exists
377
+ ).where(
378
+ PigeonLetter.where(
379
+ PigeonLetter.arel_table[:cargo_id].eq(Dragon.arel_table[:id]).and(
380
+ PigeonLetter.arel_table[:cargo_type].eq(Dragon.arel_table.name.classify)
381
+ ).and(
382
+ PigeonLetter.arel_table[:letter_type].in(["blue"])
383
+ ).and(
384
+ PigeonLetter.arel_table[:created_at].gt(1.fortnight.ago)
385
+ )
386
+ ).exists.not
387
+ ) ]
388
+ },
389
+ # Next, we'll test two flights
390
+ {
391
+ name: "two flights - some dragons, but then other dragons",
392
+ config: { flights: { aflight: [ "red dragons get a red letter" ], bflight: [ "red dragons get a blue letter" ] } },
393
+ expected: {
394
+ aflight: [ letter_not_exists(simple_scope(Dragon.where(color: 'red'),2,0), "red") ],
395
+ bflight: [ letter_not_exists(simple_scope(Dragon.where(color: 'red'),2,1), "blue") ]
396
+ }
397
+ }
398
+ ]
399
+
400
+ context "in end-to-end test flights" do
401
+ setup do
402
+ Pigeons::Settings.pigeon_config_file = "ignored"
403
+ File.stubs(:exists? => true)
404
+
405
+ PigeonMailer.stubs(respond_to?: true)
406
+ ::Time.stubs(now: ::NOW) # Make this static for a test
407
+ ::Time.stubs(current: ::CURRENT)
408
+ end
409
+ puts test_flights.map { |flight| flight[:config][:flights][:aflight].join("\n") }.join("\n")
410
+
411
+ test_flights.each do |flight_test|
412
+ config = flight_test[:config]
413
+ expected = flight_test[:expected]
414
+
415
+ should "for #{flight_test[:name]}" do
416
+ File.stubs(:read => config.to_json)
417
+ flights = Pigeons.assemble
418
+ if flights['bflight'].nil? # Test just one
419
+ assert_same_elements expected.map { |e| e.to_sql }, flights['aflight'].map { |l| l[:scope].to_sql }
420
+ else
421
+ assert_same_elements expected[:aflight].map { |e| e.to_sql }, flights['aflight'].map { |l| l[:scope].to_sql }
422
+ assert_same_elements expected[:bflight].map { |e| e.to_sql }, flights['bflight'].map { |l| l[:scope].to_sql }
423
+ end
424
+ end
425
+ end
426
+
427
+ end
428
+
429
+ end
metadata ADDED
@@ -0,0 +1,200 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pigeons
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1pre
5
+ prerelease: 5
6
+ platform: ruby
7
+ authors:
8
+ - Geoff Hayes
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: activerecord
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: actionmailer
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: mocha
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: shoulda
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: test-unit
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rake
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: sqlite3
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ description: Pigeons makes it a breeze to send your users lifecycle e-mails.
143
+ email:
144
+ - geoff@safeshepherd.com
145
+ executables: []
146
+ extensions: []
147
+ extra_rdoc_files: []
148
+ files:
149
+ - .gitignore
150
+ - Gemfile
151
+ - LICENSE.txt
152
+ - README.md
153
+ - Rakefile
154
+ - lib/generators/pigeons/install_generator.rb
155
+ - lib/generators/pigeons/templates/pigeon_letter.rb
156
+ - lib/generators/pigeons/templates/pigeon_letter_migration.rb
157
+ - lib/generators/pigeons/templates/pigeon_mailer.rb
158
+ - lib/generators/pigeons/templates/pigeons.json
159
+ - lib/pigeons.rb
160
+ - lib/pigeons/checks.rb
161
+ - lib/pigeons/elements.rb
162
+ - lib/pigeons/errors.rb
163
+ - lib/pigeons/extensions.rb
164
+ - lib/pigeons/logger.rb
165
+ - lib/pigeons/pigeons.rb
166
+ - lib/pigeons/pigeons_tasks.rb
167
+ - lib/pigeons/scope.rb
168
+ - lib/pigeons/version.rb
169
+ - pigeons.gemspec
170
+ - tasks/pigeons.rake
171
+ - test/test_helper.rb
172
+ - test/test_pigeons.rb
173
+ homepage: https://github.com/hayesgm/pigeons
174
+ licenses: []
175
+ post_install_message:
176
+ rdoc_options: []
177
+ require_paths:
178
+ - lib
179
+ required_ruby_version: !ruby/object:Gem::Requirement
180
+ none: false
181
+ requirements:
182
+ - - ! '>='
183
+ - !ruby/object:Gem::Version
184
+ version: '0'
185
+ required_rubygems_version: !ruby/object:Gem::Requirement
186
+ none: false
187
+ requirements:
188
+ - - ! '>'
189
+ - !ruby/object:Gem::Version
190
+ version: 1.3.1
191
+ requirements: []
192
+ rubyforge_project:
193
+ rubygems_version: 1.8.25
194
+ signing_key:
195
+ specification_version: 3
196
+ summary: Pigeons provides an extensible way to send our lifecycle e-mails through
197
+ simple human-readable syntax
198
+ test_files:
199
+ - test/test_helper.rb
200
+ - test/test_pigeons.rb