wee 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/cache/cache.rb +158 -0
- data/lib/wee.rb +11 -0
- data/lib/wee/application.rb +91 -0
- data/lib/wee/component.rb +126 -0
- data/lib/wee/context.rb +12 -0
- data/lib/wee/delegate_decoration.rb +22 -0
- data/lib/wee/handler_registry.rb +89 -0
- data/lib/wee/holder.rb +14 -0
- data/lib/wee/html_canvas.rb +379 -0
- data/lib/wee/html_writer.rb +63 -0
- data/lib/wee/page.rb +1 -0
- data/lib/wee/session.rb +144 -0
- data/lib/wee/snapshot.rb +47 -0
- data/lib/wee/state_registry.rb +173 -0
- data/lib/wee/stuff.rb +29 -0
- data/lib/wee/utils/cache.rb +9 -0
- data/lib/wee/webrick.rb +15 -0
- metadata +55 -0
data/lib/cache/cache.rb
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
# Abstract super class of all cache implementations.
|
2
|
+
class Cache; end
|
3
|
+
|
4
|
+
# Abstract super class of all caching strategies.
|
5
|
+
class Cache::Strategy; end
|
6
|
+
|
7
|
+
# Implements an unbounded cache strategy. The cache size can grow to infinity.
|
8
|
+
class Cache::Strategy::Unbounded < Cache::Strategy
|
9
|
+
class Item < Struct.new(:value); end
|
10
|
+
def item_class() Item end
|
11
|
+
|
12
|
+
def access(item) end
|
13
|
+
def delete(item) end
|
14
|
+
def insert_or_extrude(item, enum) end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Abstract class for a capacity bounded strategy. Only up to _capacity_ items
|
18
|
+
# are allowed to be stored in the cache at any time.
|
19
|
+
class Cache::Strategy::CapacityBounded < Cache::Strategy
|
20
|
+
attr_accessor :capacity
|
21
|
+
|
22
|
+
def initialize(capacity)
|
23
|
+
@capacity = capacity
|
24
|
+
@n_items = 0 # number of items in cache
|
25
|
+
end
|
26
|
+
|
27
|
+
def inc(item)
|
28
|
+
raise if full?
|
29
|
+
@n_items += 1
|
30
|
+
end
|
31
|
+
|
32
|
+
def dec(item)
|
33
|
+
raise if empty?
|
34
|
+
@n_items -= 1
|
35
|
+
end
|
36
|
+
|
37
|
+
def full?
|
38
|
+
@n_items >= @capacity
|
39
|
+
end
|
40
|
+
|
41
|
+
def empty?
|
42
|
+
@n_items == 0
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Implements the least frequently used (LFU) strategy.
|
47
|
+
class Cache::Strategy::LFU < Cache::Strategy::CapacityBounded
|
48
|
+
class Item < Struct.new(:value, :freq); end
|
49
|
+
def item_class() Item end
|
50
|
+
|
51
|
+
def access(item)
|
52
|
+
item.freq += 1
|
53
|
+
end
|
54
|
+
|
55
|
+
def delete(item)
|
56
|
+
dec(item)
|
57
|
+
end
|
58
|
+
|
59
|
+
# enum::
|
60
|
+
# a [key, item] enumerable
|
61
|
+
#
|
62
|
+
def insert_or_extrude(item, enum)
|
63
|
+
# find least recently used key/item and yield
|
64
|
+
yield enum.min_by {|key, it| it.freq} while full?
|
65
|
+
item.freq = 0
|
66
|
+
inc(item)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Implements the least recently used (LRU) strategy.
|
71
|
+
class Cache::Strategy::LRU < Cache::Strategy::CapacityBounded
|
72
|
+
class Item < Struct.new(:value, :time); end
|
73
|
+
def item_class() Item end
|
74
|
+
|
75
|
+
def access(item)
|
76
|
+
item.time = Time.now
|
77
|
+
end
|
78
|
+
|
79
|
+
def delete(item)
|
80
|
+
dec(item)
|
81
|
+
end
|
82
|
+
|
83
|
+
# enum::
|
84
|
+
# a [key, item] enumerable
|
85
|
+
#
|
86
|
+
def insert_or_extrude(item, enum)
|
87
|
+
# find least recently used key/item and yield
|
88
|
+
yield enum.min_by {|key, it| it.time} while full?
|
89
|
+
item.time = Time.now
|
90
|
+
inc(item)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
# Implements a cache using a parameterizable strategy and a storage.
|
96
|
+
# The protocol that the _store_ must understand is:
|
97
|
+
#
|
98
|
+
# fetch(key) -> val
|
99
|
+
# has_key?(key)
|
100
|
+
# delete(key) -> val
|
101
|
+
# each {|key, val| }
|
102
|
+
#
|
103
|
+
class Cache::StorageCache < Cache
|
104
|
+
|
105
|
+
def initialize(strategy, store=Hash.new, store_on_update=false)
|
106
|
+
@strategy = strategy
|
107
|
+
@store = store
|
108
|
+
@store_on_update = store_on_update
|
109
|
+
end
|
110
|
+
|
111
|
+
def has_key?(key)
|
112
|
+
@store.has_key?(key)
|
113
|
+
end
|
114
|
+
|
115
|
+
def delete(key)
|
116
|
+
if @store.has_key?(key)
|
117
|
+
item = @store.delete(key)
|
118
|
+
@strategy.delete(item)
|
119
|
+
item.value
|
120
|
+
else
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def fetch(key, default_value=nil)
|
126
|
+
if @store.has_key?(key)
|
127
|
+
item = @store.fetch(key)
|
128
|
+
@strategy.access(item)
|
129
|
+
@store.store(key, item) if @store_on_update
|
130
|
+
item.value
|
131
|
+
else
|
132
|
+
default_value
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def store(key, value)
|
137
|
+
if @store.has_key?(key)
|
138
|
+
# update only
|
139
|
+
item = @store.fetch(key)
|
140
|
+
item.value = value
|
141
|
+
@strategy.access(item)
|
142
|
+
@store.store(key, item) if @store_on_update
|
143
|
+
else
|
144
|
+
# insert new item
|
145
|
+
item = @strategy.item_class.new
|
146
|
+
item.value = value
|
147
|
+
@strategy.insert_or_extrude(item, @store) do |k, i|
|
148
|
+
@strategy.delete(i)
|
149
|
+
@store.delete(k)
|
150
|
+
end
|
151
|
+
@store.store(key, item) # correct!
|
152
|
+
end
|
153
|
+
value
|
154
|
+
end
|
155
|
+
|
156
|
+
alias [] fetch
|
157
|
+
alias []= store
|
158
|
+
end
|
data/lib/wee.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
class Wee::Application
|
2
|
+
attr_accessor :name, :path
|
3
|
+
attr_accessor :session_store, :session_class
|
4
|
+
attr_accessor :dumpfile
|
5
|
+
|
6
|
+
def initialize(&block)
|
7
|
+
setup_session_id_generator
|
8
|
+
|
9
|
+
block.call(self)
|
10
|
+
|
11
|
+
if [@name, @path, @session_class, @session_store, @dumpfile].any? {|i| i.nil?}
|
12
|
+
raise ArgumentError, "missing name, path, session_class, session_store or dumpfile"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def setup_session_id_generator
|
17
|
+
@session_cnt = rand(1000_000)
|
18
|
+
end
|
19
|
+
|
20
|
+
def handle_request(req, res)
|
21
|
+
hash = parse_url(req)
|
22
|
+
|
23
|
+
session_id = hash['s']
|
24
|
+
if session_id.nil?
|
25
|
+
# TODO:
|
26
|
+
# gen session, or assign default session (by default, do not show).
|
27
|
+
# but for a default session, we have to have thread-safe components (without @context=... hack)
|
28
|
+
|
29
|
+
session = @session_class.new
|
30
|
+
session_id = gen_unique_session_id
|
31
|
+
@session_store[session_id] = session
|
32
|
+
end
|
33
|
+
|
34
|
+
session = @session_store[session_id]
|
35
|
+
if session.nil?
|
36
|
+
# TODO: redirect to session-less page, or do whatever
|
37
|
+
error_invalid_session(req, res); return
|
38
|
+
end
|
39
|
+
|
40
|
+
context = Wee::Context.new(req, res, session, session_id)
|
41
|
+
context.application = self
|
42
|
+
context.session = session
|
43
|
+
context.page_id = hash['p']
|
44
|
+
context.handler_id = hash['h']
|
45
|
+
context.resource_id = hash['r']
|
46
|
+
session.handle_request(context)
|
47
|
+
end
|
48
|
+
|
49
|
+
# TODO: UrlModel, which knows how to create and parse URLs
|
50
|
+
def gen_handler_url(session_id, page_id, handler_id)
|
51
|
+
[
|
52
|
+
self.path,
|
53
|
+
['s', session_id].join(':'),
|
54
|
+
['p', page_id].join(':'),
|
55
|
+
['h', handler_id].join(':')
|
56
|
+
].join('/')
|
57
|
+
end
|
58
|
+
|
59
|
+
def gen_resource_url(session_id, page_id, resource_id)
|
60
|
+
[
|
61
|
+
self.path,
|
62
|
+
['s', session_id].join(':'),
|
63
|
+
['p', page_id].join(':'),
|
64
|
+
['r', resource_id].join(':')
|
65
|
+
].join('/')
|
66
|
+
end
|
67
|
+
|
68
|
+
def error_invalid_session(req, res)
|
69
|
+
Wee::ErrorPage.new('Invalid Session').respond(Wee::Context.new(req,res,nil,nil))
|
70
|
+
end
|
71
|
+
|
72
|
+
def store_to_disk(dumpfile=nil)
|
73
|
+
File.open(dumpfile||@dumpfile, 'w+b') {|f| f << Marshal.dump(self) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.load_from_disk(filename)
|
77
|
+
Marshal.load(File.read(filename))
|
78
|
+
end
|
79
|
+
|
80
|
+
def shutdown
|
81
|
+
store_to_disk
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# TODO: that's insecure. it's just for development!
|
87
|
+
def gen_unique_session_id
|
88
|
+
(@session_cnt += 1).to_s
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
class Wee::Component
|
2
|
+
|
3
|
+
attr_accessor :caller # who is calling us
|
4
|
+
|
5
|
+
# call another component
|
6
|
+
def call(component, return_method=nil)
|
7
|
+
component.caller = [self, return_method]
|
8
|
+
add_decoration(Wee::Delegate.new(component))
|
9
|
+
end
|
10
|
+
|
11
|
+
# return from a called component
|
12
|
+
def answer(*args)
|
13
|
+
c, return_method = self.caller
|
14
|
+
c.remove_first_decoration
|
15
|
+
c.send(return_method, *args) if return_method
|
16
|
+
end
|
17
|
+
|
18
|
+
def process_request(context)
|
19
|
+
process_request_inputs(context)
|
20
|
+
children.each {|c| c.decoration.process_request(context) }
|
21
|
+
process_request_actions(context)
|
22
|
+
end
|
23
|
+
|
24
|
+
def process_request_inputs(context)
|
25
|
+
context.request.query.each do |hid, value|
|
26
|
+
if act = context.handler_registry.get_input(hid, self)
|
27
|
+
send(act, value)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def process_request_actions(context)
|
33
|
+
# handle URL actions
|
34
|
+
if act = context.handler_registry.get_action(context.handler_id, self)
|
35
|
+
act.invoke
|
36
|
+
end
|
37
|
+
|
38
|
+
# handle form actions
|
39
|
+
context.request.query.each do |hid, value|
|
40
|
+
if act = context.handler_registry.get_action(hid, self)
|
41
|
+
act.invoke
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Creates a new renderer for this component and renders it.
|
47
|
+
def render_with_context(rendering_context)
|
48
|
+
r = renderer_class.new(rendering_context)
|
49
|
+
r.current_component = self
|
50
|
+
render_content_on(r)
|
51
|
+
end
|
52
|
+
|
53
|
+
# You call render_on to render a component.
|
54
|
+
def render_on(renderer)
|
55
|
+
decoration.render_with_context(renderer.context)
|
56
|
+
end
|
57
|
+
|
58
|
+
def render_content_on(r)
|
59
|
+
end
|
60
|
+
|
61
|
+
def renderer_class
|
62
|
+
Wee::HtmlCanvas
|
63
|
+
end
|
64
|
+
|
65
|
+
def children
|
66
|
+
@children || []
|
67
|
+
end
|
68
|
+
|
69
|
+
def add_child(c)
|
70
|
+
@children ||= []
|
71
|
+
@children << c
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_children(*c)
|
75
|
+
@children ||= []
|
76
|
+
@children.push(*c)
|
77
|
+
end
|
78
|
+
|
79
|
+
# returns the first decoration in the decoration chain, or the component
|
80
|
+
# itself, if none was specified
|
81
|
+
|
82
|
+
attr_writer :decoration
|
83
|
+
|
84
|
+
# FIXME: @decoration may never be self when backtracking
|
85
|
+
# in any case, self==nil
|
86
|
+
def decoration
|
87
|
+
@decoration || self
|
88
|
+
end
|
89
|
+
|
90
|
+
# add decoration _d_ in front of the decoration chain.
|
91
|
+
def add_decoration_in_front(d)
|
92
|
+
d.next = self.decoration
|
93
|
+
self.decoration = d
|
94
|
+
end
|
95
|
+
alias add_decoration add_decoration_in_front
|
96
|
+
|
97
|
+
def remove_decoration(d)
|
98
|
+
if d == @decoration
|
99
|
+
# d is in front
|
100
|
+
@decoration = d.next
|
101
|
+
else
|
102
|
+
last_decoration = @decoration
|
103
|
+
loop do
|
104
|
+
raise "not found" if last_decoration == self
|
105
|
+
break if d == last_decoration.next
|
106
|
+
last_decoration = last_decoration.next
|
107
|
+
end
|
108
|
+
last_decoration.next = d.next
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def add_decoration(d)
|
113
|
+
d.next = self.decoration
|
114
|
+
self.decoration = d
|
115
|
+
end
|
116
|
+
|
117
|
+
def remove_first_decoration
|
118
|
+
self.decoration = self.decoration.next
|
119
|
+
self.decoration = nil if self.decoration == self
|
120
|
+
end
|
121
|
+
|
122
|
+
def session
|
123
|
+
Wee::Session.current
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
data/lib/wee/context.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
class Wee::Context
|
2
|
+
attr_accessor :application
|
3
|
+
attr_accessor :request, :response, :session, :session_id
|
4
|
+
attr_accessor :page_id, :handler_id, :resource_id
|
5
|
+
attr_accessor :handler_registry
|
6
|
+
|
7
|
+
def initialize(request, response, session, session_id)
|
8
|
+
@request, @response, @session, @session_id, @root = request, response, session, session_id
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Wee::RenderingContext < Struct.new(:context, :document); end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Wee::Delegate
|
2
|
+
attr_accessor :next # next decoration in chain
|
3
|
+
|
4
|
+
def initialize(component)
|
5
|
+
@component = component
|
6
|
+
end
|
7
|
+
|
8
|
+
def process_request(context)
|
9
|
+
@component.decoration.process_request(context)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Creates a new renderer for this component and renders it.
|
13
|
+
def render_with_context(rendering_context)
|
14
|
+
r = renderer_class.new(rendering_context)
|
15
|
+
r.current_component = self
|
16
|
+
r.render(@component)
|
17
|
+
end
|
18
|
+
|
19
|
+
def renderer_class
|
20
|
+
Wee::HtmlCanvas
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
class Wee::ActionHandler
|
2
|
+
attr_accessor :obj, :meth, :args
|
3
|
+
|
4
|
+
def self.[](obj, meth, *args)
|
5
|
+
new(obj, meth, *args)
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(obj, meth, *args)
|
9
|
+
@obj, @meth, @args = obj, meth.to_s, args
|
10
|
+
end
|
11
|
+
|
12
|
+
def invoke
|
13
|
+
@obj.send(@meth, *@args)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Wee::ResourceHandler
|
18
|
+
attr_accessor :content, :content_type
|
19
|
+
def initialize(content, content_type)
|
20
|
+
@content, @content_type = content, content_type
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Wee::HandlerRegistry
|
25
|
+
def initialize
|
26
|
+
@next_handler_id = 1
|
27
|
+
@action_registry = Hash.new
|
28
|
+
@input_registry = Hash.new
|
29
|
+
@resource_registry = Hash.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def handler_id_for_action(action_handler)
|
33
|
+
hid = get_next_handler_id()
|
34
|
+
raise if @action_registry.has_key?(hid)
|
35
|
+
@action_registry[hid] = action_handler
|
36
|
+
return hid
|
37
|
+
end
|
38
|
+
|
39
|
+
def handler_id_for_input(obj, input)
|
40
|
+
hid = get_next_handler_id()
|
41
|
+
raise if @input_registry.has_key?(hid)
|
42
|
+
@input_registry[hid] = [obj, input.to_s]
|
43
|
+
return hid
|
44
|
+
end
|
45
|
+
|
46
|
+
def handler_id_for_resource(resource)
|
47
|
+
hid = get_next_handler_id()
|
48
|
+
raise if @resource_registry.has_key?(hid)
|
49
|
+
@resource_registry[hid] = resource
|
50
|
+
return hid
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_action(handler_id, obj)
|
54
|
+
action_handler = @action_registry[handler_id]
|
55
|
+
return nil unless action_handler
|
56
|
+
|
57
|
+
if action_handler.obj == obj
|
58
|
+
action_handler
|
59
|
+
else
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_input(handler_id, obj)
|
65
|
+
return nil unless @input_registry.has_key?(handler_id)
|
66
|
+
|
67
|
+
component, input = @input_registry[handler_id]
|
68
|
+
|
69
|
+
if component == obj
|
70
|
+
input
|
71
|
+
else
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_resource(handler_id)
|
77
|
+
return @resource_registry[handler_id]
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# TODO: randomize
|
83
|
+
def get_next_handler_id
|
84
|
+
@next_handler_id.to_s
|
85
|
+
ensure
|
86
|
+
@next_handler_id += 1
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|