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 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