primer 0.1.0

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.
Files changed (46) hide show
  1. data/README.rdoc +213 -0
  2. data/example/README.rdoc +69 -0
  3. data/example/application.rb +26 -0
  4. data/example/config.ru +5 -0
  5. data/example/environment.rb +31 -0
  6. data/example/models/blog_post.rb +4 -0
  7. data/example/models/connection.rb +10 -0
  8. data/example/public/style.css +75 -0
  9. data/example/script/setup_database.rb +11 -0
  10. data/example/views/index.erb +13 -0
  11. data/example/views/layout.erb +26 -0
  12. data/example/views/show.erb +7 -0
  13. data/example/worker.rb +3 -0
  14. data/lib/javascript/primer.js +36 -0
  15. data/lib/primer/bus/amqp.rb +43 -0
  16. data/lib/primer/bus/memory.rb +12 -0
  17. data/lib/primer/bus.rb +30 -0
  18. data/lib/primer/cache/memory.rb +60 -0
  19. data/lib/primer/cache/redis.rb +70 -0
  20. data/lib/primer/cache.rb +84 -0
  21. data/lib/primer/enabler.rb +18 -0
  22. data/lib/primer/helpers.rb +66 -0
  23. data/lib/primer/real_time.rb +80 -0
  24. data/lib/primer/route_set.rb +50 -0
  25. data/lib/primer/watcher/active_record_macros.rb +70 -0
  26. data/lib/primer/watcher/macros.rb +70 -0
  27. data/lib/primer/watcher.rb +62 -0
  28. data/lib/primer/worker/active_record_agent.rb +120 -0
  29. data/lib/primer/worker.rb +34 -0
  30. data/lib/primer.rb +31 -0
  31. data/spec/models/artist.rb +10 -0
  32. data/spec/models/blog_post.rb +5 -0
  33. data/spec/models/calendar.rb +7 -0
  34. data/spec/models/concert.rb +6 -0
  35. data/spec/models/performance.rb +6 -0
  36. data/spec/models/person.rb +14 -0
  37. data/spec/models/watchable.rb +17 -0
  38. data/spec/primer/bus_spec.rb +31 -0
  39. data/spec/primer/cache_spec.rb +309 -0
  40. data/spec/primer/helpers/erb_spec.rb +89 -0
  41. data/spec/primer/watcher/active_record_spec.rb +189 -0
  42. data/spec/primer/watcher_spec.rb +101 -0
  43. data/spec/schema.rb +31 -0
  44. data/spec/spec_helper.rb +60 -0
  45. data/spec/templates/page.erb +3 -0
  46. metadata +235 -0
@@ -0,0 +1,34 @@
1
+ module Primer
2
+ class Worker
3
+
4
+ class ConfigError < StandardError ; end
5
+
6
+ autoload :ActiveRecordAgent, ROOT + '/primer/worker/active_record_agent'
7
+
8
+ def run
9
+ raise ConfigError.new('No cache present') unless Primer.cache
10
+ raise ConfigError.new('No message bus present') unless Primer.bus
11
+
12
+ puts "Cache: #{ Primer.cache }"
13
+ puts "Message bus: #{ Primer.bus }"
14
+ puts
15
+
16
+ EM.run {
17
+ Primer.bus.subscribe :active_record do |args|
18
+ puts "[active_record] #{ args.inspect }"
19
+ end
20
+ Primer.bus.subscribe :changes do |args|
21
+ puts "[changes] #{ args.inspect }"
22
+ end
23
+
24
+ ActiveRecordAgent.bind_to_bus
25
+ Primer.cache.bind_to_bus
26
+
27
+ puts "Listening for messages..."
28
+ puts
29
+ }
30
+ end
31
+
32
+ end
33
+ end
34
+
data/lib/primer.rb ADDED
@@ -0,0 +1,31 @@
1
+ require 'faye'
2
+ require 'set'
3
+ require 'yaml'
4
+
5
+ module Primer
6
+ ROOT = File.expand_path(File.dirname(__FILE__))
7
+ VERSION = '0.1.0'
8
+
9
+ class InvalidKey < StandardError ; end
10
+ class RouteNotFound < StandardError ; end
11
+
12
+ autoload :Cache, ROOT + '/primer/cache'
13
+ autoload :Bus, ROOT + '/primer/bus'
14
+ autoload :RouteSet, ROOT + '/primer/route_set'
15
+ autoload :Enabler, ROOT + '/primer/enabler'
16
+ autoload :Watcher, ROOT + '/primer/watcher'
17
+ autoload :Helpers, ROOT + '/primer/helpers'
18
+ autoload :RealTime, ROOT + '/primer/real_time'
19
+ autoload :Worker, ROOT + '/primer/worker'
20
+
21
+ class << self
22
+ attr_accessor :cache, :bus, :real_time
23
+ end
24
+
25
+ self.bus = Bus::Memory.new
26
+
27
+ def self.worker!
28
+ Worker.new.run
29
+ end
30
+ end
31
+
@@ -0,0 +1,10 @@
1
+ class Artist < ActiveRecord::Base
2
+ has_many :performances
3
+ has_many :concerts, :through => :performances
4
+
5
+ belongs_to :calendar
6
+ has_many :upcoming_gigs, :through => :calendar, :source => :gigs
7
+
8
+ include Primer::Watcher
9
+ end
10
+
@@ -0,0 +1,5 @@
1
+ class BlogPost < ActiveRecord::Base
2
+ belongs_to :person
3
+ include Primer::Watcher
4
+ end
5
+
@@ -0,0 +1,7 @@
1
+ class Calendar < ActiveRecord::Base
2
+ has_many :artists
3
+ has_many :gigs, :class_name => 'Concert'
4
+
5
+ include Primer::Watcher
6
+ end
7
+
@@ -0,0 +1,6 @@
1
+ class Concert < ActiveRecord::Base
2
+ belongs_to :calendar
3
+ has_many :performances, :dependent => :destroy
4
+ include Primer::Watcher
5
+ end
6
+
@@ -0,0 +1,6 @@
1
+ class Performance < ActiveRecord::Base
2
+ belongs_to :artist
3
+ belongs_to :concert
4
+ include Primer::Watcher
5
+ end
6
+
@@ -0,0 +1,14 @@
1
+ class Person < ActiveRecord::Base
2
+ has_many :blog_posts
3
+
4
+ include Primer::Watcher
5
+
6
+ def all_attributes
7
+ [id, the_name]
8
+ end
9
+
10
+ def the_name
11
+ name
12
+ end
13
+ end
14
+
@@ -0,0 +1,17 @@
1
+ class Watchable
2
+ include Primer::Watcher
3
+ watch_calls_to :name, :is_called?
4
+
5
+ def initialize(name)
6
+ @name = name
7
+ end
8
+
9
+ def name
10
+ @name
11
+ end
12
+
13
+ def is_called?(name)
14
+ @name == name
15
+ end
16
+ end
17
+
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for "primer event bus" do
4
+ before do
5
+ @message = nil
6
+ bus.subscribe(:changes) { |message| @message = message }
7
+ end
8
+
9
+ it "transmits messages verbatim" do
10
+ bus.publish :changes, ["series", "of", "params"]
11
+ sleep 1.0
12
+ @message.should == ["series", "of", "params"]
13
+ end
14
+
15
+ it "routes messages to the right channel" do
16
+ bus.publish :other, "something"
17
+ sleep 1.0
18
+ @message.should be_nil
19
+ end
20
+ end
21
+
22
+ describe Primer::Bus::Memory do
23
+ let(:bus) { Primer::Bus::Memory.new }
24
+ it_should_behave_like "primer event bus"
25
+ end
26
+
27
+ describe Primer::Bus::AMQP do
28
+ let(:bus) { Primer::Bus::AMQP.new(:queue => 'data_changes') }
29
+ it_should_behave_like "primer event bus"
30
+ end
31
+
@@ -0,0 +1,309 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for "primer cache" do
4
+ before do
5
+ Primer.cache = cache
6
+ Primer.bus = bus
7
+
8
+ cache.bind_to_bus
9
+ Primer::Worker::ActiveRecordAgent.bind_to_bus
10
+ sync
11
+
12
+ @person = Person.create(:name => "Abe")
13
+ @impostor = Person.create(:name => "Aaron")
14
+ @post = BlogPost.create(:person => @impostor, :title => "roflmillions")
15
+ end
16
+
17
+ def sync
18
+ sleep sync_time
19
+ end
20
+
21
+ describe "#compute with a block" do
22
+ def compute_value
23
+ cache.compute("/people/abe/name") { @person.name }
24
+ end
25
+
26
+ it "returns the value of the block" do
27
+ compute_value.should == "Abe"
28
+ end
29
+
30
+ it "calls the implementation to get the value" do
31
+ @person.should_receive(:name)
32
+ compute_value
33
+ end
34
+
35
+ it "stores the result of the computation" do
36
+ cache.should_receive(:put).with("/people/abe/name", "Abe")
37
+ compute_value
38
+ end
39
+
40
+ it "notes that the value is related to some ActiveRecord data" do
41
+ cache.should_receive(:relate).with("/people/abe/name", [["ActiveRecord", "Person", @person.id, "name"]])
42
+ compute_value
43
+ end
44
+
45
+ describe "when the value is already known" do
46
+ before { compute_value }
47
+
48
+ it "returns the value of the block" do
49
+ compute_value.should == "Abe"
50
+ end
51
+
52
+ it "does not call the implementation" do
53
+ @person.should_not_receive(:name)
54
+ compute_value
55
+ end
56
+
57
+ it "invalidates the cache when related data changes" do
58
+ cache.should_receive(:invalidate).with("/people/abe/name")
59
+ @person.update_attribute(:name, "Aaron")
60
+ sync
61
+ end
62
+
63
+ it "does not invalidate the cache when a different object changes" do
64
+ cache.should_not_receive(:invalidate)
65
+ @impostor.update_attribute(:name, "Weeble")
66
+ sync
67
+ end
68
+
69
+ it "does not invalidate the cache when unrelated data changes" do
70
+ cache.should_not_receive(:invalidate)
71
+ @person.update_attribute(:age, 28)
72
+ sync
73
+ end
74
+ end
75
+ end
76
+
77
+ describe "#compute without a block" do
78
+ let(:compute_value) { cache.compute("/name") }
79
+ let(:compute_count) { cache.compute("/count") }
80
+ let(:compute_author) { cache.compute("/author") }
81
+
82
+ before do
83
+ cache.routes = Primer::RouteSet.new do
84
+ get('/name') { Person.first.name }
85
+ get('/bar/:id') { params[:id] }
86
+ get('/count') { Person.first.blog_posts.count }
87
+ get('/author') { BlogPost.first.person.name }
88
+ end
89
+ end
90
+
91
+ it "uses the routes to find the right value" do
92
+ compute_value.should == "Abe"
93
+ end
94
+
95
+ it "stores the result of the computation" do
96
+ cache.should_receive(:put).with("/name", "Abe")
97
+ compute_value
98
+ end
99
+
100
+ it "notes that the value is related to some ActiveRecord data" do
101
+ cache.should_receive(:relate).with("/name", [["ActiveRecord", "Person", @person.id, "name"]])
102
+ compute_value
103
+ end
104
+
105
+ it "handles routing patterns and params" do
106
+ cache.should_receive(:put).with("/bar/pattern_match", "pattern_match")
107
+ cache.should_receive(:relate).with("/bar/pattern_match", [])
108
+ cache.compute("/bar/pattern_match").should == "pattern_match"
109
+ end
110
+
111
+ it "raise an error for paths that don't match anything" do
112
+ cache.should_not_receive(:put)
113
+ cache.should_not_receive(:relate)
114
+ lambda { cache.compute("/qux") }.should raise_error(Primer::RouteNotFound)
115
+ end
116
+
117
+ describe "when the value is already known" do
118
+ before do
119
+ compute_value
120
+ compute_count
121
+ compute_author
122
+ end
123
+
124
+ it "returns the value of the block" do
125
+ compute_value.should == "Abe"
126
+ end
127
+
128
+ it "does not call the implementation" do
129
+ @person.should_not_receive(:name)
130
+ compute_value
131
+ end
132
+
133
+ it "regenerates the cache when related data changes" do
134
+ @person.update_attribute(:name, "Aaron")
135
+ sync
136
+ cache.get("/name").should == "Aaron"
137
+ end
138
+
139
+ it "regenerates the cache when an associated collection changes" do
140
+ BlogPost.create(:person => @person, :title => "ROFLscale")
141
+ sync
142
+ cache.get("/count").to_i.should == 1
143
+ @person.blog_posts.destroy_all
144
+ sync
145
+ cache.get("/count").to_i.should == 0
146
+ end
147
+
148
+ it "regenerates the cache when an association is changed" do
149
+ @post.update_attribute(:person, @person)
150
+ sync
151
+ cache.get("/author").should == "Abe"
152
+ end
153
+
154
+ it "regenerates the cache when an associated object changes" do
155
+ @impostor.update_attribute(:name, "Steve")
156
+ sync
157
+ cache.get("/author").should == "Steve"
158
+ end
159
+
160
+ it "removes a cache key when data it uses is deleted" do
161
+ cache.compute("/aaron") { Person.find_by_name("Aaron").name }
162
+ sync
163
+ cache.get("/aaron").should == "Aaron"
164
+ @impostor.destroy
165
+ sync
166
+ cache.get("/aaron").should be_nil
167
+ cache.has_key?("/aaron").should be_false
168
+ end
169
+ end
170
+ end
171
+
172
+ describe "nested cache values" do
173
+ before do
174
+ cache.routes = Primer::RouteSet.new do
175
+ get('/title') { BlogPost.first.title }
176
+ get('/content') { BlogPost.first.person.name + Primer.cache.compute('/title') }
177
+ end
178
+ end
179
+
180
+ it "returns the correct value for the outer computation" do
181
+ cache.compute("/content").should == "Aaronroflmillions"
182
+ end
183
+
184
+ describe "when the inner cache value is known before computing the outer value" do
185
+ before do
186
+ cache.compute("/title")
187
+ cache.compute("/content")
188
+ end
189
+
190
+ it "returns the correct value for the outer computation" do
191
+ cache.compute("/content").should == "Aaronroflmillions"
192
+ end
193
+
194
+ it "updates the cache when the title changes" do
195
+ @post.update_attribute(:title, "It's toasted")
196
+ sync
197
+ cache.get("/title").should == "It's toasted"
198
+ cache.get("/content").should == "AaronIt's toasted"
199
+ end
200
+ end
201
+ end
202
+
203
+ describe "throttling" do
204
+ before do
205
+ cache.routes = Primer::RouteSet.new do
206
+ get("/data") { [BlogPost.first.title, Person.first.name] }
207
+ end
208
+ cache.throttle = 0.5
209
+ cache.compute("/data")
210
+ end
211
+
212
+ it "restricts how often the cache is recalculated" do
213
+ cache.should_receive(:regenerate).once
214
+ @post.update_attribute(:title, "The new title")
215
+ @person.update_attribute(:name, "The new name")
216
+ sleep 1.0
217
+ end
218
+
219
+ it "regenerates after the given throttle time, not after the first trigger" do
220
+ @post.update_attribute(:title, "The new title")
221
+ cache.get("/data").should == ["roflmillions", "Abe"]
222
+
223
+ @person.update_attribute(:name, "The new name")
224
+ cache.get("/data").should == ["roflmillions", "Abe"]
225
+
226
+ sleep 1.0
227
+ cache.get("/data").should == ["The new title", "The new name"]
228
+ end
229
+ end
230
+
231
+ describe "#get" do
232
+ it "can be caught by the Watcher" do
233
+ calls = []
234
+ Primer::Watcher.watching(calls) { cache.get("/a/key") }
235
+ calls.should == [[cache, :get, ["/a/key"], nil, nil]]
236
+ end
237
+ end
238
+
239
+ describe "#put" do
240
+ before { cache.get("/key").should be_nil }
241
+
242
+ it "writes a value to the cache" do
243
+ cache.put("/key", "value")
244
+ cache.get("/key").should == "value"
245
+ end
246
+
247
+ it "raises an error if any invalid key is used" do
248
+ lambda { cache.put("invalid", "hmm") }.should raise_error(Primer::InvalidKey)
249
+ end
250
+
251
+ it "can store arbitrary data" do
252
+ value = ["foo", 4, [5, :bar], {:qux => [6, 7]}]
253
+ cache.put("/key", value)
254
+ cache.get("/key").should == value
255
+ end
256
+ end
257
+
258
+ describe "#invalidate" do
259
+ before { cache.put("/some/key", "value") }
260
+
261
+ it "removes the key from the cache" do
262
+ cache.invalidate("/some/key")
263
+ cache.get("/some/key").should be_nil
264
+ end
265
+
266
+ describe "when a cache value has been generated from a computation" do
267
+ before { cache.compute("/people/abe/name") { @person.name } }
268
+
269
+ it "removes existing relations between the model and the cache" do
270
+ cache.invalidate("/people/abe/name")
271
+ cache.should_not_receive(:invalidate)
272
+ @person.update_attribute(:name, "Weeble")
273
+ end
274
+ end
275
+ end
276
+
277
+ describe "#clear" do
278
+ before { cache.put("/some/key", "value") }
279
+
280
+ it "empties the cache" do
281
+ cache.clear
282
+ cache.get("/some/key").should be_nil
283
+ end
284
+ end
285
+ end
286
+
287
+ describe Primer::Cache::Memory do
288
+ let(:cache) { Primer::Cache::Memory.new }
289
+
290
+ describe "with an in-memory bus" do
291
+ let(:bus) { Primer::Bus::Memory.new }
292
+ let(:sync_time) { 0 }
293
+ it_should_behave_like "primer cache"
294
+ end
295
+
296
+ describe "with an AMQP bus" do
297
+ let(:bus) { Primer::Bus::AMQP.new(:queue => "data_changes_#{Helper.next_id}") }
298
+ let(:sync_time) { 0.2 }
299
+ it_should_behave_like "primer cache"
300
+ end
301
+ end
302
+
303
+ describe Primer::Cache::Redis do
304
+ let(:cache) { Primer::Cache::Redis.new }
305
+ let(:bus) { Primer::Bus::Memory.new }
306
+ let(:sync_time) { 0 }
307
+ it_should_behave_like "primer cache"
308
+ end
309
+
@@ -0,0 +1,89 @@
1
+ require 'spec_helper'
2
+
3
+ module RenderingHelper
4
+ include Primer::Helpers::ERB
5
+ attr_accessor :name
6
+ end
7
+
8
+ class Context
9
+ include RenderingHelper
10
+ end
11
+
12
+ shared_examples_for "erb helper" do
13
+ before { context.name = "Aaron" }
14
+
15
+ before do
16
+ Primer.cache = Primer::Cache::Memory.new
17
+ Primer.cache.routes = Primer::RouteSet.new do
18
+ get('/user') { "Master" }
19
+ end
20
+ end
21
+
22
+ describe "#primer" do
23
+ describe "with an empty cache" do
24
+ it "renders the contents of the block" do
25
+ output.should == "Welcome, Master\nHello, Aaron\nThanks\n"
26
+ end
27
+
28
+ it "uses the model to get data" do
29
+ context.should_receive(:name).and_return("Abe")
30
+ output.should == "Welcome, Master\nHello, Abe\nThanks\n"
31
+ end
32
+
33
+ it "writes the block result to the cache" do
34
+ Primer.cache.should_receive(:put).with("/user", "Master")
35
+ Primer.cache.should_receive(:put).with("/cache/key", "Hello, Aaron")
36
+ output
37
+ end
38
+ end
39
+
40
+ describe "with a value in the cache" do
41
+ before { Primer.cache.put("/cache/key", "Text from the cache") }
42
+
43
+ it "does not use the model to get data" do
44
+ context.should_not_receive(:name)
45
+ output
46
+ end
47
+
48
+ it "renders the cached value into the template" do
49
+ output.should == "Welcome, Master\nText from the cache\nThanks\n"
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ describe "Rails3 ERB templates" do
56
+ class ApplicationController < ActionController::Base
57
+ def self._helpers
58
+ RenderingHelper
59
+ end
60
+ end
61
+
62
+ before do
63
+ view_context = context
64
+ controller.stub(:view_context).and_return(view_context)
65
+ end
66
+
67
+ let(:controller) { ApplicationController.new }
68
+ let(:context) { controller.view_context }
69
+
70
+ let :output do
71
+ controller.render(:file => "spec/templates/page.erb", :action => "show") rescue
72
+ body = controller.response_body
73
+ Array === body ? body.first : body
74
+ end
75
+
76
+ it_should_behave_like "erb helper"
77
+ end
78
+
79
+ describe "Sinatra ERB templates" do
80
+ let(:context) { Context.new }
81
+
82
+ let :output do
83
+ template = Tilt[:erb].new("spec/templates/page.erb", 0, :outvar => "@_out_buf")
84
+ template.render(context)
85
+ end
86
+
87
+ it_should_behave_like "erb helper"
88
+ end
89
+