goat 0.1.3
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.
- data/bin/yodel +28 -0
- data/lib/goat.rb +938 -0
- data/lib/goat/extn.rb +26 -0
- data/lib/goat/goat.js +172 -0
- data/lib/goat/html.rb +147 -0
- data/lib/goat/logger.rb +39 -0
- data/lib/goat/mongo.rb +28 -0
- data/lib/goat/notifications.rb +121 -0
- data/lib/goat/sinatra.rb +11 -0
- data/lib/goat/yodel.rb +38 -0
- data/lib/views/plain_layout.erb +7 -0
- metadata +77 -0
data/bin/yodel
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__), '../lib/goat/yodel')
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
$bport = 8000
|
7
|
+
$rport = 8001
|
8
|
+
$host = '127.0.0.1'
|
9
|
+
|
10
|
+
def usage
|
11
|
+
$stderr.puts <<-EOH
|
12
|
+
#{__FILE__} [-b broadcast-port] [-r receive-port] [-h host]
|
13
|
+
EOH
|
14
|
+
end
|
15
|
+
|
16
|
+
OptionParser.new do |opts|
|
17
|
+
# this is the jankiest optparser I've ever used
|
18
|
+
opts.on('-bMANDATORY', Integer) {|b| $bport = b}
|
19
|
+
opts.on('-rMANDATORY', Integer) {|r| $rport = r}
|
20
|
+
opts.on('-HMANDATORY', String) {|h| $host = h}
|
21
|
+
opts.on('-v') {} # suppress optparse wanting to use -v as version
|
22
|
+
opts.on('-h') {usage; exit 1}
|
23
|
+
end.parse!(ARGV)
|
24
|
+
|
25
|
+
EM.run do
|
26
|
+
Yodel::BroadcastServer.start($host, $bport)
|
27
|
+
Yodel::RecvServer.start($host, $rport)
|
28
|
+
end
|
data/lib/goat.rb
ADDED
@@ -0,0 +1,938 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'cgi'
|
3
|
+
require 'json'
|
4
|
+
require 'eventmachine'
|
5
|
+
require 'thin'
|
6
|
+
require 'term/ansicolor'
|
7
|
+
require 'tilt'
|
8
|
+
|
9
|
+
%w{logger notifications extn html}.each do |file|
|
10
|
+
require File.join(File.dirname(__FILE__), 'goat', file)
|
11
|
+
end
|
12
|
+
|
13
|
+
$verbose ||= ARGV.include?('-v')
|
14
|
+
|
15
|
+
class ActionHash < Hash; end
|
16
|
+
class ActionProc < Proc; end
|
17
|
+
|
18
|
+
def action(hash={})
|
19
|
+
hash
|
20
|
+
# ActionProc.new { hash }
|
21
|
+
# ActionHash[hash]
|
22
|
+
end
|
23
|
+
|
24
|
+
class Object
|
25
|
+
def self.make_me
|
26
|
+
meth = nil
|
27
|
+
if caller[1] =~ /`(.+)'/
|
28
|
+
meth = $1
|
29
|
+
end
|
30
|
+
|
31
|
+
raise("Subclass #{self.name} should implement" + (meth ? " #{meth}" : ''))
|
32
|
+
end
|
33
|
+
|
34
|
+
def make_me; self.class.make_me end
|
35
|
+
|
36
|
+
def self.kind_of?(cls)
|
37
|
+
self == cls || \
|
38
|
+
(self == Object ? \
|
39
|
+
false : \
|
40
|
+
(self.superclass && self.superclass != self && self.superclass.kind_of?(cls)))
|
41
|
+
end
|
42
|
+
|
43
|
+
def glimpse(n=100)
|
44
|
+
ins = self.inspect
|
45
|
+
if ins =~ />$/ && ins.size > n
|
46
|
+
"#{ins[0..n]}...>"
|
47
|
+
else
|
48
|
+
ins
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Set
|
54
|
+
def glimpse(n=100)
|
55
|
+
"#<Set: #{self.map{|x| x.glimpse(n)}.join(', ')}>"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Array
|
60
|
+
def glimpse(n=100)
|
61
|
+
"[" + self.map{|x| x.glimpse(n)}.join(', ') + "]"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class Hash
|
66
|
+
def glimpse(n=100)
|
67
|
+
"{" + self.map{|k, v| k.glimpse + "=>" + v.glimpse}.join(', ') + "}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Hash
|
72
|
+
def map_to_hash
|
73
|
+
h = {}
|
74
|
+
self.map do |k, v|
|
75
|
+
nk, nv = yield(k, v)
|
76
|
+
h[nk] = nv
|
77
|
+
end
|
78
|
+
h
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class String
|
83
|
+
def prefix_ns(ns)
|
84
|
+
self.gsub(/^%(.+)$/, "#{ns}_\\1")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
module Goat
|
89
|
+
def self.goat_path(f); File.join(File.dirname(__FILE__), 'goat', f); end
|
90
|
+
|
91
|
+
def self.extend_sinatra
|
92
|
+
require goat_path('sinatra')
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.extend_mongo
|
96
|
+
require goat_path('mongo')
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.enable_notifications(opts={})
|
100
|
+
NotificationCenter.configure(opts)
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.load_all(dir_fragment)
|
104
|
+
dir = File.join(File.dirname($0), dir_fragment)
|
105
|
+
if File.directory?(dir)
|
106
|
+
Dir.entries(dir).select{|f| f =~ /\.rb$/}.each {|f| require(File.join(dir, f))}
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
@settings = {}
|
111
|
+
def self.setting(opt)
|
112
|
+
@settings[opt]
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.setting!(opt)
|
116
|
+
if @settings.include?(opt)
|
117
|
+
@settings[opt]
|
118
|
+
else
|
119
|
+
raise "#{opt} not set"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.settings; @settings; end
|
124
|
+
|
125
|
+
def self.add_component_helpers(modul)
|
126
|
+
Goat::Component.send(:include, modul)
|
127
|
+
end
|
128
|
+
|
129
|
+
def self.configure(&blk)
|
130
|
+
blk.call
|
131
|
+
|
132
|
+
load_all('components')
|
133
|
+
load_all('pages')
|
134
|
+
|
135
|
+
Goat.extend_mongo if Goat.setting(:mongo)
|
136
|
+
Goat.extend_sinatra if Goat.setting(:sinatra)
|
137
|
+
|
138
|
+
if Goat.setting(:debug)
|
139
|
+
if defined?(Thin)
|
140
|
+
Thin::Logging.debug = true
|
141
|
+
end
|
142
|
+
|
143
|
+
Goat::Logger.levels = [:debug, :error]
|
144
|
+
end
|
145
|
+
|
146
|
+
NotificationCenter.configure(Goat.setting(:notifications)) if Goat.setting(:notifications)
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.rack_builder(app)
|
150
|
+
Rack::Builder.new do
|
151
|
+
if cookies = Goat.setting(:cookies)
|
152
|
+
use Rack::Session::Cookie, cookies
|
153
|
+
end
|
154
|
+
|
155
|
+
if static = Goat.setting(:static)
|
156
|
+
use Rack::Static, :urls => static.fetch(:urls), :root => static.fetch(:root)
|
157
|
+
end
|
158
|
+
|
159
|
+
use Rack::CommonLogger
|
160
|
+
use Rack::Flash if defined?(Rack::Flash) # TODO hack
|
161
|
+
|
162
|
+
run app
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
class NotFoundError < RuntimeError
|
167
|
+
attr_reader :path
|
168
|
+
|
169
|
+
def initialize(path)
|
170
|
+
@path = path
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class ChannelPusher
|
175
|
+
include EM::Deferrable
|
176
|
+
|
177
|
+
def initialize(channel)
|
178
|
+
@channel = channel
|
179
|
+
@finished = false
|
180
|
+
@messages = []
|
181
|
+
|
182
|
+
self.errback do
|
183
|
+
Logger.log :live, "Channel closed"
|
184
|
+
@finished = true
|
185
|
+
end
|
186
|
+
|
187
|
+
self.callback do
|
188
|
+
@finished = true
|
189
|
+
@channel.emchannel.unsubscribe(@subscription) if @subscription
|
190
|
+
end
|
191
|
+
|
192
|
+
@subscription = @channel.emchannel.subscribe do |msg|
|
193
|
+
@messages << msg
|
194
|
+
# send on next tick so we get a batch of messages together, rather than a single message
|
195
|
+
EM.next_tick { self.send_and_finish }
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def messages_without_duplicates
|
200
|
+
@ids = Set.new
|
201
|
+
@messages.select do |x|
|
202
|
+
if x.include?(:id)
|
203
|
+
id = x[:id]
|
204
|
+
if @ids.include?(id)
|
205
|
+
nil
|
206
|
+
else
|
207
|
+
@ids << id
|
208
|
+
x
|
209
|
+
end
|
210
|
+
else
|
211
|
+
x
|
212
|
+
end
|
213
|
+
end.compact
|
214
|
+
end
|
215
|
+
|
216
|
+
def send_and_finish
|
217
|
+
return if @finished # may be called several times
|
218
|
+
@body_callback.call({'messages' => messages_without_duplicates}.to_json)
|
219
|
+
succeed
|
220
|
+
end
|
221
|
+
|
222
|
+
def each(&blk)
|
223
|
+
@body_callback = blk
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
class Halt < Exception
|
228
|
+
attr_reader :response
|
229
|
+
|
230
|
+
def initialize(response)
|
231
|
+
@response = response
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
class ReqHandler
|
236
|
+
|
237
|
+
# we can't know this at initialize time because we start using
|
238
|
+
# the req handler in context of Goat::App, which would lead to
|
239
|
+
# app = Goat::App, which is wrong
|
240
|
+
attr_accessor :app_class
|
241
|
+
|
242
|
+
def initialize
|
243
|
+
@before_handler_bindings = {}
|
244
|
+
end
|
245
|
+
|
246
|
+
class ::Proc
|
247
|
+
def handle_request(app)
|
248
|
+
self.call(app)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def mappings
|
253
|
+
@map ||= Hash.new {|h, k| h[k] = Hash.new}
|
254
|
+
end
|
255
|
+
|
256
|
+
def add_mapping(opts)
|
257
|
+
opts = {
|
258
|
+
:metal => false,
|
259
|
+
:method => :get
|
260
|
+
}.merge(opts)
|
261
|
+
|
262
|
+
%w{method path hook}.each do |key|
|
263
|
+
raise "#{key} must be set" unless opts[key.to_sym]
|
264
|
+
end
|
265
|
+
|
266
|
+
method = opts[:method]
|
267
|
+
path = opts[:path]
|
268
|
+
hook = opts[:hook]
|
269
|
+
|
270
|
+
if hook.kind_of?(Proc) && !opts[:metal]
|
271
|
+
hook = App.bind(opts[:hook])
|
272
|
+
end
|
273
|
+
|
274
|
+
mappings[method][path] = hook
|
275
|
+
end
|
276
|
+
|
277
|
+
def before_handler_binding(handler)
|
278
|
+
@before_handler_bindings[handler] ||= App.bind(handler)
|
279
|
+
end
|
280
|
+
|
281
|
+
def run_before_handlers(app)
|
282
|
+
app.class.before_handlers.each do |handler|
|
283
|
+
before_handler_binding(handler).call(app)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def resp_for_error(e, app)
|
288
|
+
resp = nil
|
289
|
+
|
290
|
+
if e.kind_of?(NotFoundError)
|
291
|
+
# 404
|
292
|
+
if app.class.not_found_handler
|
293
|
+
@not_found_binding ||= App.bind(app.class.not_found_handler)
|
294
|
+
resp = @not_found_binding.call(app, e)
|
295
|
+
else
|
296
|
+
resp = [404, {}, 'not found']
|
297
|
+
end
|
298
|
+
else
|
299
|
+
# not a 404 -- an actual problem
|
300
|
+
if app.class.error_handler
|
301
|
+
@error_handler_binding ||= App.bind(app.class.error_handler)
|
302
|
+
resp = @error_handler_binding.call(app, e)
|
303
|
+
else
|
304
|
+
Logger.error(:req, e.inspect)
|
305
|
+
Logger.error(:req, e.backtrace.join("\n"))
|
306
|
+
|
307
|
+
resp = Rack::Response.new
|
308
|
+
resp.status = 500
|
309
|
+
resp['Content-Type'] = 'text/plain'
|
310
|
+
resp.body = e.inspect + "\n" + e.backtrace.join("\n")
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
resp
|
315
|
+
end
|
316
|
+
|
317
|
+
def handle_request(env)
|
318
|
+
path = env['PATH_INFO']
|
319
|
+
meth = env['REQUEST_METHOD']
|
320
|
+
hook = mappings[meth.downcase.to_sym][path]
|
321
|
+
hdrs = {}
|
322
|
+
resp = nil
|
323
|
+
|
324
|
+
begin
|
325
|
+
req = Rack::Request.new(env)
|
326
|
+
|
327
|
+
app = @app_class.new(req)
|
328
|
+
|
329
|
+
begin
|
330
|
+
run_before_handlers(app)
|
331
|
+
rescue Halt => halt
|
332
|
+
return halt.response.to_a
|
333
|
+
end
|
334
|
+
|
335
|
+
if hook
|
336
|
+
resp = hook.handle_request(app)
|
337
|
+
else
|
338
|
+
raise NotFoundError.new(path)
|
339
|
+
end
|
340
|
+
rescue Halt => halt
|
341
|
+
resp = halt.response
|
342
|
+
rescue Exception => e
|
343
|
+
resp = resp_for_error(e, app)
|
344
|
+
end
|
345
|
+
|
346
|
+
resp.to_a
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
module ERBHelper
|
351
|
+
def erb(name, opts={}, &blk)
|
352
|
+
opts = {
|
353
|
+
:partial => false,
|
354
|
+
:layout => true,
|
355
|
+
:locals => {}
|
356
|
+
}.merge(opts)
|
357
|
+
|
358
|
+
partial = opts[:partial]
|
359
|
+
use_layout = opts[:layout]
|
360
|
+
locals = opts[:locals]
|
361
|
+
|
362
|
+
if partial
|
363
|
+
name = name.to_s
|
364
|
+
d = File.dirname(name)
|
365
|
+
d = d == '.' ? '' : "#{d}/"
|
366
|
+
f = File.basename(name)
|
367
|
+
|
368
|
+
# slashes are actually allowed in syms
|
369
|
+
name = "#{d}_#{f}".to_sym
|
370
|
+
end
|
371
|
+
|
372
|
+
if name =~ /\.erb$/ # allow an absolute path to be passed
|
373
|
+
erbf = name
|
374
|
+
else
|
375
|
+
erbf = File.join(Goat.setting!(:root), 'views', "#{name}.erb")
|
376
|
+
end
|
377
|
+
|
378
|
+
layf = File.join(Goat.setting!(:root), 'views', 'layout.erb')
|
379
|
+
template = Tilt[:erb].new(erbf) { File.read(erbf) }
|
380
|
+
|
381
|
+
layout = File.read(layf) if File.exists?(layf) && !partial && use_layout
|
382
|
+
out = template.render(self, locals, &blk)
|
383
|
+
|
384
|
+
if layout
|
385
|
+
laytpl = Tilt[:erb].new(layf) { layout }
|
386
|
+
laytpl.render(self, locals) { out }
|
387
|
+
else
|
388
|
+
out
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
def partial_erb(name, opts={})
|
393
|
+
erb(name, {:partial => true}.merge(opts))
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
module AppHelpers
|
398
|
+
def halt
|
399
|
+
raise Halt.new(response)
|
400
|
+
end
|
401
|
+
|
402
|
+
def redirect(url)
|
403
|
+
response.status = 302
|
404
|
+
response['Location'] = url
|
405
|
+
halt
|
406
|
+
end
|
407
|
+
|
408
|
+
def session
|
409
|
+
request.env['rack.session'] ||= {}
|
410
|
+
end
|
411
|
+
|
412
|
+
def render_component(c)
|
413
|
+
c.processed_html
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
module FlashHelper
|
418
|
+
def flash
|
419
|
+
request.env['x-rack.flash']
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
class IndifferentHash < Hash
|
424
|
+
def self.from_hash(hash)
|
425
|
+
ih = self.new
|
426
|
+
hash.each do |k, v|
|
427
|
+
ih[k] = v
|
428
|
+
end
|
429
|
+
ih
|
430
|
+
end
|
431
|
+
|
432
|
+
def [](k)
|
433
|
+
if k.kind_of?(Symbol)
|
434
|
+
k_sym = k
|
435
|
+
k_str = k.to_s
|
436
|
+
raise 'Invalid hash' if self.include?(k_sym) && self.include?(k_str)
|
437
|
+
|
438
|
+
self.include?(k_str) ? self.fetch(k_str) : self.fetch(k_sym, nil)
|
439
|
+
else
|
440
|
+
super(k)
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
module HTMLHelpers
|
446
|
+
include Rack::Utils
|
447
|
+
alias_method :h, :escape_html
|
448
|
+
|
449
|
+
def jsesc(x); x.gsub('\\', '\\\\\\').gsub('"', '\"'); end
|
450
|
+
end
|
451
|
+
|
452
|
+
class App
|
453
|
+
class << self
|
454
|
+
|
455
|
+
MAX_ACTIVE_PAGES = 100
|
456
|
+
@@error_handler = nil
|
457
|
+
@@not_found_handler = nil
|
458
|
+
@@active_pages = {}
|
459
|
+
@@active_page_queue = Queue.new
|
460
|
+
@@before_handlers = []
|
461
|
+
|
462
|
+
def active_pages; @@active_pages; end
|
463
|
+
def active_page_queue; @@active_page_queue; end
|
464
|
+
|
465
|
+
def add_active_page(pg)
|
466
|
+
pgid = pg.id
|
467
|
+
active_page_queue << pg.id
|
468
|
+
self.active_pages[pg.id] = pg
|
469
|
+
end
|
470
|
+
|
471
|
+
def active_page_gc
|
472
|
+
deleted = 0
|
473
|
+
while active_page_queue.size > MAX_ACTIVE_PAGES
|
474
|
+
pgid = active_page_queue.pop
|
475
|
+
pg = active_pages[pgid]
|
476
|
+
pg.mark_dead!
|
477
|
+
NotificationCenter.delegate_gc
|
478
|
+
active_pages.delete(pgid)
|
479
|
+
Logger.log :gc, "Removing page #{pgid}"
|
480
|
+
deleted += 1
|
481
|
+
end
|
482
|
+
ObjectSpace.garbage_collect
|
483
|
+
Logger.log :gc, "Page GC ejected #{deleted} pages"
|
484
|
+
rescue Exception => e
|
485
|
+
Logger.error(:gc, e.inspect)
|
486
|
+
Logger.error(:gc, e.backtrace.join("\n"))
|
487
|
+
end
|
488
|
+
|
489
|
+
def req_handler
|
490
|
+
@@reqhandler ||= ReqHandler.new
|
491
|
+
end
|
492
|
+
|
493
|
+
def get(path, hook=nil, &proc)
|
494
|
+
hook = proc unless proc.nil?
|
495
|
+
req_handler.add_mapping(:method => :get, :path => path, :hook => hook)
|
496
|
+
end
|
497
|
+
|
498
|
+
def post(path, hook=nil, &proc)
|
499
|
+
hook = proc unless proc.nil?
|
500
|
+
req_handler.add_mapping(:method => :post, :path => path, :hook => hook)
|
501
|
+
end
|
502
|
+
|
503
|
+
def map(opts)
|
504
|
+
req_handler.add_mapping(opts)
|
505
|
+
end
|
506
|
+
|
507
|
+
def before_handlers; @@before_handlers; end
|
508
|
+
|
509
|
+
def before(&blk)
|
510
|
+
before_handlers << blk
|
511
|
+
end
|
512
|
+
|
513
|
+
def enable_notifications(opts={})
|
514
|
+
NotificationCenter.configure(opts)
|
515
|
+
end
|
516
|
+
|
517
|
+
def debug_pages
|
518
|
+
active_pages.each do |i, pg|
|
519
|
+
Logger.log :gc, [i, pg.dead?].inspect
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
def handle_channel(req)
|
524
|
+
id = req['_id']
|
525
|
+
pg = active_pages[id]
|
526
|
+
|
527
|
+
# active_page_gc # we do GC here since a slight delay in opening channel is better than in loading page
|
528
|
+
|
529
|
+
if pg
|
530
|
+
pg.mark_alive!
|
531
|
+
body = ChannelPusher.new(pg.channel)
|
532
|
+
|
533
|
+
EM.next_tick do
|
534
|
+
req.env['async.callback'].call([200, {}, body])
|
535
|
+
end
|
536
|
+
|
537
|
+
body.callback { pg.mark_dead! }
|
538
|
+
body.errback { pg.mark_dead! }
|
539
|
+
|
540
|
+
Logger.log :req, 'respond'
|
541
|
+
|
542
|
+
[-1, {}, []]
|
543
|
+
else
|
544
|
+
Logger.error(:req, "Couldn't find page #{id}")
|
545
|
+
|
546
|
+
respond_failed
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
def respond_success; [200, {}, ['ok']]; end
|
551
|
+
def respond_failed; [500, {}, ['failed']]; end
|
552
|
+
|
553
|
+
def with_component_from_req(req)
|
554
|
+
id = req['_id']
|
555
|
+
compid = req['_component']
|
556
|
+
|
557
|
+
pg = active_pages[id]
|
558
|
+
if pg
|
559
|
+
comp = pg.components.detect{|x| x.id == compid}
|
560
|
+
Logger.error :req, "Couldn't find component #{compid.inspect}" unless comp
|
561
|
+
yield comp if comp
|
562
|
+
else
|
563
|
+
Logger.error(:req, "Couldn't find page #{id}")
|
564
|
+
|
565
|
+
respond_failed
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
569
|
+
def handle_post(req)
|
570
|
+
with_component_from_req(req) do |comp|
|
571
|
+
comp.submitted(req)
|
572
|
+
respond_success
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
def handle_dispatch(req)
|
577
|
+
with_component_from_req(req) do |comp|
|
578
|
+
k = req['_dispatch'];
|
579
|
+
if comp.callback_registered?(k)
|
580
|
+
comp.run_callback(k)
|
581
|
+
respond_success
|
582
|
+
else
|
583
|
+
Logger.error(:req, "Couldn't find handler #{k.inspect}")
|
584
|
+
respond_failed
|
585
|
+
end
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
def error(&blk)
|
590
|
+
@@error_handler = blk
|
591
|
+
end
|
592
|
+
|
593
|
+
def error_handler; @@error_handler; end
|
594
|
+
|
595
|
+
def not_found(&blk)
|
596
|
+
@@not_found_handler = blk
|
597
|
+
end
|
598
|
+
|
599
|
+
def not_found_handler; @@not_found_handler; end
|
600
|
+
|
601
|
+
end # end class << self
|
602
|
+
|
603
|
+
def self.call(env)
|
604
|
+
# TODO better place to put this?
|
605
|
+
NotificationCenter.init # will do nothing if not enabled
|
606
|
+
|
607
|
+
self.req_handler.app_class = self
|
608
|
+
|
609
|
+
self.req_handler.handle_request(env)
|
610
|
+
end
|
611
|
+
|
612
|
+
include ERBHelper
|
613
|
+
include FlashHelper
|
614
|
+
include AppHelpers
|
615
|
+
|
616
|
+
def self.bind(hook)
|
617
|
+
mname = String.random
|
618
|
+
|
619
|
+
lambda do |app, *args|
|
620
|
+
kls = app.class
|
621
|
+
|
622
|
+
unless kls.instance_methods.include?(mname)
|
623
|
+
Logger.log :req, "defining #{mname} on #{kls}"
|
624
|
+
kls.send(:define_method, mname, hook)
|
625
|
+
hook = kls.instance_method(mname)
|
626
|
+
end
|
627
|
+
|
628
|
+
Logger.log :req, "running #{mname}"
|
629
|
+
app.respond_with_hook(mname, *args)
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
def initialize(req, meth=nil)
|
634
|
+
@req = req
|
635
|
+
@meth = meth
|
636
|
+
@response = Rack::Response.new
|
637
|
+
@params = IndifferentHash.from_hash(req.params)
|
638
|
+
end
|
639
|
+
|
640
|
+
def response; @response; end
|
641
|
+
def request; @req; end
|
642
|
+
def params; @params; end
|
643
|
+
|
644
|
+
def respond_with_hook(hook, *args)
|
645
|
+
response.body = self.send(hook, *args) || ''
|
646
|
+
response.finish
|
647
|
+
end
|
648
|
+
|
649
|
+
map :path => '/channel', :metal => true, :hook => lambda {|app| handle_channel(app.request)}
|
650
|
+
map :path => '/post', :metal => true, :hook => lambda {|app| handle_post(app.request) }
|
651
|
+
map :path => '/dispatch', :metal => true, :hook => lambda {|app| handle_dispatch(app.request) }
|
652
|
+
end
|
653
|
+
|
654
|
+
class Channel
|
655
|
+
# thin wrapper for EM::Channel
|
656
|
+
|
657
|
+
attr_reader :emchannel
|
658
|
+
|
659
|
+
def initialize
|
660
|
+
@emchannel = EM::Channel.new
|
661
|
+
end
|
662
|
+
|
663
|
+
def send_message(msg)
|
664
|
+
Logger.log :live, "Pushing #{msg.inspect}"
|
665
|
+
@emchannel.push(msg)
|
666
|
+
end
|
667
|
+
end
|
668
|
+
|
669
|
+
class Page
|
670
|
+
attr_reader :canvas, :request, :id, :canvas, :layout, :params, :channel
|
671
|
+
|
672
|
+
@live_updates = true
|
673
|
+
|
674
|
+
def self.disable_live_updates; @live_updates = false; end
|
675
|
+
def self.enable_live_updates; @live_updates = true; end
|
676
|
+
def self.live_updates_enabled?; @live_updates != false; end
|
677
|
+
|
678
|
+
include ERBHelper
|
679
|
+
include HTMLHelpers
|
680
|
+
include FlashHelper
|
681
|
+
|
682
|
+
alias_method :old_erb, :erb
|
683
|
+
|
684
|
+
def erb(name, opts={}, &blk)
|
685
|
+
old_erb(name, {:partial => false}.merge(opts), &blk)
|
686
|
+
end
|
687
|
+
|
688
|
+
def self.handle_request(app)
|
689
|
+
pg = self.new(app)
|
690
|
+
app.class.add_active_page(pg)
|
691
|
+
resp = pg.response
|
692
|
+
|
693
|
+
app.class.active_page_gc
|
694
|
+
|
695
|
+
action_hashes = 0
|
696
|
+
|
697
|
+
ObjectSpace.each_object do |obj|
|
698
|
+
if obj.kind_of?(Page) && obj.class != Class
|
699
|
+
puts "Page: #{obj.glimpse}"
|
700
|
+
elsif obj.kind_of?(Component) && obj.class != Class
|
701
|
+
puts "Component: #{obj.glimpse}"
|
702
|
+
elsif obj.kind_of?(ActionProc) && obj.class != Class
|
703
|
+
action_hashes += 1
|
704
|
+
end
|
705
|
+
end
|
706
|
+
|
707
|
+
puts "ActionHashes: #{action_hashes}"
|
708
|
+
|
709
|
+
resp
|
710
|
+
end
|
711
|
+
|
712
|
+
def initialize(app)
|
713
|
+
@request = app.request
|
714
|
+
@channel = Channel.new
|
715
|
+
@canvas = PageCanvas.new
|
716
|
+
@id = String.random(10, :alpha => true)
|
717
|
+
@params = IndifferentHash.from_hash(request.params)
|
718
|
+
end
|
719
|
+
|
720
|
+
def components; @canvas.components; end
|
721
|
+
|
722
|
+
def empty_response
|
723
|
+
Rack::Response.new
|
724
|
+
end
|
725
|
+
|
726
|
+
def halt
|
727
|
+
raise Halt.new(empty_response)
|
728
|
+
end
|
729
|
+
|
730
|
+
def dead?
|
731
|
+
components.first.dead?
|
732
|
+
end
|
733
|
+
|
734
|
+
def mark_dead!
|
735
|
+
components.each(&:mark_dead!)
|
736
|
+
end
|
737
|
+
|
738
|
+
def mark_alive!
|
739
|
+
components.each(&:mark_alive!)
|
740
|
+
end
|
741
|
+
|
742
|
+
def html
|
743
|
+
layout = self.layout
|
744
|
+
|
745
|
+
if @canvas.erb
|
746
|
+
body = self.erb(@canvas.erb, :layout => false)
|
747
|
+
style = @canvas.components.map(&:processed_css)
|
748
|
+
html = "<style>#{style}</style>#{body}"
|
749
|
+
|
750
|
+
render_to_layout(html, layout)
|
751
|
+
elsif !canvas.body.empty?
|
752
|
+
body = @canvas.body.map do |item|
|
753
|
+
if item.kind_of?(Component)
|
754
|
+
"<style>#{item.processed_css}</style>" +
|
755
|
+
item.processed_html
|
756
|
+
elsif item.kind_of?(String)
|
757
|
+
item
|
758
|
+
else
|
759
|
+
raise "Unknown body item: #{item.inspect}"
|
760
|
+
end
|
761
|
+
end
|
762
|
+
|
763
|
+
render_to_layout(body.join, layout)
|
764
|
+
else
|
765
|
+
raise "You can define an erb or append to the body, but not both"
|
766
|
+
end
|
767
|
+
end
|
768
|
+
|
769
|
+
def render_component(c)
|
770
|
+
canvas.components << c
|
771
|
+
c.processed_html
|
772
|
+
end
|
773
|
+
|
774
|
+
def response
|
775
|
+
Rack::Response.new(self.html, 200, {})
|
776
|
+
end
|
777
|
+
|
778
|
+
def render_to_layout(body, layout)
|
779
|
+
layout ||= File.join(File.dirname(__FILE__), 'views/plain_layout.erb')
|
780
|
+
|
781
|
+
erb(layout,
|
782
|
+
:locals => {:page => self},
|
783
|
+
# don't want a layout in our layout so we can layout while we layout
|
784
|
+
:layout => false) do
|
785
|
+
body
|
786
|
+
end
|
787
|
+
end
|
788
|
+
end
|
789
|
+
|
790
|
+
class PageCanvas
|
791
|
+
attr_accessor :body, :title, :components, :erb
|
792
|
+
|
793
|
+
def initialize
|
794
|
+
@body = []
|
795
|
+
@components = []
|
796
|
+
end
|
797
|
+
|
798
|
+
def components
|
799
|
+
if !body.empty?
|
800
|
+
@body.select{|c| c.kind_of?(Component)}.map(&:all_components).flatten
|
801
|
+
else
|
802
|
+
@components
|
803
|
+
end
|
804
|
+
end
|
805
|
+
end
|
806
|
+
|
807
|
+
class Component
|
808
|
+
attr_reader :id, :handlers, :page, :params
|
809
|
+
|
810
|
+
include HTMLHelpers # sure just shit it in with everything else
|
811
|
+
include ERBHelper
|
812
|
+
|
813
|
+
def initialize(page)
|
814
|
+
@id = String.random(10, :alpha => true)
|
815
|
+
@page = page
|
816
|
+
@callbacks = {}
|
817
|
+
@params = page.params if page
|
818
|
+
@dead = false
|
819
|
+
end
|
820
|
+
|
821
|
+
def halt; @page.halt; end
|
822
|
+
|
823
|
+
def channel; @page.channel; end
|
824
|
+
def id; @id; end
|
825
|
+
|
826
|
+
def render
|
827
|
+
page.channel.send_message(
|
828
|
+
:type => :update,
|
829
|
+
:id => id,
|
830
|
+
:html => processed_html
|
831
|
+
)
|
832
|
+
end
|
833
|
+
|
834
|
+
def page_error(msg)
|
835
|
+
page.channel.send_message(
|
836
|
+
:type => :alert,
|
837
|
+
:message => msg
|
838
|
+
)
|
839
|
+
end
|
840
|
+
|
841
|
+
def redirect(loc)
|
842
|
+
channel.send_message(
|
843
|
+
:type => :redirect,
|
844
|
+
:location => loc
|
845
|
+
)
|
846
|
+
end
|
847
|
+
|
848
|
+
def mark_dead!
|
849
|
+
@dead = true
|
850
|
+
end
|
851
|
+
|
852
|
+
def mark_alive!
|
853
|
+
@dead = false
|
854
|
+
end
|
855
|
+
|
856
|
+
def dead?; @dead; end
|
857
|
+
|
858
|
+
def submit_form(name)
|
859
|
+
"Goat.submitForm('#{name.prefix_ns(@id)}')"
|
860
|
+
end
|
861
|
+
|
862
|
+
def register_handler(target, selector)
|
863
|
+
register_callback { target.send(selector) }
|
864
|
+
end
|
865
|
+
|
866
|
+
def callback_registered?(k); @callbacks.include?(k); end
|
867
|
+
|
868
|
+
def run_callback(k)
|
869
|
+
# @callbacks[k].call
|
870
|
+
c = @callbacks[k].call
|
871
|
+
c[:target].send(c[:method], *c[:args])
|
872
|
+
end
|
873
|
+
|
874
|
+
def js
|
875
|
+
<<-EOJ
|
876
|
+
$(document).ready(function(){
|
877
|
+
Goat.registerComponent('#{id}')
|
878
|
+
})
|
879
|
+
EOJ
|
880
|
+
end
|
881
|
+
|
882
|
+
def component(body)
|
883
|
+
[:div, {:id => id},
|
884
|
+
[:script, js],
|
885
|
+
body
|
886
|
+
]
|
887
|
+
end
|
888
|
+
|
889
|
+
def html; make_me; end
|
890
|
+
def css; ''; end
|
891
|
+
|
892
|
+
def processed_html
|
893
|
+
HTMLBuilder.new(self, component(self.html)).html
|
894
|
+
end
|
895
|
+
|
896
|
+
def processed_css
|
897
|
+
css.gsub(/%(\w*)([\ \t\{])/) do |str|
|
898
|
+
m = $1
|
899
|
+
ws = $2
|
900
|
+
"#{@id}#{m.empty? ? '' : "_#{m}"}#{ws}"
|
901
|
+
end
|
902
|
+
end
|
903
|
+
|
904
|
+
def model_changed(item, notif)
|
905
|
+
render
|
906
|
+
end
|
907
|
+
|
908
|
+
def register_callback(callback)
|
909
|
+
# if @callbacks.values.include?(callback)
|
910
|
+
# @callbacks.to_a.detect{|k, v| k if v == callback}
|
911
|
+
# else
|
912
|
+
key = String.random
|
913
|
+
@callbacks[key] = callback
|
914
|
+
key
|
915
|
+
# end
|
916
|
+
end
|
917
|
+
|
918
|
+
def all_components
|
919
|
+
[self]
|
920
|
+
end
|
921
|
+
end
|
922
|
+
|
923
|
+
class Model
|
924
|
+
def initialize
|
925
|
+
@delegates = Set.new
|
926
|
+
end
|
927
|
+
|
928
|
+
def data_changed(notif=nil)
|
929
|
+
delegates.each {|d| d.model_changed(self, notif)}
|
930
|
+
end
|
931
|
+
|
932
|
+
def delegates; @delegates; end
|
933
|
+
|
934
|
+
def add_delegate(d)
|
935
|
+
@delegates << d
|
936
|
+
end
|
937
|
+
end
|
938
|
+
end
|