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/lib/goat/extn.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'securerandom'
2
+
3
+ class Symbol
4
+ def to_proc
5
+ Proc.new { |*args| args.shift.__send__(self, *args) }
6
+ end
7
+ end
8
+
9
+ class Integer
10
+ def times_collect
11
+ (1..self).collect {|n| yield}
12
+ end
13
+ end
14
+
15
+ class String
16
+ unless self.instance_methods.include?('random')
17
+ def self.random(len=10, opts={})
18
+ opts = {:alpha => false}.merge(opts)
19
+ chars = ["a".."z", "A".."Z"]
20
+ chars << ("0".."9") unless opts[:alpha]
21
+
22
+ flat = chars.map(&:to_a).flatten
23
+ len.times_collect {flat[SecureRandom.random_number(flat.size)]}.join
24
+ end
25
+ end
26
+ end
data/lib/goat/goat.js ADDED
@@ -0,0 +1,172 @@
1
+ var Goat = {
2
+ channelOpenFails: 0,
3
+ activeChannel: null,
4
+ components: {},
5
+ page_id: null
6
+ }
7
+
8
+ function closure(target, fn) {
9
+ return function() {
10
+ return fn.apply(target, arguments);
11
+ }
12
+ }
13
+
14
+ $.extend(Goat, {
15
+ closure: function(fn) { return closure(this, fn); },
16
+
17
+ node: function(str) {
18
+ return $('#' + str);
19
+ },
20
+
21
+ createNode: function(m) {
22
+ var after = m['after'];
23
+ var html = m['html'];
24
+
25
+ if(after) {
26
+ var anode = this.node(after);
27
+ if(anode.size() == 0)
28
+ console.error("Couldn't find node " + after + " to insert after");
29
+ else
30
+ anode.after(html);
31
+ } else {
32
+ $(document).after(html);
33
+ }
34
+ },
35
+
36
+ removeNode: function(m) {
37
+ var id = m['id'];
38
+ var html = m['html'];
39
+ var rm = this.node(id);
40
+ if(rm.size() == 0)
41
+ console.error("Couldn't find node " + id + " to remove");
42
+ else
43
+ rm.remove();
44
+ },
45
+
46
+ showAlert: function(m) {
47
+ alert_msg = m['message'];
48
+ alert(alert_msg);
49
+ },
50
+
51
+ messageReceived: function(m) {
52
+ var t = m['type'];
53
+ if(t == 'update') {
54
+ var id = m['id'];
55
+ var html = m['html'];
56
+ this.node(id).html(html);
57
+ } else if(t == 'redirect') {
58
+ console.log("Redirecting to " + m['location']);
59
+ sleep(2);
60
+ window.location = m['location'];
61
+ } else if (t == 'create') {
62
+ this.createNode(m);
63
+ } else if (t == 'remove') {
64
+ this.removeNode(m);
65
+ } else if(t == 'alert') {
66
+ this.showAlert(m);
67
+ } else {
68
+ console.log("Unknown message type: " + m);
69
+ }
70
+ },
71
+
72
+ channelDataReceived: function(data) {
73
+ if(data == "") {
74
+ this.reopenChannel();
75
+ return;
76
+ }
77
+
78
+ // console.warn("channel data: " + data);
79
+ data = $.evalJSON(data);
80
+
81
+ this.channelOpenFails = 0;
82
+
83
+ if(this.messageReceivedDelegate)
84
+ this.messageReceivedDelegate(data);
85
+
86
+ if(data['messages']) {
87
+ $(data['messages']).each(this.closure(function(i, m) { this.messageReceived(m) }));
88
+ this.openChannel();
89
+ } else {
90
+ console.error("Bad channel data: " + data)
91
+ }
92
+ },
93
+
94
+ reopenChannel: function() {
95
+ console.error("reopenChannel()");
96
+ var fails = this.channelOpenFails;
97
+ setTimeout(function() { Goat.openChannel(); }, (fails > 20) ? 20000 : (fails * 500));
98
+ this.channelOpenFails++;
99
+ },
100
+
101
+ openChannel: function() {
102
+ console.log("opening channel");
103
+
104
+ if(this.activeChannel) {
105
+ console.error("can't open channel: channel already open");
106
+ return;
107
+ }
108
+
109
+ this.activeChannel = $.ajax({
110
+ url: '/channel',
111
+ async: true,
112
+ data: {_id: Goat.page_id},
113
+ cache: false,
114
+ timeout: 1000000,
115
+ success: this.closure(function(data) {
116
+ this.activeChannel = null;
117
+ this.channelDataReceived(data);
118
+ }),
119
+ error: this.closure(function(req, status, err) {
120
+ this.activeChannel = null;
121
+ console.error("ajax channel error");
122
+ console.error({req: req, status: status, err: err});
123
+ this.reopenChannel();
124
+ })
125
+ });
126
+ },
127
+
128
+ submitForm: function(id) {
129
+ var data = {};
130
+ var appendValue = function(i, input){ data[input.name] = $(input).val(); };
131
+ $("#" + id + " input").each(appendValue);
132
+ $("#" + id + " textarea").each(appendValue);
133
+ data['_id'] = Goat.page_id;
134
+ $.ajax({
135
+ url: '/post',
136
+ async: true,
137
+ data: data,
138
+ cache: false,
139
+ success: function(data) {},
140
+ error: function(req, stat, err) {
141
+ console.log("submit form error");
142
+ console.log({req: req, status: stat, err: err});
143
+ }
144
+ });
145
+
146
+ return false;
147
+ },
148
+
149
+ remoteDispatch: function(c, k) {
150
+ $.ajax({
151
+ url: '/dispatch',
152
+ async: true,
153
+ data: {_id: Goat.page_id, _component: c, _dispatch: k},
154
+ success: function(data) {},
155
+ error: function(req, stat, err) {
156
+ console.log("remote dispatch error");
157
+ console.log({req: req, status: stat, err: err});
158
+ }
159
+ });
160
+
161
+ return false;
162
+ },
163
+
164
+ registerComponent: function(id) {
165
+ this.components[id] = id;
166
+ }
167
+ });
168
+
169
+ $(document).ready(function() {
170
+ setTimeout(function() { if(Goat.page_id) Goat.openChannel(); }, 1);
171
+ });
172
+
data/lib/goat/html.rb ADDED
@@ -0,0 +1,147 @@
1
+ module Goat
2
+ class HTMLBuilder
3
+ class ::String
4
+ def to_html(builder, comp)
5
+ builder.string_to_html(self, comp)
6
+ end
7
+ end
8
+
9
+ class ::Array
10
+ def to_html(builder, comp)
11
+ raise InvalidBodyError.new(self) if self.include?(nil)
12
+ builder.array_to_html(self, comp)
13
+ end
14
+ end
15
+
16
+ class InvalidBodyError < RuntimeError
17
+ attr_reader :body
18
+
19
+ def initialize(body)
20
+ super("Invalid body: #{body.inspect}")
21
+ @body = body
22
+ end
23
+ end
24
+
25
+ class TagBuilder
26
+ # TODO: gmail trick of only a single onclick() handler
27
+
28
+ def self.build(tag, attrs, body, comp)
29
+ self.new(tag, attrs, body, comp).dispatch
30
+ end
31
+
32
+ def initialize(tag, attrs, body, comp)
33
+ @tag = tag
34
+ @attrs = attrs
35
+ @body = body
36
+ @comp = comp
37
+
38
+ rewrite_attrs
39
+ end
40
+
41
+ def rewrite_attrs
42
+ new = {}
43
+
44
+ @attrs.map_to_hash do |k, v|
45
+ if k == :onclick && v.kind_of?(Hash)
46
+ raise "Invalid onclick" unless v.include?(:target) && v.include?(:selector)
47
+ target = v[:target]
48
+ sel = v[:selector]
49
+
50
+ key = @comp.register_handler(target, sel)
51
+ new[k] = "Goat.remoteDispatch('#{@comp.id}', '#{key}')"
52
+ new[:href] = "#"
53
+ elsif k == :class && v.kind_of?(Array)
54
+ new[:class] = @attrs[:class].join(' ')
55
+ else
56
+ new[k] = v
57
+ end
58
+ end
59
+
60
+ @attrs = new
61
+ end
62
+
63
+ def form_tag
64
+ target = @attrs.delete(:target)
65
+ [:form, @attrs, @body + [[:input, {:type => 'hidden', :name => '_component', :value => @comp.id}]]]
66
+ end
67
+
68
+ def a_tag
69
+ if action = @attrs.delete(:action)
70
+ # [:a, {}, 'dead']
71
+ # key = @comp.register_callback(action)
72
+ # @attrs[:onclick] = "Goat.remoteDispatch('#{@comp.id}', '#{key}')"
73
+ # @attrs[:href] = '#'
74
+ # [:a, @attrs, @body]
75
+ action[:target] ||= @comp
76
+ action[:args] ||= []
77
+ key = @comp.register_callback(ActionProc.new { action })
78
+ @attrs[:onclick] = "return Goat.remoteDispatch('#{@comp.id}', '#{key}')"
79
+ @attrs[:href] = '#'
80
+ [:a, @attrs, @body]
81
+ else
82
+ identity
83
+ end
84
+ end
85
+
86
+ def identity
87
+ [@tag, @attrs, @body]
88
+ end
89
+
90
+ def dispatch
91
+ meth = "#{@tag}_tag".to_sym
92
+
93
+ if self.respond_to?(meth)
94
+ self.send(meth)
95
+ else
96
+ identity
97
+ end
98
+ end
99
+ end
100
+
101
+ def standalone_tags
102
+ %w{br img input}
103
+ end
104
+
105
+ def inject_prefix(attrs, id)
106
+ attrs.map_to_hash {|k, v| [k, v.prefix_ns(id)]}
107
+ end
108
+
109
+ def attrs_to_html(attrs)
110
+ attrs.map {|k, v| "#{k}=\"#{v}\""}.join(' ')
111
+ end
112
+
113
+ def string_to_html(str, comp)
114
+ str
115
+ end
116
+
117
+ def array_to_html(ar, comp)
118
+ if ar.first.kind_of?(Symbol)
119
+ tag = ar[0]
120
+ have_attrs = ar[1].is_a?(Hash)
121
+ attrs = have_attrs ? ar[1] : {}
122
+ body = ar[(have_attrs ? 2 : 1)..-1]
123
+
124
+ tag, attrs, body = TagBuilder.build(tag, attrs, body, comp)
125
+ attrs = inject_prefix(attrs, comp.id)
126
+ lonely = standalone_tags.include?(tag.to_s)
127
+
128
+ open_tag = "<#{tag}#{attrs.empty? ? '' : (' ' + attrs_to_html(attrs))}>"
129
+ body_html = body.empty? ? '' : body.map{|x| x.to_html(self, comp)}.join
130
+ close_tag = (lonely ? '' : "</#{tag}>")
131
+
132
+ "#{open_tag}#{body_html}#{close_tag}"
133
+ else
134
+ ar.map{|x| x.to_html(self, comp)}.join
135
+ end
136
+ end
137
+
138
+ def initialize(component, input)
139
+ @input = input
140
+ @component = component
141
+ end
142
+
143
+ def html
144
+ @input.to_html(self, @component)
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,39 @@
1
+ require 'term/ansicolor'
2
+
3
+ module Goat
4
+ class Logger
5
+ class << self
6
+ def init
7
+ @levels = [:error]
8
+ @categories = nil
9
+ end
10
+
11
+ def levels=(types)
12
+ @levels = types
13
+ end
14
+
15
+ def categories=(cats)
16
+ @categories = cats
17
+ end
18
+
19
+ def error(cat, str)
20
+ log(cat, str, :error)
21
+ end
22
+
23
+ def log(cat, str, level = :debug)
24
+ if @categories.kind_of?(Array) && level != :error
25
+ return unless @categories.include?(cat)
26
+ end
27
+
28
+ return unless @levels.include?(level)
29
+
30
+ levelstr = level.to_s.ljust(13)
31
+ colorlevel = level == :error ? Term::ANSIColor.red(levelstr) : levelstr
32
+
33
+ $stderr.puts "#{colorlevel} [#{Time.now.strftime("%d/%b/%Y %H:%M:%S")}] #{Term::ANSIColor.green(str)}"
34
+ end
35
+ end
36
+
37
+ init
38
+ end
39
+ end
data/lib/goat/mongo.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'mongo'
2
+
3
+ module Mongo
4
+ class Collection
5
+ alias_method :old_save, :save
6
+ alias_method :old_update, :update
7
+
8
+ def notify_update_or_save(more_info)
9
+ notif = more_info.merge(
10
+ 'type' => 'mongo_change',
11
+ 'db' => self.db.name,
12
+ 'collection' => self.name
13
+ )
14
+
15
+ Goat::NotificationCenter.notify(notif)
16
+ end
17
+
18
+ def save(doc, opts={})
19
+ id = old_save(doc, opts)
20
+ notify_update_or_save('_id' => id)
21
+ end
22
+
23
+ def update(sel, doc, opts={})
24
+ old_update(sel, doc, opts)
25
+ notify_update_or_save(sel)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,121 @@
1
+ require 'eventmachine'
2
+
3
+ require File.join(File.dirname(__FILE__), 'logger')
4
+
5
+ module Goat
6
+ module NotificationCenter
7
+ class Receiver < EM::Connection
8
+ include EM::P::LineText2
9
+
10
+ def self.start(host, port)
11
+ EM.connect(host, port, self)
12
+ rescue RuntimeError => e
13
+ Logger.error :live, "Couldn't connect to notification server at #{host}:#{port}"
14
+ raise e
15
+ end
16
+
17
+ def receive_line(line)
18
+ NotificationCenter.receive(line)
19
+ end
20
+
21
+ def unbind
22
+ Logger.error :live, "Lost connection"
23
+ EM.add_timer(1) do
24
+ NotificationCenter.start_receiver
25
+ end
26
+ end
27
+ end
28
+
29
+ def self.init
30
+ unless @running
31
+ start if @configured
32
+ end
33
+ end
34
+
35
+ def self.start_receiver
36
+ Receiver.start(@host, @recv_port)
37
+ end
38
+
39
+ def self.start
40
+ start_receiver
41
+ @running = true
42
+ end
43
+
44
+ def self.configure(opts={})
45
+ opts = {:host => '127.0.0.1', :recv_port => 8000, :send_port => 8001}.merge(opts)
46
+ @configured = true
47
+ @host = opts[:host]
48
+ @recv_port = opts[:recv_port]
49
+ @send_port = opts[:send_port]
50
+
51
+ @subscribers = Set.new
52
+ end
53
+
54
+ def self.wants_notification(sub, notif)
55
+ wants = true
56
+ sub[:sig].each do |k, v|
57
+ wants = false unless notif[k] == v
58
+ end
59
+ wants
60
+ end
61
+
62
+ def self.load_notif(notif)
63
+ JSON.load(notif)
64
+ end
65
+
66
+ def self.notif_to_json(notif)
67
+ notif.to_json
68
+ end
69
+
70
+ def self.delegate_gc
71
+ kill_list = Set.new
72
+ Logger.log :gc, "Delegate gc"
73
+ @subscribers.each do |sub|
74
+ # $stderr.puts "Testing removal of #{sub.inspect}"
75
+ dlg = sub[:delegate]
76
+ # $stderr.puts "Responds to dead? #{dlg.respond_to?(:dead?)}"
77
+ if dlg.respond_to?(:dead?) && dlg.dead?
78
+ Logger.log :gc, "Dead"
79
+ kill_list << sub
80
+ next
81
+ end
82
+ end
83
+ # $stderr.puts "Removing #{kill_list.inspect}"
84
+ kill_list.each {|sub| @subscribers.delete(sub)}
85
+ # $stderr.puts "subscribers now: #{@subscribers.map{|x| x.inspect[0..50]}.inspect}"
86
+ end
87
+
88
+ def self.receive(line)
89
+ notif = load_notif(line)
90
+ Logger.log :live, "received notif #{notif.inspect}" if $verbose
91
+
92
+ delegate_gc
93
+
94
+ @subscribers.each do |sub|
95
+ blk = sub[:block]
96
+ if wants_notification(sub, notif)
97
+ blk.arity == 1 ? blk.call(notif) : blk.call
98
+ end
99
+ end
100
+ end
101
+
102
+ def self.notify(notif)
103
+ if EM.reactor_running?
104
+ EM.connect(@host, @send_port) do |c|
105
+ # TODO: alert if this fails
106
+ c.send_data(notif_to_json(notif) + "\n")
107
+ # TODO: 2 secs right here?
108
+ EM.add_timer(2) { c.close_connection }
109
+ end
110
+ else
111
+ s = TCPSocket.open(@host, @send_port)
112
+ s.write(notif_to_json(notif) + "\n")
113
+ s.close
114
+ end
115
+ end
116
+
117
+ def self.subscribe(obj, sig, &blk)
118
+ @subscribers << {:sig => sig, :block => blk, :delegate => obj}
119
+ end
120
+ end
121
+ end