tofu 0.1.0

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/tofu.rb ADDED
@@ -0,0 +1,636 @@
1
+ require "tofu/version"
2
+
3
+ require 'erb'
4
+ require 'drb/drb'
5
+ require 'monitor'
6
+ require 'digest/md5'
7
+ require 'webrick'
8
+ require 'webrick/cgi'
9
+ require 'uri'
10
+
11
+ module Tofu
12
+ class Session
13
+ include MonitorMixin
14
+
15
+ def initialize(bartender, hint=nil)
16
+ super()
17
+ @session_id = Digest::MD5.hexdigest(Time.now.to_s + __id__.to_s)
18
+ @contents = {}
19
+ @hint = hint
20
+ renew
21
+ end
22
+ attr_reader :session_id
23
+ attr_accessor :hint
24
+
25
+ def service(context)
26
+ case context.req_method
27
+ when 'GET', 'POST', 'HEAD'
28
+ do_GET(context)
29
+ else
30
+ context.res_method_not_allowed
31
+ end
32
+ end
33
+
34
+ def expires
35
+ Time.now + 24 * 60 * 60
36
+ end
37
+
38
+ def hint_expires
39
+ Time.now + 60 * 24 * 60 * 60
40
+ end
41
+
42
+ def renew
43
+ @expires = expires
44
+ end
45
+
46
+ def expired?
47
+ @expires && Time.now > @expires
48
+ end
49
+
50
+ def do_GET(context)
51
+ dispatch_event(context)
52
+ tofu = lookup_view(context)
53
+ body = tofu ? tofu.to_html(context) : ''
54
+ context.res_header('content-type', 'text/html; charset=utf-8')
55
+ context.res_body(body)
56
+ end
57
+
58
+ def dispatch_event(context)
59
+ params = context.req_params
60
+ tofu_id ,= params['tofu_id']
61
+ tofu = fetch(tofu_id)
62
+ return unless tofu
63
+ tofu.send_request(context, context.req_params)
64
+ end
65
+
66
+ def lookup_view(context)
67
+ nil
68
+ end
69
+
70
+ def entry(tofu)
71
+ synchronize do
72
+ @contents[tofu.tofu_id] = tofu
73
+ end
74
+ end
75
+
76
+ def fetch(ref)
77
+ @contents[ref]
78
+ end
79
+ end
80
+
81
+ class SessionBar
82
+ include MonitorMixin
83
+
84
+ def initialize
85
+ super()
86
+ @pool = {}
87
+ @keeper = keeper
88
+ @interval = 60
89
+ end
90
+
91
+ def store(session)
92
+ key = session.session_id
93
+ synchronize do
94
+ @pool[key] = session
95
+ end
96
+ @keeper.wakeup
97
+ return key
98
+ end
99
+
100
+ def fetch(key)
101
+ return nil if key.nil?
102
+ synchronize do
103
+ session = @pool[key]
104
+ return nil unless session
105
+ if session.expired?
106
+ @pool.delete(key)
107
+ return nil
108
+ end
109
+ return session
110
+ end
111
+ end
112
+
113
+ private
114
+ def keeper
115
+ Thread.new do
116
+ loop do
117
+ synchronize do
118
+ @pool.delete_if do |k, v|
119
+ v.nil? || v.expired?
120
+ end
121
+ end
122
+ Thread.stop if @pool.size == 0
123
+ sleep @interval
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ class Bartender
130
+ def initialize(factory, name=nil)
131
+ @factory = factory
132
+ @prefix = name ? name : factory.to_s.split(':')[-1]
133
+ @bar = SessionBar.new
134
+ end
135
+ attr_reader :prefix
136
+
137
+ def service(context)
138
+ begin
139
+ session = retrieve_session(context)
140
+ catch(:tofu_done) { session.service(context) }
141
+ store_session(context, session)
142
+ ensure
143
+ end
144
+ end
145
+
146
+ private
147
+ def retrieve_session(context)
148
+ sid = context.req_cookie(@prefix + '_id')
149
+ session = @bar.fetch(sid) || make_session(context)
150
+ return session
151
+ end
152
+
153
+ def store_session(context, session)
154
+ sid = @bar.store(session)
155
+ context.res_add_cookie(@prefix + '_id', sid, session.expires)
156
+ hint = session.hint
157
+ if hint
158
+ context.res_add_cookie(@prefix +'_hint', hint, session.hint_expires)
159
+ end
160
+ session.renew
161
+ return sid
162
+ end
163
+
164
+ def make_session(context)
165
+ hint = context.req_cookie(@prefix + '_hint')
166
+ @factory.new(self, hint)
167
+ end
168
+ end
169
+
170
+ class ERBMethod
171
+ def initialize(method_name, fname, dir=nil)
172
+ @fname = build_fname(fname, dir)
173
+ @method_name = method_name
174
+ end
175
+
176
+ def reload(mod)
177
+ erb = File.open(@fname) {|f| ERB.new(f.read)}
178
+ erb.def_method(mod, @method_name, @fname)
179
+ end
180
+
181
+ private
182
+ def build_fname(fname, dir)
183
+ case dir
184
+ when String
185
+ ary = [dir]
186
+ when Array
187
+ ary = dir
188
+ else
189
+ ary = $:
190
+ end
191
+
192
+ found = fname # default
193
+ ary.each do |dir|
194
+ path = File::join(dir, fname)
195
+ if File::readable?(path)
196
+ found = path
197
+ break
198
+ end
199
+ end
200
+ found
201
+ end
202
+ end
203
+
204
+ class Tofu
205
+ include DRbUndumped
206
+ include ERB::Util
207
+
208
+ @erb_method = []
209
+ def self.add_erb(method_name, fname, dir=nil)
210
+ erb = ERBMethod.new(method_name, fname, dir)
211
+ @erb_method.push(erb)
212
+ end
213
+
214
+ def self.set_erb(fname, dir=nil)
215
+ @erb_method = [ERBMethod.new('to_html(context=nil)', fname, dir)]
216
+ reload_erb
217
+ end
218
+
219
+ def self.reload_erb1(erb)
220
+ erb.reload(self)
221
+ rescue SyntaxError
222
+ end
223
+
224
+ def self.reload_erb
225
+ @erb_method.each do |erb|
226
+ reload_erb1(erb)
227
+ end
228
+ end
229
+
230
+ def initialize(session)
231
+ @session = session
232
+ @session.entry(self)
233
+ @tofu_seq = nil
234
+ end
235
+ attr_reader :session
236
+
237
+ def tofu_class
238
+ self.class.to_s
239
+ end
240
+
241
+ def tofu_id
242
+ self.__id__.to_s
243
+ end
244
+
245
+ def to_div(context)
246
+ to_elem('div', context)
247
+ end
248
+
249
+ def to_span(context)
250
+ to_elem('span', context)
251
+ end
252
+
253
+ def to_elem(elem, context)
254
+ elem('elem', {'class'=>tofu_class, 'id'=>tofu_id}) {
255
+ begin
256
+ to_html(context)
257
+ rescue
258
+ "<p>error! #{h($!)}</p>"
259
+ end
260
+ }
261
+ end
262
+
263
+ def to_html(context); ''; end
264
+
265
+ def to_inner_html(context)
266
+ to_html(context)
267
+ end
268
+
269
+ def send_request(context, params)
270
+ cmd, = params['tofu_cmd']
271
+ msg = 'do_' + cmd.to_s
272
+
273
+ if @tofu_seq
274
+ seq, = params['tofu_seq']
275
+ unless @tofu_seq.to_s == seq
276
+ p [seq, @tofu_seq.to_s] if $DEBUG
277
+ return
278
+ end
279
+ end
280
+
281
+ if respond_to?(msg)
282
+ send(msg, context, params)
283
+ else
284
+ do_else(context, params)
285
+ end
286
+ ensure
287
+ @tofu_seq = @tofu_seq.succ if @tofu_seq
288
+ end
289
+
290
+ def do_else(context, params)
291
+ end
292
+
293
+ def action(context)
294
+ context.req_script_name.to_s + context.req_path_info.to_s
295
+ end
296
+
297
+ private
298
+ def attr(opt)
299
+ ary = opt.collect do |k, v|
300
+ if v
301
+ %Q!#{k}="#{h(v)}"!
302
+ else
303
+ nil
304
+ end
305
+ end.compact
306
+ return nil if ary.size == 0
307
+ ary.join(' ')
308
+ end
309
+
310
+ def elem(name, opt={})
311
+ head = ["#{name}", attr(opt)].compact.join(" ")
312
+ if block_given?
313
+ %Q!<#{head}>\n#{yield}\n</#{name}>!
314
+ else
315
+ %Q!<#{head} />!
316
+ end
317
+ end
318
+
319
+ def make_param(method_name, add_param={})
320
+ param = {
321
+ 'tofu_id' => tofu_id,
322
+ 'tofu_cmd' => method_name
323
+ }
324
+ param['tofu_seq'] = @tofu_seq if @tofu_seq
325
+ param.update(add_param)
326
+ return param
327
+ end
328
+
329
+ def form(method_name, context_or_param, context_or_empty=nil)
330
+ if context_or_empty.nil?
331
+ context = context_or_param
332
+ add_param = {}
333
+ else
334
+ context = context_or_empty
335
+ add_param = context_or_param
336
+ end
337
+ param = make_param(method_name, add_param)
338
+ hidden = input_hidden(param)
339
+ %Q!<form action="#{action(context)}" method="post">\n! + hidden
340
+ end
341
+
342
+ def href(method_name, add_param, context)
343
+ param = make_param(method_name, add_param)
344
+ ary = param.collect do |k, v|
345
+ "#{u(k)}=#{u(v)}"
346
+ end
347
+ %Q!href="#{action(context)}?#{ary.join(';')}"!
348
+ end
349
+
350
+ def input_hidden(param)
351
+ ary = param.collect do |k, v|
352
+ %Q!<input type="hidden" name="#{h(k)}" value="#{h(v)}" />\n!
353
+ end
354
+ ary.join('')
355
+ end
356
+
357
+ def make_anchor(method, param, context)
358
+ "<a #{href(method, param, context)}>"
359
+ end
360
+
361
+ def a(method, param, context)
362
+ make_anchor(method, param, context)
363
+ end
364
+ end
365
+
366
+ def reload_erb
367
+ ObjectSpace.each_object(Class) do |o|
368
+ if o.ancestors.include?(Tofu::Tofu)
369
+ o.reload_erb
370
+ end
371
+ end
372
+ end
373
+ module_function :reload_erb
374
+
375
+ class Context
376
+ def initialize(req, res)
377
+ @req = req
378
+ @res = res
379
+ end
380
+ attr_reader :req, :res
381
+
382
+ def done
383
+ throw(:tofu_done)
384
+ rescue NameError
385
+ nil
386
+ end
387
+
388
+ def service(bartender)
389
+ bartender.service(self)
390
+ nil
391
+ end
392
+
393
+ def req_params
394
+ hash = {}
395
+ @req.query.each do |k,v|
396
+ hash[k] = v.list
397
+ end
398
+ hash
399
+ end
400
+
401
+ def req_cookie(name)
402
+ found = @req.cookies.find {|c| c.name == name}
403
+ found ? found.value : nil
404
+ end
405
+
406
+ def res_add_cookie(name, value, expires=nil)
407
+ c = WEBrick::Cookie.new(name, value)
408
+ c.expires = expires if expires
409
+ @res.cookies.push(c)
410
+ end
411
+
412
+ def req_method
413
+ @req.request_method
414
+ end
415
+
416
+ def res_method_not_allowed
417
+ raise HTTPStatus::MethodNotAllowed, "unsupported method `#{req_method}'."
418
+ end
419
+
420
+ def req_path_info
421
+ @req.path_info
422
+ end
423
+
424
+ def req_script_name
425
+ @req.script_name
426
+ end
427
+
428
+ def req_absolute_path
429
+ (@req.request_uri + '/').to_s.chomp('/')
430
+ end
431
+
432
+ def res_body(v)
433
+ @res.body = v
434
+ end
435
+
436
+ def res_header(k, v)
437
+ if k.downcase == 'status'
438
+ @res.status = v.to_i
439
+ return
440
+ end
441
+ @res[k] = v
442
+ end
443
+ end
444
+
445
+ class Tofulet < WEBrick::HTTPServlet::AbstractServlet
446
+ def initialize(config, bartender, *options)
447
+ @bartender = bartender
448
+ super(config, *options)
449
+ @logger.debug("#{self.class}(initialize)")
450
+ end
451
+ attr_reader :logger, :config, :options, :bartender
452
+
453
+ def service(req, res)
454
+ Context.new(req, res).service(@bartender)
455
+ end
456
+ end
457
+
458
+ class CGITofulet < WEBrick::CGI
459
+ def initialize(bartender, *args)
460
+ @bartender = bartender
461
+ super(*args)
462
+ end
463
+
464
+ def service(req, res)
465
+ Context.new(req, res).service(@bartender)
466
+ end
467
+ end
468
+ end
469
+
470
+ module Tofu
471
+ class Tofu
472
+ def update_js
473
+ <<-"EOS"
474
+ function tofu_x_eval(tofu_id) {
475
+ var ary = document.getElementsByName(tofu_id + "tofu_x_eval");
476
+ for (var j = 0; j < ary.length; j++) {
477
+ var tofu_arg = ary[j];
478
+ for (var i = 0; i < tofu_arg.childNodes.length; i++) {
479
+ var node = tofu_arg.childNodes[i];
480
+ if (node.attributes.getNamedItem('name').nodeValue == 'tofu_x_eval') {
481
+ var script = node.attributes.getNamedItem('value').nodeValue;
482
+ try {
483
+ eval(script);
484
+ } catch(e) {
485
+ }
486
+ }
487
+ }
488
+ }
489
+ }
490
+
491
+ function tofu_x_update(tofu_id, url) {
492
+ var x;
493
+ try {
494
+ x = new ActiveXObject("Msxml2.XMLHTTP");
495
+ } catch (e) {
496
+ try {
497
+ x = new ActiveXObject("Microsoft.XMLHTTP");
498
+ } catch (e) {
499
+ x = null;
500
+ }
501
+ }
502
+ if (!x && typeof XMLHttpRequest != "undefined") {
503
+ x = new XMLHttpRequest();
504
+ }
505
+ if (x) {
506
+ x.onreadystatechange = function() {
507
+ if (x.readyState == 4 && x.status == 200) {
508
+ var tofu = document.getElementById(tofu_id);
509
+ tofu.innerHTML = x.responseText;
510
+ tofu_x_eval(tofu_id);
511
+ }
512
+ }
513
+ x.open("GET", url);
514
+ x.send(null);
515
+ }
516
+ }
517
+ EOS
518
+ end
519
+
520
+ def a_and_update(method_name, add_param, context, target=nil)
521
+ target ||= self
522
+ param = {
523
+ 'tofu_inner_id' => target.tofu_id
524
+ }
525
+ param.update(add_param)
526
+
527
+ param = make_param(method_name, param)
528
+ ary = param.collect do |k, v|
529
+ "#{u(k)}=#{u(v)}"
530
+ end
531
+ path = URI.parse(context.req_absolute_path)
532
+ url = path + %Q!#{action(context)}?#{ary.join(';')}!
533
+ %Q!tofu_x_update("#{target.tofu_id}", #{url.to_s.dump});!
534
+ end
535
+
536
+ def on_update_script(ary_or_script)
537
+ ary = if String === ary_or_script
538
+ [ary_or_script]
539
+ else
540
+ ary_or_script
541
+ end
542
+ str = %Q!<form name="#{tofu_id}tofu_x_eval">!
543
+ ary.each do |script|
544
+ str << %Q!<input type='hidden' name='tofu_x_eval' value="#{script.gsub('"', '&quot;')}" />!
545
+ end
546
+ str << '</form>'
547
+ str
548
+ end
549
+
550
+ def update_me(context)
551
+ a_and_update('else', {}, context)
552
+ end
553
+
554
+ def update_after(msec, context)
555
+ callback = update_me(context)
556
+ script = %Q!setTimeout(#{callback.dump}, #{msec})!
557
+ on_update_script(script)
558
+ end
559
+ end
560
+
561
+ class Session
562
+ def do_inner_html(context)
563
+ params = context.req_params
564
+ tofu_id ,= params['tofu_inner_id']
565
+ return false unless tofu_id
566
+
567
+ tofu = fetch(tofu_id)
568
+ body = tofu ? tofu.to_inner_html(context) : ''
569
+
570
+ context.res_header('content-type', 'text/html; charset=utf-8')
571
+ context.res_body(body)
572
+
573
+ throw(:tofu_done)
574
+ end
575
+ end
576
+ end
577
+
578
+ if __FILE__ == $0
579
+ require 'pp'
580
+
581
+ class EnterTofu < Tofu::Tofu
582
+ ERB.new(<<EOS).def_method(self, 'to_html(context)')
583
+ <%=form('enter', {}, context)%>
584
+ <dl>
585
+ <dt>hint</dt><dd><%=h @session.hint %><input class='enter' type='text' size='40' name='hint' value='<%=h @session.hint %>'/></dd>
586
+ <dt>volatile</dt><dd><%=h @session.text %><input class='enter' type='text' size='40' name='text' value='<%=h @session.text%>'/></dd>
587
+ </dl>
588
+ <input type='submit' />
589
+ </form>
590
+ EOS
591
+ def do_enter(context, params)
592
+ hint ,= params['hint']
593
+ @session.hint = hint || ''
594
+ text ,= params['text']
595
+ @session.text = text || ''
596
+ end
597
+ end
598
+
599
+ class BaseTofu < Tofu::Tofu
600
+ ERB.new(<<EOS).def_method(self, 'to_html(context)')
601
+ <html><title>base</title><body>
602
+ Hello, World.
603
+ <%= @enter.to_html(context) %>
604
+ <hr />
605
+ <pre><%=h context.pretty_inspect%></pre>
606
+ </body></html>
607
+ EOS
608
+ def initialize(session)
609
+ super(session)
610
+ @enter = EnterTofu.new(session)
611
+ end
612
+ end
613
+
614
+ class HelloSession < Tofu::Session
615
+ def initialize(bartender, hint=nil)
616
+ super
617
+ @base = BaseTofu.new(self)
618
+ @text = ''
619
+ end
620
+ attr_accessor :text
621
+
622
+ def lookup_view(context)
623
+ @base
624
+ end
625
+ end
626
+
627
+ tofu = Tofu::Bartender.new(HelloSession)
628
+ if false
629
+ DRb.start_service('druby://localhost:54322', Tofu::CGITofulet.new(tofu))
630
+ DRb.thread.join
631
+ else
632
+ s = WEBrick::HTTPServer.new(:Port => 8080)
633
+ s.mount("/", Tofu::Tofulet, tofu)
634
+ s.start
635
+ end
636
+ end
@@ -0,0 +1,53 @@
1
+ <html>
2
+ <head>
3
+ <script language="javascript">
4
+ <%= update_js %>
5
+ </script>
6
+ <title><%=h @session.headline %></title>
7
+ <style type="text/css" media="screen">
8
+ body {
9
+ font-family: Helvetica;
10
+ background: #FFFFFF;
11
+ color: #000000;
12
+ }
13
+
14
+ .EnterTofu * input {
15
+ box-sizing: border-box;
16
+ width: 100%;
17
+ padding: 16px 6px 6px 6px;
18
+ font-size: 16px;
19
+ font-weight: normal;
20
+ background: #6d84a2;
21
+ }
22
+
23
+ .ListTofu > ul {
24
+ margin: 0;
25
+ padding: 0;
26
+ }
27
+
28
+ .ListTofu > ul > li {
29
+ border-bottom: 1px solid #E0E0E0;
30
+ padding: 8px 0 8px 10px;
31
+ font-size: 14px;
32
+ font-weight: bold;
33
+ list-style: none:
34
+ }
35
+
36
+ .ListTofu > ul > li.group {
37
+ top: -1px;
38
+ margin-bottom: -2px;
39
+ border-top: 1px solid #7d7d7d;
40
+ border-bottom: 1px solid #999999;
41
+ padding: 1px 5px;
42
+ font-size: 13px;
43
+ font-weight: bold;
44
+ text-shadow: rgba(0, 0, 0, 0.4) 0 1px 0;
45
+ color: #FFFFFF;
46
+ }
47
+ </style>
48
+ </head>
49
+ <body onload='setTimeout(<%= @list.update_me(context).dump%>, <%= @session.interval %>)'>
50
+ <%= @enter.to_div(context)%>
51
+ <%= @list.to_div(context)%>
52
+ </body>
53
+ </html>
@@ -0,0 +1,3 @@
1
+ <%=form('enter', {}, context)%>
2
+ <input class='enter' type='text' size='40' name='str' value='' autocomplete='off' autofocus/>
3
+ </form>