mullen-wee 2.2.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/README.rdoc +127 -0
- data/Rakefile +25 -0
- data/aWee.gemspec +26 -0
- data/examples/ObjectSpaceBrowser.rb +191 -0
- data/examples/ajax.rb +73 -0
- data/examples/apotomo-webhunter/main.rb +75 -0
- data/examples/apotomo-webhunter/public/images/bear_trap_charged.png +0 -0
- data/examples/apotomo-webhunter/public/images/bear_trap_snapped.png +0 -0
- data/examples/apotomo-webhunter/public/images/cheese.png +0 -0
- data/examples/apotomo-webhunter/public/images/dark_forest.jpg +0 -0
- data/examples/apotomo-webhunter/public/images/mouse.png +0 -0
- data/examples/apotomo-webhunter/public/javascripts/jquery-1.3.2.min.js +19 -0
- data/examples/apotomo-webhunter/public/javascripts/wee-jquery.js +19 -0
- data/examples/apotomo-webhunter/public/stylesheets/forest.css +33 -0
- data/examples/arc_challenge.rb +42 -0
- data/examples/arc_challenge2.rb +46 -0
- data/examples/cheese_task.rb +27 -0
- data/examples/continuations.rb +28 -0
- data/examples/demo.rb +135 -0
- data/examples/demo/calculator.rb +63 -0
- data/examples/demo/calendar.rb +333 -0
- data/examples/demo/counter.rb +38 -0
- data/examples/demo/editable_counter.rb +36 -0
- data/examples/demo/example.rb +142 -0
- data/examples/demo/file_upload.rb +19 -0
- data/examples/demo/messagebox.rb +15 -0
- data/examples/demo/radio.rb +33 -0
- data/examples/demo/window.rb +71 -0
- data/examples/hw.rb +11 -0
- data/examples/i18n/app.rb +16 -0
- data/examples/i18n/locale/de/app.po +25 -0
- data/examples/i18n/locale/en/app.po +25 -0
- data/examples/pager.rb +102 -0
- data/lib/wee.rb +109 -0
- data/lib/wee/application.rb +89 -0
- data/lib/wee/callback.rb +109 -0
- data/lib/wee/component.rb +363 -0
- data/lib/wee/decoration.rb +251 -0
- data/lib/wee/dialog.rb +171 -0
- data/lib/wee/external_resource.rb +39 -0
- data/lib/wee/html_brushes.rb +795 -0
- data/lib/wee/html_canvas.rb +254 -0
- data/lib/wee/html_document.rb +52 -0
- data/lib/wee/html_writer.rb +71 -0
- data/lib/wee/id_generator.rb +81 -0
- data/lib/wee/jquery.rb +11 -0
- data/lib/wee/jquery/jquery-1.3.2.min.js +19 -0
- data/lib/wee/jquery/wee-jquery.js +19 -0
- data/lib/wee/locale.rb +56 -0
- data/lib/wee/lru_cache.rb +91 -0
- data/lib/wee/presenter.rb +44 -0
- data/lib/wee/renderer.rb +72 -0
- data/lib/wee/request.rb +56 -0
- data/lib/wee/response.rb +68 -0
- data/lib/wee/rightjs.rb +11 -0
- data/lib/wee/rightjs/rightjs-1.5.2.min.js +9 -0
- data/lib/wee/rightjs/wee-rightjs.js +18 -0
- data/lib/wee/root_component.rb +45 -0
- data/lib/wee/session.rb +366 -0
- data/lib/wee/state.rb +102 -0
- data/lib/wee/task.rb +16 -0
- data/test/bm_render.rb +34 -0
- data/test/component_spec.rb +40 -0
- data/test/stress/plotter.rb +84 -0
- data/test/stress/stress_client.rb +51 -0
- data/test/stress/stress_local.rb +86 -0
- data/test/stress/stress_server.rb +83 -0
- data/test/test_component.rb +106 -0
- data/test/test_html_canvas.rb +25 -0
- data/test/test_html_writer.rb +32 -0
- data/test/test_lru_cache.rb +51 -0
- data/test/test_request.rb +42 -0
- metadata +185 -0
data/lib/wee.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
module Wee
|
2
|
+
Version = "2.2.0"
|
3
|
+
end
|
4
|
+
|
5
|
+
require 'rack'
|
6
|
+
|
7
|
+
require 'wee/state'
|
8
|
+
require 'wee/callback'
|
9
|
+
|
10
|
+
require 'wee/presenter'
|
11
|
+
require 'wee/decoration'
|
12
|
+
require 'wee/component'
|
13
|
+
require 'wee/root_component'
|
14
|
+
require 'wee/task'
|
15
|
+
require 'wee/dialog'
|
16
|
+
|
17
|
+
require 'wee/application'
|
18
|
+
require 'wee/request'
|
19
|
+
require 'wee/response'
|
20
|
+
require 'wee/session'
|
21
|
+
|
22
|
+
require 'wee/html_document'
|
23
|
+
require 'wee/html_brushes'
|
24
|
+
require 'wee/html_canvas'
|
25
|
+
|
26
|
+
if RUBY_VERSION >= "1.9"
|
27
|
+
begin
|
28
|
+
require 'continuation'
|
29
|
+
rescue LoadError
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Wee::HelloWorld < Wee::RootComponent
|
34
|
+
def render(r)
|
35
|
+
r.text "Hello World from Wee!"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def Wee.run(component_class=nil, params=nil, &block)
|
40
|
+
raise ArgumentError if component_class and block
|
41
|
+
|
42
|
+
params ||= Hash.new
|
43
|
+
params[:mount_path] ||= '/'
|
44
|
+
params[:port] ||= 2000
|
45
|
+
params[:public_path] ||= nil
|
46
|
+
params[:additional_builder_procs] ||= []
|
47
|
+
params[:use_continuations] ||= true
|
48
|
+
params[:print_message] ||= false
|
49
|
+
params[:autoreload] ||= false
|
50
|
+
|
51
|
+
if component_class <= Wee::RootComponent
|
52
|
+
component_class.external_resources.each do |ext_res|
|
53
|
+
params[:additional_builder_procs] << proc {|builder| ext_res.install(builder)}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
raise ArgumentError if params[:use_continuations] and block
|
58
|
+
|
59
|
+
unless block
|
60
|
+
block ||= if params[:use_continuations]
|
61
|
+
proc { Wee::Session.new(component_class.instanciate,
|
62
|
+
Wee::Session::ThreadSerializer.new) }
|
63
|
+
else
|
64
|
+
proc { Wee::Session.new(component_class.instanciate) }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
app = Rack::Builder.app do
|
69
|
+
map params[:mount_path] do
|
70
|
+
a = Wee::Application.new(&block)
|
71
|
+
|
72
|
+
if params[:auth_md5]
|
73
|
+
a = Rack::Auth::Digest::MD5.new(a, ¶ms[:auth_md5])
|
74
|
+
a.realm = params[:auth_realm] || 'Wee App'
|
75
|
+
a.opaque = params[:auth_md5_opaque] || Wee::IdGenerator::Secure.new.next
|
76
|
+
end
|
77
|
+
|
78
|
+
if params[:auth_basic]
|
79
|
+
a = Rack::Auth::Basic.new(a, params[:auth_realm] || 'Wee App', ¶ms[:auth_basic])
|
80
|
+
end
|
81
|
+
|
82
|
+
if params[:autoreload]
|
83
|
+
if params[:autoreload].kind_of?(Integer)
|
84
|
+
timer = Integer(params[:autoreload])
|
85
|
+
else
|
86
|
+
timer = 0
|
87
|
+
end
|
88
|
+
use Rack::Reloader, timer
|
89
|
+
end
|
90
|
+
|
91
|
+
if params[:public_path]
|
92
|
+
run Rack::Cascade.new([Rack::File.new(params[:public_path]), a])
|
93
|
+
else
|
94
|
+
run a
|
95
|
+
end
|
96
|
+
end
|
97
|
+
params[:additional_builder_procs].each {|bproc| bproc.call(self)}
|
98
|
+
end
|
99
|
+
|
100
|
+
if params[:print_message]
|
101
|
+
url = "http://localhost:#{params[:port]}#{params[:mount_path]}"
|
102
|
+
io = params[:print_message].kind_of?(IO) ? params[:print_message] : STDERR
|
103
|
+
io.puts
|
104
|
+
io.puts "Open your browser at: #{url}"
|
105
|
+
io.puts
|
106
|
+
end
|
107
|
+
|
108
|
+
Rack::Handler::WEBrick.run(app, :Port => params[:port])
|
109
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'wee/id_generator'
|
3
|
+
require 'wee/lru_cache'
|
4
|
+
|
5
|
+
module Wee
|
6
|
+
|
7
|
+
#
|
8
|
+
# A Wee::Application manages all Session's of a single application. It
|
9
|
+
# dispatches the request to the correct handler by examining the request.
|
10
|
+
#
|
11
|
+
class Application
|
12
|
+
|
13
|
+
def self.for(component_class, session_class=Wee::Session, *component_args)
|
14
|
+
new { session_class.new(component_class.new(*component_args)) }
|
15
|
+
end
|
16
|
+
|
17
|
+
class SessionCache < Wee::LRUCache
|
18
|
+
def garbage_collect
|
19
|
+
delete_if {|id, session| session.dead? }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Creates a new application. The block, when called, must
|
25
|
+
# return a new Session instance.
|
26
|
+
#
|
27
|
+
# Wee::Application.new { Wee::Session.new(root_component) }
|
28
|
+
#
|
29
|
+
def initialize(max_sessions=10_000, &block)
|
30
|
+
@session_factory = block || raise(ArgumentError)
|
31
|
+
@session_ids ||= Wee::IdGenerator::Secure.new
|
32
|
+
@sessions = SessionCache.new(max_sessions)
|
33
|
+
@mutex = Mutex.new
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Garbage collect dead sessions
|
38
|
+
#
|
39
|
+
def cleanup_sessions
|
40
|
+
@mutex.synchronize { @sessions.garbage_collect }
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Handles a web request
|
45
|
+
#
|
46
|
+
def call(env)
|
47
|
+
request = Wee::Request.new(env)
|
48
|
+
|
49
|
+
if request.session_id
|
50
|
+
session = @mutex.synchronize { @sessions.fetch(request.session_id) }
|
51
|
+
if session and session.alive?
|
52
|
+
session.call(env)
|
53
|
+
else
|
54
|
+
url = request.build_url(:session_id => nil, :page_id => nil)
|
55
|
+
Wee::RefreshResponse.new("Invalid or expired session", url).finish
|
56
|
+
end
|
57
|
+
else
|
58
|
+
session = new_session()
|
59
|
+
url = request.build_url(:session_id => session.id, :page_id => nil)
|
60
|
+
Wee::RedirectResponse.new(url).finish
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
|
66
|
+
def new_session
|
67
|
+
session = @session_factory.call
|
68
|
+
session.application = self
|
69
|
+
insert_session(session)
|
70
|
+
return session
|
71
|
+
end
|
72
|
+
|
73
|
+
def insert_session(session, retries=3)
|
74
|
+
retries.times do
|
75
|
+
@mutex.synchronize {
|
76
|
+
id = @session_ids.next
|
77
|
+
if not @sessions.has_key?(id)
|
78
|
+
@sessions.store(id, session)
|
79
|
+
session.id = id
|
80
|
+
return
|
81
|
+
end
|
82
|
+
}
|
83
|
+
end
|
84
|
+
raise
|
85
|
+
end
|
86
|
+
|
87
|
+
end # class Application
|
88
|
+
|
89
|
+
end # module Wee
|
data/lib/wee/callback.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
module Wee
|
2
|
+
|
3
|
+
class CallbackRegistry
|
4
|
+
def initialize(prefix="")
|
5
|
+
@prefix = prefix
|
6
|
+
@next_id = 0
|
7
|
+
@callbacks = {} # {callback_id1 => callback1, callback_id2 => callback2}
|
8
|
+
@triggered = nil
|
9
|
+
@obj_map = {} # obj => [callback_id1, callback_id2, ...]
|
10
|
+
end
|
11
|
+
|
12
|
+
def empty?
|
13
|
+
@callbacks.empty?
|
14
|
+
end
|
15
|
+
|
16
|
+
def register(object, callback)
|
17
|
+
id = @next_id
|
18
|
+
@next_id += 1
|
19
|
+
@callbacks[id] = callback
|
20
|
+
(@obj_map[object] ||= []) << id
|
21
|
+
return "#{@prefix}#{id}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def unregister(object)
|
25
|
+
if arr = @obj_map.delete(object)
|
26
|
+
arr.each {|id| @callbacks.delete(id) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# NOTE that if fields named "xxx" and "xxx.yyy" occur, the value of
|
32
|
+
# @fields['xxx'] is { nil => ..., 'yyy' => ... }. This is required
|
33
|
+
# to make image buttons work correctly.
|
34
|
+
#
|
35
|
+
def prepare_triggered(ids_and_values)
|
36
|
+
@triggered = {}
|
37
|
+
ids_and_values.each do |id, value|
|
38
|
+
if id =~ /^#{@prefix}(\d+)([.](.*))?$/
|
39
|
+
id, suffix = Integer($1), $3
|
40
|
+
next unless @callbacks[id]
|
41
|
+
|
42
|
+
if @triggered[id].kind_of?(Hash)
|
43
|
+
@triggered[id][suffix] = value
|
44
|
+
elsif suffix
|
45
|
+
@triggered[id] = {nil => @triggered[id], suffix => value}
|
46
|
+
else
|
47
|
+
@triggered[id] = value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def reset_triggered
|
54
|
+
@triggered = nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def each_triggered(object)
|
58
|
+
if ary = @obj_map[object]
|
59
|
+
for id in ary
|
60
|
+
yield @callbacks[id], @triggered[id] if @triggered.has_key?(id)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def each_triggered_call_with_value(object)
|
66
|
+
if ary = @obj_map[object]
|
67
|
+
for id in ary
|
68
|
+
@callbacks[id].call(@triggered[id]) if @triggered.has_key?(id)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def first_triggered(object)
|
74
|
+
if ary = @obj_map[object]
|
75
|
+
for id in ary
|
76
|
+
return @callbacks[id] if @triggered.has_key?(id)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
return nil
|
80
|
+
end
|
81
|
+
|
82
|
+
end # class CallbackRegistry
|
83
|
+
|
84
|
+
class Callbacks
|
85
|
+
attr_reader :input_callbacks
|
86
|
+
attr_reader :action_callbacks
|
87
|
+
|
88
|
+
def initialize
|
89
|
+
@input_callbacks = CallbackRegistry.new("")
|
90
|
+
@action_callbacks = CallbackRegistry.new("a")
|
91
|
+
end
|
92
|
+
|
93
|
+
def unregister(object)
|
94
|
+
@input_callbacks.unregister(object)
|
95
|
+
@action_callbacks.unregister(object)
|
96
|
+
end
|
97
|
+
|
98
|
+
def with_triggered(ids_and_values)
|
99
|
+
@input_callbacks.prepare_triggered(ids_and_values)
|
100
|
+
@action_callbacks.prepare_triggered(ids_and_values)
|
101
|
+
yield
|
102
|
+
ensure
|
103
|
+
@input_callbacks.reset_triggered
|
104
|
+
@action_callbacks.reset_triggered
|
105
|
+
end
|
106
|
+
|
107
|
+
end # class Callbacks
|
108
|
+
|
109
|
+
end # module Wee
|
@@ -0,0 +1,363 @@
|
|
1
|
+
require 'wee/presenter'
|
2
|
+
require 'wee/decoration'
|
3
|
+
|
4
|
+
module Wee
|
5
|
+
|
6
|
+
#
|
7
|
+
# The base class of all components. You should at least overwrite method
|
8
|
+
# #render in your own subclasses.
|
9
|
+
#
|
10
|
+
class Component < Presenter
|
11
|
+
|
12
|
+
#
|
13
|
+
# Constructs a new instance of the component.
|
14
|
+
#
|
15
|
+
# Overwrite this method when you want to use it both as a root component
|
16
|
+
# and as a non-root component. Here you can add neccessary decorations
|
17
|
+
# when used as root component, as for example a PageDecoration or a
|
18
|
+
# FormDecoration.
|
19
|
+
#
|
20
|
+
# By default this methods adds no decoration.
|
21
|
+
#
|
22
|
+
# See also class RootComponent.
|
23
|
+
#
|
24
|
+
def self.instanciate(*args, &block)
|
25
|
+
new(*args, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Return an array of classes onto which the current component depends.
|
30
|
+
# Right now this is only used to determine the required ExternalResources.
|
31
|
+
#
|
32
|
+
def self.depends
|
33
|
+
[]
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Initializes a newly created component.
|
38
|
+
#
|
39
|
+
def initialize
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# This method renders the content of the component.
|
44
|
+
#
|
45
|
+
# *OVERWRITE* this method in your own component classes to implement the
|
46
|
+
# view. By default this method does nothing!
|
47
|
+
#
|
48
|
+
# [+r+]
|
49
|
+
# An instance of class <tt>renderer_class()</tt>
|
50
|
+
#
|
51
|
+
def render(r)
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Take snapshots of objects that should correctly be backtracked.
|
56
|
+
#
|
57
|
+
# Backtracking means that you can go back in time of the components' state.
|
58
|
+
# Therefore it is neccessary to take snapshots of those objects that want to
|
59
|
+
# participate in backtracking. Taking snapshots of the whole component tree
|
60
|
+
# would be too expensive and unflexible. Note that methods
|
61
|
+
# <i>take_snapshot</i> and <i>restore_snapshot</i> are called for those
|
62
|
+
# objects to take the snapshot (they behave like <i>marshal_dump</i> and
|
63
|
+
# <i>marshal_load</i>). Overwrite them if you want to define special
|
64
|
+
# behaviour.
|
65
|
+
#
|
66
|
+
# By default only the decoration chain is backtracked. This is
|
67
|
+
# required to correctly backtrack called components. To disable
|
68
|
+
# backtracking of the decorations, change method
|
69
|
+
# Component#state_decoration to a no-operation:
|
70
|
+
#
|
71
|
+
# def state_decoration(s)
|
72
|
+
# # nothing here
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# [+s+]
|
76
|
+
# An object of class State
|
77
|
+
#
|
78
|
+
def state(s)
|
79
|
+
state_decoration(s)
|
80
|
+
for child in self.children
|
81
|
+
child.decoration.state(s)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
NO_CHILDREN = [].freeze
|
86
|
+
#
|
87
|
+
# Return all child components.
|
88
|
+
#
|
89
|
+
# *OVERWRITE* this method and return all child components
|
90
|
+
# collected in an array.
|
91
|
+
#
|
92
|
+
def children
|
93
|
+
return NO_CHILDREN
|
94
|
+
end
|
95
|
+
|
96
|
+
#
|
97
|
+
# Process and invoke all input callbacks specified for this component
|
98
|
+
# and all of it's child components.
|
99
|
+
#
|
100
|
+
# Returns the action callback to be invoked.
|
101
|
+
#
|
102
|
+
def process_callbacks(callbacks)
|
103
|
+
callbacks.input_callbacks.each_triggered_call_with_value(self)
|
104
|
+
|
105
|
+
action_callback = nil
|
106
|
+
|
107
|
+
# process callbacks of all children
|
108
|
+
for child in self.children
|
109
|
+
if act = child.decoration.process_callbacks(callbacks)
|
110
|
+
raise "Duplicate action callback" if action_callback
|
111
|
+
action_callback = act
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
if act = callbacks.action_callbacks.first_triggered(self)
|
116
|
+
raise "Duplicate action callback" if action_callback
|
117
|
+
action_callback = act
|
118
|
+
end
|
119
|
+
|
120
|
+
return action_callback
|
121
|
+
end
|
122
|
+
|
123
|
+
def state_decoration(s)
|
124
|
+
s.add_ivar(self, :@decoration, @decoration)
|
125
|
+
end
|
126
|
+
|
127
|
+
protected :state_decoration
|
128
|
+
|
129
|
+
# -------------------------------------------------------------
|
130
|
+
# Decoration Methods
|
131
|
+
# -------------------------------------------------------------
|
132
|
+
|
133
|
+
def decoration=(d) @decoration = d end
|
134
|
+
def decoration() @decoration || self end
|
135
|
+
|
136
|
+
#
|
137
|
+
# Iterates over all decorations
|
138
|
+
# (note that the component itself is excluded)
|
139
|
+
#
|
140
|
+
def each_decoration # :yields: decoration
|
141
|
+
d = @decoration
|
142
|
+
while d and d != self
|
143
|
+
yield d
|
144
|
+
d = d.next
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
#
|
149
|
+
# Searches a decoration in the decoration chain
|
150
|
+
#
|
151
|
+
def find_decoration
|
152
|
+
each_decoration {|d| yield d and return d }
|
153
|
+
return nil
|
154
|
+
end
|
155
|
+
|
156
|
+
#
|
157
|
+
# Adds decoration +d+ to the decoration chain.
|
158
|
+
#
|
159
|
+
# A global decoration is added in front of the decoration chain, a local
|
160
|
+
# decoration is added in front of all other local decorations but after all
|
161
|
+
# global decorations.
|
162
|
+
#
|
163
|
+
# Returns: +self+
|
164
|
+
#
|
165
|
+
def add_decoration(d)
|
166
|
+
if d.global?
|
167
|
+
d.next = self.decoration
|
168
|
+
self.decoration = d
|
169
|
+
else
|
170
|
+
last_global = nil
|
171
|
+
each_decoration {|i|
|
172
|
+
if i.global?
|
173
|
+
last_global = i
|
174
|
+
else
|
175
|
+
break
|
176
|
+
end
|
177
|
+
}
|
178
|
+
if last_global.nil?
|
179
|
+
# no global decorations specified -> add in front
|
180
|
+
d.next = self.decoration
|
181
|
+
self.decoration = d
|
182
|
+
else
|
183
|
+
# add after last_global
|
184
|
+
d.next = last_global.next
|
185
|
+
last_global.next = d
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
return self
|
190
|
+
end
|
191
|
+
|
192
|
+
#
|
193
|
+
# Remove decoration +d+ from the decoration chain.
|
194
|
+
#
|
195
|
+
# Returns the removed decoration or +nil+ if it did not exist in the
|
196
|
+
# decoration chain.
|
197
|
+
#
|
198
|
+
def remove_decoration(d)
|
199
|
+
if d == self.decoration # 'd' is in front
|
200
|
+
self.decoration = d.next
|
201
|
+
else
|
202
|
+
last_decoration = self.decoration
|
203
|
+
next_decoration = nil
|
204
|
+
loop do
|
205
|
+
return nil if last_decoration == self or last_decoration.nil?
|
206
|
+
next_decoration = last_decoration.next
|
207
|
+
break if d == next_decoration
|
208
|
+
last_decoration = next_decoration
|
209
|
+
end
|
210
|
+
last_decoration.next = d.next
|
211
|
+
end
|
212
|
+
d.next = nil # decoration 'd' no longer is an owner of anything!
|
213
|
+
return d
|
214
|
+
end
|
215
|
+
|
216
|
+
#
|
217
|
+
# Remove all decorations that match the block condition.
|
218
|
+
#
|
219
|
+
# Example (removes all decorations of class +HaloDecoration+):
|
220
|
+
#
|
221
|
+
# remove_decoration_if {|d| d.class == HaloDecoration}
|
222
|
+
#
|
223
|
+
def remove_decoration_if # :yields: decoration
|
224
|
+
to_remove = []
|
225
|
+
each_decoration {|d| to_remove << d if yield d}
|
226
|
+
to_remove.each {|d| remove_decoration(d)}
|
227
|
+
end
|
228
|
+
|
229
|
+
# -------------------------------------------------------------
|
230
|
+
# Call/Answer Methods
|
231
|
+
# -------------------------------------------------------------
|
232
|
+
|
233
|
+
#
|
234
|
+
# Call another component (without using continuations). The calling
|
235
|
+
# component is neither rendered nor are it's callbacks processed
|
236
|
+
# until the called component answers using method #answer.
|
237
|
+
#
|
238
|
+
# [+component+]
|
239
|
+
# The component to be called.
|
240
|
+
#
|
241
|
+
# [+return_callback+]
|
242
|
+
# Is invoked when the called component answers.
|
243
|
+
#
|
244
|
+
# <b>How it works</b>
|
245
|
+
#
|
246
|
+
# The component to be called is wrapped with an AnswerDecoration and a
|
247
|
+
# Delegate decoration. The latter is used to redirect to the called
|
248
|
+
# component. Once the decorations are installed, we end the processing of
|
249
|
+
# callbacks prematurely.
|
250
|
+
#
|
251
|
+
# When at a later point in time the called component invokes #answer, this
|
252
|
+
# will raise a AnswerDecoration::Answer exception which is catched by the
|
253
|
+
# AnswerDecoration we installed before calling this component, and as such,
|
254
|
+
# whose process_callbacks method was called before we gained control.
|
255
|
+
#
|
256
|
+
# The AnswerDecoration then invokes the <tt>answer_callback</tt> to cleanup
|
257
|
+
# the decorations we added during #call and finally passes control to the
|
258
|
+
# <tt>return_callback</tt>.
|
259
|
+
#
|
260
|
+
def call(component, &return_callback)
|
261
|
+
delegate = Delegate.new(component)
|
262
|
+
answer = AnswerDecoration.new
|
263
|
+
answer.answer_callback = UnwindCall.new(self, component, delegate, answer, &return_callback)
|
264
|
+
add_decoration(delegate)
|
265
|
+
component.add_decoration(answer)
|
266
|
+
session.send_response(nil)
|
267
|
+
end
|
268
|
+
|
269
|
+
protected :call
|
270
|
+
|
271
|
+
#
|
272
|
+
# Reverts the changes made due to Component#call. Is called when
|
273
|
+
# Component#call 'answers'.
|
274
|
+
#
|
275
|
+
class UnwindCall
|
276
|
+
def initialize(calling, called, delegate, answer, &return_callback)
|
277
|
+
@calling, @called, @delegate, @answer = calling, called, delegate, answer
|
278
|
+
@return_callback = return_callback
|
279
|
+
end
|
280
|
+
|
281
|
+
def call(answ)
|
282
|
+
@calling.remove_decoration(@delegate)
|
283
|
+
@called.remove_decoration(@answer)
|
284
|
+
@return_callback.call(*answ.args) if @return_callback
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
#
|
289
|
+
# Similar to method #call, but using continuations.
|
290
|
+
#
|
291
|
+
def callcc(component)
|
292
|
+
delegate = Delegate.new(component)
|
293
|
+
answer = AnswerDecoration.new
|
294
|
+
|
295
|
+
add_decoration(delegate)
|
296
|
+
component.add_decoration(answer)
|
297
|
+
|
298
|
+
answ = Kernel.callcc {|cc|
|
299
|
+
answer.answer_callback = cc
|
300
|
+
session.send_response(nil)
|
301
|
+
}
|
302
|
+
remove_decoration(delegate)
|
303
|
+
component.remove_decoration(answer)
|
304
|
+
|
305
|
+
args = answ.args
|
306
|
+
case args.size
|
307
|
+
when 0
|
308
|
+
return
|
309
|
+
when 1
|
310
|
+
return args.first
|
311
|
+
else
|
312
|
+
return *args
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
protected :callcc
|
317
|
+
|
318
|
+
#
|
319
|
+
# Chooses one of #call or #callcc depending on whether a block is
|
320
|
+
# given or not.
|
321
|
+
#
|
322
|
+
def call!(comp, &block)
|
323
|
+
if block
|
324
|
+
call comp, &block
|
325
|
+
else
|
326
|
+
callcc comp
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
protected :call!
|
331
|
+
|
332
|
+
def call_inline(&render_block)
|
333
|
+
callcc BlockComponent.new(&render_block)
|
334
|
+
end
|
335
|
+
|
336
|
+
protected :call_inline
|
337
|
+
|
338
|
+
#
|
339
|
+
# Return from a called component.
|
340
|
+
#
|
341
|
+
# NOTE that #answer never returns.
|
342
|
+
#
|
343
|
+
# See #call for a detailed description of the call/answer mechanism.
|
344
|
+
#
|
345
|
+
def answer(*args)
|
346
|
+
raise AnswerDecoration::Answer.new(args)
|
347
|
+
end
|
348
|
+
|
349
|
+
protected :answer
|
350
|
+
|
351
|
+
end # class Component
|
352
|
+
|
353
|
+
class BlockComponent < Component
|
354
|
+
def initialize(&block)
|
355
|
+
@block = block
|
356
|
+
end
|
357
|
+
|
358
|
+
def render(r)
|
359
|
+
instance_exec(r, &@block)
|
360
|
+
end
|
361
|
+
end # class BlockComponent
|
362
|
+
|
363
|
+
end # module Wee
|