primer 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/History.txt +17 -0
  2. data/README.rdoc +74 -35
  3. data/example/README.rdoc +33 -23
  4. data/example/application.rb +2 -2
  5. data/example/console.rb +133 -0
  6. data/example/db/blog.sqlite3 +0 -0
  7. data/example/{models → db}/connection.rb +0 -0
  8. data/example/environment.rb +17 -4
  9. data/example/models/comment.rb +5 -0
  10. data/example/models/post.rb +5 -0
  11. data/example/script/setup_database.rb +8 -2
  12. data/example/views/comments.erb +6 -0
  13. data/example/views/layout.erb +1 -1
  14. data/example/views/show.erb +5 -1
  15. data/lib/primer/bus/amqp.rb +7 -5
  16. data/lib/primer/bus/amqp.rbc +1045 -0
  17. data/lib/primer/bus/memory.rbc +344 -0
  18. data/lib/primer/bus.rb +3 -2
  19. data/lib/primer/bus.rbc +872 -0
  20. data/lib/primer/cache/memory.rbc +1443 -0
  21. data/lib/primer/cache/redis.rb +2 -2
  22. data/lib/primer/cache/redis.rbc +1643 -0
  23. data/lib/primer/cache.rb +2 -16
  24. data/lib/primer/cache.rbc +1502 -0
  25. data/lib/primer/enabler.rbc +414 -0
  26. data/lib/primer/helpers.rbc +1438 -0
  27. data/lib/primer/lazyness.rb +63 -0
  28. data/lib/primer/lazyness.rbc +1442 -0
  29. data/lib/primer/real_time.rbc +1999 -0
  30. data/lib/primer/route_set.rb +1 -0
  31. data/lib/primer/route_set.rbc +1475 -0
  32. data/lib/primer/watcher/active_record_macros.rb +13 -3
  33. data/lib/primer/watcher/active_record_macros.rbc +1796 -0
  34. data/lib/primer/watcher/macros.rb +11 -14
  35. data/lib/primer/watcher/macros.rbc +1628 -0
  36. data/lib/primer/watcher.rb +2 -7
  37. data/lib/primer/watcher.rbc +1416 -0
  38. data/lib/primer/worker/active_record_agent.rb +30 -33
  39. data/lib/primer/worker/active_record_agent.rbc +2840 -0
  40. data/lib/primer/worker/changes_agent.rb +20 -0
  41. data/lib/primer/worker/changes_agent.rbc +578 -0
  42. data/lib/primer/worker.rb +20 -3
  43. data/lib/primer/worker.rbc +1254 -0
  44. data/lib/primer.rb +10 -2
  45. data/lib/primer.rbc +944 -0
  46. data/spec/db/test.sqlite3 +0 -0
  47. data/spec/models/artist.rb +3 -2
  48. data/spec/models/artist.rbc +288 -0
  49. data/spec/models/blog_post.rb +3 -1
  50. data/spec/models/blog_post.rbc +181 -0
  51. data/spec/models/calendar.rbc +209 -0
  52. data/spec/models/concert.rbc +211 -0
  53. data/spec/models/performance.rbc +177 -0
  54. data/spec/models/person.rb +1 -0
  55. data/spec/models/person.rbc +306 -0
  56. data/spec/models/watchable.rbc +363 -0
  57. data/spec/primer/bus_spec.rbc +940 -0
  58. data/spec/primer/cache_spec.rb +3 -3
  59. data/spec/primer/cache_spec.rbc +8535 -0
  60. data/spec/primer/helpers/erb_spec.rb +14 -14
  61. data/spec/primer/helpers/erb_spec.rbc +2485 -0
  62. data/spec/primer/lazyness_spec.rb +61 -0
  63. data/spec/primer/lazyness_spec.rbc +1408 -0
  64. data/spec/primer/watcher/active_record_spec.rb +15 -15
  65. data/spec/primer/watcher/active_record_spec.rbc +5202 -0
  66. data/spec/primer/watcher_spec.rbc +2645 -0
  67. data/spec/schema.rbc +775 -0
  68. data/spec/spec_helper.rb +3 -0
  69. data/spec/spec_helper.rbc +1193 -0
  70. data/spec/templates/page.erb +0 -1
  71. metadata +77 -70
  72. data/example/models/blog_post.rb +0 -4
data/History.txt ADDED
@@ -0,0 +1,17 @@
1
+ === 0.2.0 / 2011-07-14
2
+
3
+ * New Lazyness module to avoid ActiveRecord database calls when not needed
4
+ * Handle namespaced ActiveRecord classes when traversing associations
5
+ * Allow Watcher to be added to ActiveRecord classes before associations
6
+ * Ensure compatibility with latest Sinatra router
7
+ * Use AMQP::Channel instead of MQ when using newer amqp gems
8
+ * Add EventMachine-compatible console for platforms with blocking IRBs
9
+ * Move cache updating logic into Worker::ChangesAgent
10
+
11
+
12
+ === 0.1.0 / 2010-11-25
13
+
14
+ * Initial release. Includes caching, method logging, cache expiry and
15
+ regeneration, offline workers, real-time page updates and ActiveRecord
16
+ integration for updating object graphs
17
+
data/README.rdoc CHANGED
@@ -1,18 +1,18 @@
1
1
  = Primer
2
2
 
3
3
  This is an experiment to bring automatic cache expiry and regeneration to Rails.
4
- At Songkick[http://www.songkick.com], we have a ton of code that deals with caches
5
- and denormalization and messages and offline processing and it's hard to maintain.
6
- I want to get rid of it. All of it.
4
+ At Songkick[http://www.songkick.com], we have a ton of code that deals with
5
+ caches and denormalization and messages and offline processing and it's hard to
6
+ maintain. I want to get rid of it. All of it.
7
7
 
8
8
 
9
9
  == What?
10
10
 
11
11
  Inspired by LunaScript[http://asana.com/luna] and Fun[http://marcuswest.in/read/fun-intro/],
12
12
  I figured Ruby could figure out which values a computation uses, and use that to
13
- expire caches for you without having to write any expiry code. This turns out to be
14
- possible, at least for typical ActiveRecord usage, and Primer includes an engine
15
- for that.
13
+ expire caches for you without having to write any expiry code. This turns out to
14
+ be possible, at least for typical ActiveRecord usage, and Primer includes an
15
+ engine for that.
16
16
 
17
17
  Primer currently lets you do the following:
18
18
 
@@ -22,14 +22,14 @@ Primer currently lets you do the following:
22
22
  * Declare how caches should be recalculated for eager cache population
23
23
  * Update pages in real time when their data is updated
24
24
 
25
- It does all this without you having to write a single cache sweeper. You
26
- just declare how to render your site, Primer does the rest.
25
+ It does all this without you having to write a single cache sweeper. You just
26
+ declare how to render your site, Primer does the rest.
27
27
 
28
28
 
29
29
  == Enough waffle, show me the code!
30
30
 
31
31
  The following is the minimal, 'hello world' use case. Get your ActiveRecord
32
- model, put a mixn in it (<em>after</em> declaring the model's associations):
32
+ model, put a mixn in it:
33
33
 
34
34
  class BlogPost < ActiveRecord::Base
35
35
  include Primer::Watcher
@@ -62,30 +62,29 @@ output is cached, the block will not be called again. The cache is invalidated
62
62
  when (and only when) the title of <tt>@post</tt> changes; Primer figures this
63
63
  out and you don't need to write any cache sweeping code.
64
64
 
65
- Finally you need to bind the cache to the event bus, unless you want to run the
66
- cache monitoring work in a background process (see below):
65
+ Finally you need to run the cache agent, unless you want to run the cache
66
+ monitoring work in a background process (see below):
67
67
 
68
- Primer.cache.bind_to_bus
68
+ Primer::Worker::ChangesAgent.run!
69
69
 
70
70
  # If you're using ActiveRecord
71
- Primer::Worker::ActiveRecordAgent.bind_to_bus
71
+ Primer::Worker::ActiveRecordAgent.run!
72
72
 
73
73
 
74
74
  === Declaring cache generators
75
75
 
76
76
  You may have noticed that Primer forces the use of path-style keys for your
77
- cache. Instead of wrapping code you want to memoize in a block, you can
78
- declare how to calculate it elsewhere and use a router to map cache keys
79
- to calculations. For example we could rewrite our post title example like
80
- this:
77
+ cache. Instead of wrapping code you want to memoize in a block, you can declare
78
+ how to calculate it elsewhere and use a router to map cache keys to calculations.
79
+ For example we could rewrite our post title example like this:
81
80
 
82
81
  # views/posts/show.html.erb
83
82
  # note '=' sign here, not used with block form
84
83
 
85
84
  <%= primer "/posts/#{@post.id}/title" %>
86
85
 
87
- Then you can declare how to calculate this in a router attached to your
88
- cache object:
86
+ Then you can declare how to calculate this in a router attached to your cache
87
+ object:
89
88
 
90
89
  Primer.cache.routes do
91
90
  get "/posts/:id/title" do
@@ -96,19 +95,59 @@ cache object:
96
95
 
97
96
  The advantage of this is that the cache now has a way to generate cache values
98
97
  outside of your rendering stack, meaning that instead of just invalidating the
99
- cache it can actually calculate the new value so the cache is always ready
100
- for incoming requests.
98
+ cache it can actually calculate the new value so the cache is always ready for
99
+ incoming requests.
101
100
 
102
- It also means you can generate cache content offline; running the following
103
- will generate the cache of the first post's title:
101
+ It also means you can generate cache content offline; running the following will
102
+ generate the cache of the first post's title:
104
103
 
105
104
  Primer.cache.compute("/posts/1/title")
106
105
 
107
106
 
107
+ === Lazy loading
108
+
109
+ A common problem when caching view fragments is not knowing whether you need to
110
+ load database objects in the controller. If the parts of the view that use that
111
+ object are already cached, there's no point loading the object since it won't be
112
+ needed to render the page.
113
+
114
+ Rails has some lazy-loading capability built in, for example if you have a model
115
+ that says <tt>BlogPost.has_many :comments</tt>, a call to <tt>blog_post.comments</tt>
116
+ won't actually load the comments until you call <tt>each</tt> on that collection
117
+ to read the data. Primer introduces the same idea for all database calls.
118
+
119
+ If you <tt>include Primer::Lazyness</tt> in a model, then <tt>Model.find(id)</tt>
120
+ will not call the database. It just returns an object with the ID you asked for,
121
+ and won't actually load the model from the database until you try to access
122
+ other properties that we cannot infer from the <tt>find()</tt> call.
123
+
124
+ For example, this makes it easy to load a model in the controller and use its ID
125
+ to key cache fragments, safe in the knowledge that no unnecessary database calls
126
+ will be made.
127
+
128
+ BlogPost < ActiveRecord::Base
129
+ include Primer::Lazyness
130
+ end
131
+
132
+ # Does not call the database
133
+ post = BlogPost.find(1)
134
+
135
+ # Does not call the database unless the title is not cached
136
+ primer "/posts/#{post.id}/title" do
137
+ post.title
138
+ end
139
+
140
+ Primer also lazy-loads on other properties, for example if I call
141
+ <tt>BlogPost.find_by_title("Hello World")</tt> then Primer will create an object
142
+ whose <tt>title</tt> is <tt>"Hello World"</tt> without calling the database, but
143
+ then if I ask for the object's <tt>id</tt> or other properties the real model is
144
+ loaded.
145
+
146
+
108
147
  === Throttling
109
148
 
110
- Let's say you have a cache value that depends on many little bits of model
111
- data, a common situation in web front-ends. For example:
149
+ Let's say you have a cache value that depends on many little bits of model data,
150
+ a common situation in web front-ends. For example:
112
151
 
113
152
  Primer.cache.routes do
114
153
  get "/posts/:id/summary" do
@@ -158,15 +197,15 @@ To make a background worker, you just need a file like this:
158
197
  Primer.worker!
159
198
 
160
199
  Running <tt>ruby worker.rb</tt> will start a process in the shell that listens
161
- for change notifications and updates the cache for you. You can start as many
162
- of these as you like to spread the load out.
200
+ for change notifications and updates the cache for you. You can start as many of
201
+ these as you like to spread the load out.
163
202
 
164
203
 
165
204
  === Real-time page updates
166
205
 
167
- If you want to be properly web-scale, you'll need to be updating your pages
168
- in real time as your data changes. Primer lets you update any fragment generated
169
- by a block-less <tt>primer</tt> call in your view automatically.
206
+ If you want to be properly web-scale, you'll need to be updating your pages in
207
+ real time as your data changes. Primer lets you update any fragment generated by
208
+ a block-less <tt>primer</tt> call in your view automatically.
170
209
 
171
210
  All you need to do is place some middleware in your Rack config:
172
211
 
@@ -181,10 +220,10 @@ Add the client-side script to your templates (this must be in the <tt>HEAD</tt>)
181
220
 
182
221
  <script type="text/javascript" src="/primer.js"></script>
183
222
 
184
- Then configure it wherever your data model gets used to tell it you want to
185
- use real-time updates and where the messaging server is. You should also set
186
- a password - this will stop third parties being able to publish to the message
187
- bus and inject arbitrary HTML into your pages.
223
+ Then configure it wherever your data model gets used to tell it you want to use
224
+ real-time updates and where the messaging server is. You should also set a
225
+ password - this will stop third parties being able to publish to the message bus
226
+ and inject arbitrary HTML into your pages.
188
227
 
189
228
  Primer.real_time = true
190
229
  Primer::RealTime.bayeux_server = 'http://localhost:9292'
@@ -208,6 +247,6 @@ welcome, pull requests considered, bug reports likely to gather dust.
208
247
 
209
248
  == License
210
249
 
211
- Copyright (c) 2010 Songkick.com, James Coglan. Named by the inimitable
250
+ Copyright (c) 2010-2011 Songkick.com, James Coglan. Named by the inimitable
212
251
  grillpanda[http://github.com/grillpanda]. Released under the MIT license.
213
252
 
data/example/README.rdoc CHANGED
@@ -4,17 +4,19 @@ This is a simple app that demonstrates how you can use Primer. It's based on
4
4
  ActiveRecord for the model and Sinatra for the front-end. To start it up,
5
5
  install some gems, build the database then use Rack:
6
6
 
7
- gem install redis amqp faye sinatra activerecord sqlite3-ruby
7
+ bundle install
8
8
  ruby script/setup_database.rb
9
+ redis-server
10
+ rabbitmq-server
9
11
  rackup -s thin -E production config.ru
10
12
 
11
13
 
12
14
  === models
13
15
 
14
16
  Let's take a look at the internals. In the +models+ directory, you'll see a
15
- simple BlogPost model:
17
+ simple blog post model:
16
18
 
17
- class BlogPost < ActiveRecord::Base
19
+ class Post < ActiveRecord::Base
18
20
  include Primer::Watcher
19
21
  end
20
22
 
@@ -23,47 +25,55 @@ That's all we need for Primer to monitor how our data changes.
23
25
 
24
26
  === public, views
25
27
 
26
- The +public+ and +views+ directories are used by the Sinatra app, and should
27
- be pretty self-explanatory. Note which bits of the views are cached, and which
28
+ The +public+ and +views+ directories are used by the Sinatra app, and should be
29
+ pretty self-explanatory. Note which bits of the views are cached, and which
28
30
  parts don't use blocks - we set these keys up in <tt>environment.rb</tt>.
29
31
 
30
32
 
31
33
  === application.rb
32
34
 
33
- The <tt>application.rb</tt> file is a simple Sinatra app - it loads the
34
- model from <tt>environment.rb</tt> and just sets up a couple of pages.
35
+ The <tt>application.rb</tt> file is a simple Sinatra app - it loads the model
36
+ from <tt>environment.rb</tt> and just sets up a couple of pages.
35
37
 
36
38
 
37
39
  === environment.rb
38
40
 
39
- The meat of the system is in <tt>environment.rb</tt>. This file loads the
40
- gems we need, loads our models, and configures Primer. See that file for more
41
- details, but note that it uses an AMQP bus - this means we can compute cache
42
- changes outside the app process. You'll need to start <tt>worker.rb</tt> so
43
- that your changes get processed.
41
+ The meat of the system is in <tt>environment.rb</tt>. This file loads the gems
42
+ we need, loads our models, and configures Primer. See that file for more details,
43
+ but note that it uses an AMQP bus - this means we can compute cache changes
44
+ outside the app process. You'll need to start <tt>worker.rb</tt> so that your
45
+ changes get processed.
44
46
 
45
47
 
46
48
  === worker.rb
47
49
 
48
50
  This file just loads <tt>environment.rb</tt>, then tells Primer to start a
49
- worker. The call to <tt>Primer.worker!</tt> is blocking, and makes Primer
50
- pick up messages from the AMQP bus and process them. You can start as many
51
- of these as you like to distribute the work. Just run:
51
+ worker. The call to <tt>Primer.worker!</tt> is blocking, and makes Primer pick
52
+ up messages from the AMQP bus and process them. You can start as many of these
53
+ as you like to distribute the work. Just run:
52
54
 
53
55
  ruby worker.rb
54
56
 
55
57
  If you don't run any of these, messages will build up in AMQP until you start
56
- a worker. Each data change message goes to only one worker process so you
57
- won't duplicate any work.
58
+ a worker. Each data change message goes to only one worker process so you won't
59
+ duplicate any work.
58
60
 
59
61
 
60
- === console
62
+ === console.rb
61
63
 
62
- You can interact with the model by starting IRB with the app's environment:
64
+ You can interact with the model by starting IRB with the app's environment.
65
+ Because EventMachine does not play nice with IRB, you may need to use the custom
66
+ console to script the application:
63
67
 
64
- irb -r ./environment.rb
68
+ # in one shell:
69
+ ruby console.rb server
70
+
71
+ # in another shell
72
+ ruby console.rb client
73
+ >> require './environment'
74
+ >> etc.
65
75
 
66
- Go and create posts and change their data to see how the front-end reacts.
67
- To get the real-time updates to work you must have a worker running to
68
- update caches; this is not done by the web app process.
76
+ Go and create posts and change their data to see how the front-end reacts. To
77
+ get the real-time updates to work you must have a worker running to update
78
+ caches; this is not done by the web app process.
69
79
 
@@ -14,12 +14,12 @@ class Application < Sinatra::Base
14
14
  helpers { include Primer::Helpers::ERB }
15
15
 
16
16
  get '/' do
17
- @posts = BlogPost.all
17
+ @posts = Post.all
18
18
  erb :index
19
19
  end
20
20
 
21
21
  get '/posts/:id' do
22
- @post = BlogPost.find(params[:id])
22
+ @post = Post.find(params[:id])
23
23
  erb :show
24
24
  end
25
25
  end
@@ -0,0 +1,133 @@
1
+ # EventMachine-aware console by Aman Gupta
2
+ # https://gist.github.com/145471
3
+
4
+ require 'rubygems'
5
+ require 'eventmachine'
6
+ require 'stringio'
7
+
8
+ module Console
9
+ PROMPT = "\n>> ".freeze
10
+
11
+ def post_init
12
+ send_data PROMPT
13
+ send_data "\0"
14
+ end
15
+
16
+ def receive_data data
17
+ return close_connection if data =~ /exit|quit/
18
+
19
+ begin
20
+ @ret, @out, $stdout = :exception, $stdout, StringIO.new
21
+ @ret = eval(data, scope, '(rirb)')
22
+ rescue StandardError, ScriptError, Exception, SyntaxError
23
+ $! = RuntimeError.new("unknown exception raised") unless $!
24
+ print $!.class, ": ", $!, "\n"
25
+
26
+ trace = []
27
+ $!.backtrace.each do |line|
28
+ trace << "\tfrom #{line}"
29
+ break if line =~ /\(rirb\)/
30
+ end
31
+
32
+ puts trace
33
+ ensure
34
+ $stdout, @out = @out, $stdout
35
+ @out.rewind
36
+ @out = @out.read
37
+ end
38
+
39
+ send_data @out unless @out.empty?
40
+ send_data "=> #{@ret.inspect}" unless @ret == :exception
41
+ send_data "\0\n>> \0"
42
+ end
43
+
44
+ # def send_data data
45
+ # p ['server send', data]
46
+ # super
47
+ # end
48
+
49
+ def scope
50
+ @scope ||= instance_eval{ binding }
51
+ end
52
+
53
+ def handle_error
54
+ $! = RuntimeError.new("unknown exception raised") unless $!
55
+ print $!.class, ": ", $!, "\n"
56
+
57
+ trace = []
58
+ $!.backtrace.each do |line|
59
+ trace << "\\tfrom \#{line}"
60
+ break if line =~ /\(rirb\)/
61
+ end
62
+
63
+ puts trace
64
+ end
65
+
66
+ def self.start port = 7331
67
+ EM.run{
68
+ @server ||= EM.start_server '127.0.0.1', port, self
69
+ }
70
+ end
71
+
72
+ def self.stop
73
+ @server.close_connection if @server
74
+ @server = nil
75
+ end
76
+ end
77
+
78
+ module RIRB
79
+ def connection_completed
80
+ p 'connected to console'
81
+ end
82
+
83
+ def receive_data data
84
+ # p ['receive', data]
85
+ (@buffer ||= BufferedTokenizer.new("\0")).extract(data).each do |d|
86
+ process(d)
87
+ end
88
+ end
89
+
90
+ def process data
91
+ if data.strip == '>>'
92
+ while l = Readline.readline('>> ')
93
+ unless l.nil? or l.strip.empty?
94
+ Readline::HISTORY.push(l)
95
+ send_data l
96
+ break
97
+ end
98
+ end
99
+ else
100
+ puts data
101
+ end
102
+ end
103
+
104
+ def unbind
105
+ p 'disconnected'
106
+ EM.stop_event_loop
107
+ end
108
+
109
+ def self.connect host = 'localhost', port = 7331
110
+ require 'readline'
111
+ EM.run{
112
+ trap('INT'){ exit }
113
+ @connection.close_connection if @connection
114
+ @connection = EM.connect host, port, self do |c|
115
+ c.instance_eval{ @host, @port = host, port }
116
+ end
117
+ }
118
+ end
119
+ attr_reader :host, :port
120
+ end
121
+
122
+ if __FILE__ == $0
123
+ EM.run{
124
+ if ARGV[0] == 'server'
125
+ Console.start
126
+ elsif ARGV[0] == 'client'
127
+ RIRB.connect
128
+ else
129
+ puts "#{$0} <server|client>"
130
+ EM.stop
131
+ end
132
+ }
133
+ end
Binary file
File without changes
@@ -6,13 +6,19 @@ require 'active_record'
6
6
  require dir + '/../lib/primer'
7
7
 
8
8
  # Load database config and models
9
- require dir + '/models/connection'
10
- require dir + '/models/blog_post'
9
+ require dir + '/db/connection'
10
+ require dir + '/models/post'
11
+ require dir + '/models/comment'
11
12
 
12
13
  # Configure Primer with a Redis cache and AMQP bus
13
14
  Primer.cache = Primer::Cache::Redis.new
14
15
  Primer.bus = Primer::Bus::AMQP.new(:queue => 'blog_changes')
15
16
 
17
+ # If you don't want to use AMQP, delete the above bus config
18
+ # and add these lines to run invalidation work in this process:
19
+ # Primer::Worker::ActiveRecordAgent.run!
20
+ # Primer::Worker::ChangesAgent.run!
21
+
16
22
  # Enable real-time page updates
17
23
  Primer.real_time = true
18
24
  Primer::RealTime.bayeux_server = 'http://0.0.0.0:9292'
@@ -21,11 +27,18 @@ Primer::RealTime.password = 'omg_rofl_scale'
21
27
  # Set up cache generation routes
22
28
  Primer.cache.routes do
23
29
  get '/posts/:id/date' do
24
- BlogPost.find(params[:id]).created_at.strftime('%A %e %B %Y')
30
+ post = Post.find(params[:id])
31
+ post.created_at.strftime('%A %e %B %Y')
25
32
  end
26
33
 
27
34
  get '/posts/:id/title' do
28
- BlogPost.find(params[:id]).title.upcase
35
+ post = Post.find(params[:id])
36
+ post.title.upcase
37
+ end
38
+
39
+ get '/posts/:id/comments' do
40
+ @post = Post.find(params[:id])
41
+ ERB.new(File.read(dir + '/views/comments.erb')).result(binding)
29
42
  end
30
43
  end
31
44
 
@@ -0,0 +1,5 @@
1
+ class Comment < ActiveRecord::Base
2
+ include Primer::Watcher
3
+ belongs_to :post
4
+ end
5
+
@@ -0,0 +1,5 @@
1
+ class Post < ActiveRecord::Base
2
+ include Primer::Watcher
3
+ has_many :comments
4
+ end
5
+
@@ -1,11 +1,17 @@
1
- require File.expand_path(File.dirname(__FILE__)) + '/../models/connection'
1
+ require File.expand_path(File.dirname(__FILE__)) + '/../db/connection'
2
2
 
3
3
  ActiveRecord::Schema.define do |version|
4
- create_table :blog_posts, :force => true do |t|
4
+ create_table :posts, :force => true do |t|
5
5
  t.timestamps
6
6
  t.string :title
7
7
  t.text :body
8
8
  t.string :author
9
9
  end
10
+
11
+ create_table :comments, :force => true do |t|
12
+ t.timestamps
13
+ t.belongs_to :post
14
+ t.text :body
15
+ end
10
16
  end
11
17
 
@@ -0,0 +1,6 @@
1
+ <h4>Comments</h4>
2
+
3
+ <% @post.comments.each do |comment| %>
4
+ <h5><%= comment.created_at.strftime('%A %e %B %Y') %></h5>
5
+ <p><%= comment.body %></p>
6
+ <% end %>
@@ -18,7 +18,7 @@
18
18
  </div></div>
19
19
 
20
20
  <div class="footer"><div class="sub">
21
- <p>Copyright &copy; 2010 That&rsquo;s What She Said</p>
21
+ <p>Copyright &copy; 2011 I Can&rsquo;t Even Words</p>
22
22
  </div></div>
23
23
 
24
24
  </body>
@@ -1,7 +1,11 @@
1
1
  <h3><%= @post.title.upcase %></h3>
2
- <h4>By <%= @post.author %> &mdash; <%= primer "/posts/#{@post.id}/date", :span %></h4>
2
+ <h4>
3
+ By <%= @post.author %> &mdash;
4
+ <%= primer "/posts/#{@post.id}/date", :span %>
5
+ </h4>
3
6
 
4
7
  <% primer "/posts/#{@post.id}/body" do %>
5
8
  <p><%= @post.body %></p>
6
9
  <% end %>
7
10
 
11
+ <%= primer "/posts/#{@post.id}/comments" %>
@@ -1,4 +1,5 @@
1
- require 'mq'
1
+ require 'amqp'
2
+ require 'mq' unless defined? AMQP::Channel
2
3
 
3
4
  module Primer
4
5
  class Bus
@@ -10,8 +11,8 @@ module Primer
10
11
  end
11
12
 
12
13
  def publish(topic, message)
13
- tuple = [topic, message]
14
- queue.publish(YAML.dump(tuple))
14
+ tuple = [topic.to_s, message]
15
+ queue.publish(Primer.serialize(tuple))
15
16
  end
16
17
 
17
18
  def subscribe(*args, &block)
@@ -25,13 +26,14 @@ module Primer
25
26
  Faye.ensure_reactor_running!
26
27
  return @queue if defined?(@queue)
27
28
  raise "I need a queue name!" unless @config[:queue]
28
- @queue = MQ.new.queue(@config[:queue])
29
+ amqp_klass = defined?(MQ) ? MQ : ::AMQP::Channel
30
+ @queue = amqp_klass.new.queue(@config[:queue])
29
31
  end
30
32
 
31
33
  def bind
32
34
  return if @bound
33
35
  queue.subscribe do |message|
34
- data = YAML.load(message)
36
+ data = Primer.deserialize(message)
35
37
  distribute(*data)
36
38
  end
37
39
  @bound = true