goat 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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