manveru-innate 2009.02.06
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/CHANGELOG +1409 -0
- data/COPYING +18 -0
- data/MANIFEST +100 -0
- data/README.md +485 -0
- data/Rakefile +139 -0
- data/example/app/retro_games.rb +57 -0
- data/example/app/whywiki_erb/layout/wiki.html.erb +15 -0
- data/example/app/whywiki_erb/spec/wiki.rb +19 -0
- data/example/app/whywiki_erb/start.rb +45 -0
- data/example/app/whywiki_erb/view/edit.html.erb +6 -0
- data/example/app/whywiki_erb/view/index.html.erb +10 -0
- data/example/custom_middleware.rb +43 -0
- data/example/error_handling.rb +31 -0
- data/example/hello.rb +12 -0
- data/example/howto_spec.rb +60 -0
- data/example/link.rb +35 -0
- data/example/providing_hash.rb +46 -0
- data/example/session.rb +42 -0
- data/innate.gemspec +118 -0
- data/lib/innate.rb +191 -0
- data/lib/innate/action.rb +156 -0
- data/lib/innate/adapter.rb +89 -0
- data/lib/innate/cache.rb +117 -0
- data/lib/innate/cache/api.rb +106 -0
- data/lib/innate/cache/drb.rb +58 -0
- data/lib/innate/cache/file_based.rb +39 -0
- data/lib/innate/cache/marshal.rb +17 -0
- data/lib/innate/cache/memory.rb +22 -0
- data/lib/innate/cache/yaml.rb +17 -0
- data/lib/innate/core_compatibility/basic_object.rb +9 -0
- data/lib/innate/core_compatibility/string.rb +3 -0
- data/lib/innate/current.rb +37 -0
- data/lib/innate/dynamap.rb +81 -0
- data/lib/innate/helper.rb +195 -0
- data/lib/innate/helper/aspect.rb +62 -0
- data/lib/innate/helper/cgi.rb +39 -0
- data/lib/innate/helper/flash.rb +36 -0
- data/lib/innate/helper/link.rb +55 -0
- data/lib/innate/helper/partial.rb +90 -0
- data/lib/innate/helper/redirect.rb +85 -0
- data/lib/innate/helper/send_file.rb +18 -0
- data/lib/innate/log.rb +23 -0
- data/lib/innate/log/color_formatter.rb +43 -0
- data/lib/innate/log/hub.rb +72 -0
- data/lib/innate/mock.rb +49 -0
- data/lib/innate/node.rb +471 -0
- data/lib/innate/options.rb +91 -0
- data/lib/innate/options/dsl.rb +155 -0
- data/lib/innate/request.rb +165 -0
- data/lib/innate/response.rb +18 -0
- data/lib/innate/route.rb +109 -0
- data/lib/innate/session.rb +104 -0
- data/lib/innate/session/flash.rb +94 -0
- data/lib/innate/setup.rb +23 -0
- data/lib/innate/spec.rb +42 -0
- data/lib/innate/state.rb +22 -0
- data/lib/innate/state/accessor.rb +130 -0
- data/lib/innate/state/fiber.rb +68 -0
- data/lib/innate/state/thread.rb +39 -0
- data/lib/innate/traited.rb +20 -0
- data/lib/innate/trinity.rb +22 -0
- data/lib/innate/version.rb +3 -0
- data/lib/innate/view.rb +67 -0
- data/lib/innate/view/erb.rb +17 -0
- data/lib/innate/view/none.rb +9 -0
- data/lib/rack/middleware_compiler.rb +62 -0
- data/lib/rack/reloader.rb +192 -0
- data/spec/example/hello.rb +14 -0
- data/spec/example/link.rb +29 -0
- data/spec/helper.rb +2 -0
- data/spec/innate/cache/common.rb +45 -0
- data/spec/innate/cache/marshal.rb +5 -0
- data/spec/innate/cache/memory.rb +5 -0
- data/spec/innate/cache/yaml.rb +5 -0
- data/spec/innate/dynamap.rb +22 -0
- data/spec/innate/helper.rb +66 -0
- data/spec/innate/helper/aspect.rb +80 -0
- data/spec/innate/helper/cgi.rb +37 -0
- data/spec/innate/helper/flash.rb +148 -0
- data/spec/innate/helper/link.rb +82 -0
- data/spec/innate/helper/partial.rb +66 -0
- data/spec/innate/helper/redirect.rb +148 -0
- data/spec/innate/helper/send_file.rb +21 -0
- data/spec/innate/helper/view/aspect_hello.erb +1 -0
- data/spec/innate/helper/view/locals.erb +1 -0
- data/spec/innate/helper/view/loop.erb +4 -0
- data/spec/innate/helper/view/num.erb +1 -0
- data/spec/innate/helper/view/partial.erb +1 -0
- data/spec/innate/helper/view/recursive.erb +8 -0
- data/spec/innate/mock.rb +84 -0
- data/spec/innate/node.rb +180 -0
- data/spec/innate/node/bar.html +1 -0
- data/spec/innate/node/foo.html.erb +1 -0
- data/spec/innate/node/with_layout.erb +3 -0
- data/spec/innate/options.rb +90 -0
- data/spec/innate/parameter.rb +154 -0
- data/spec/innate/request.rb +73 -0
- data/spec/innate/route.rb +129 -0
- data/spec/innate/session.rb +59 -0
- data/spec/innate/traited.rb +55 -0
- metadata +160 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
module Rack
|
2
|
+
module Handler
|
3
|
+
autoload :Thin, 'rack/handler/thin'
|
4
|
+
autoload :Ebb, 'ebb'
|
5
|
+
autoload :SwiftipliedMongrel, 'rack/handler/swiftiplied_mongrel'
|
6
|
+
|
7
|
+
register 'thin', 'Rack::Handler::Thin'
|
8
|
+
register 'ebb', 'Rack::Handler::Ebb'
|
9
|
+
register 'smongrel', 'Rack::Handler::SwiftipliedMongrel'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Innate
|
14
|
+
|
15
|
+
# Lightweight wrapper around Rack::Handler, will apply our options in a
|
16
|
+
# unified manner and deal with adapters that don't like to do what we want or
|
17
|
+
# where Rack doesn't want to take a stand.
|
18
|
+
|
19
|
+
module Adapter
|
20
|
+
class << self
|
21
|
+
|
22
|
+
# Pass given app to the Handler, handler is chosen based on
|
23
|
+
# config.adapter option.
|
24
|
+
# If there is a method named start_name_of_adapter it will be run instead
|
25
|
+
# of the default run method of the handler, this makes it easy to define
|
26
|
+
# custom startup of handlers for your server of choice
|
27
|
+
def start(app, options = Innate.options)
|
28
|
+
adapter_name = options[:adapter].to_s.downcase
|
29
|
+
config = { :Host => options[:host], :Port => options[:port] }
|
30
|
+
Log.debug "Innate uses #{adapter_name}"
|
31
|
+
|
32
|
+
if respond_to?(method = "start_#{adapter_name}")
|
33
|
+
send(method, app, config)
|
34
|
+
else
|
35
|
+
Rack::Handler.get(adapter_name).run(app, config)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Due to buggy autoload on Ruby 1.8 we have to require 'ebb' manually.
|
40
|
+
# This most likely happens because autoload doesn't respect the require
|
41
|
+
# of rubygems and uses the C require directly.
|
42
|
+
def start_ebb(app, config)
|
43
|
+
require 'ebb'
|
44
|
+
Rack::Handler.get('ebb').run(app, config)
|
45
|
+
end
|
46
|
+
|
47
|
+
# We want webrick to use our logger.
|
48
|
+
|
49
|
+
def start_webrick(app, config)
|
50
|
+
handler = Rack::Handler.get('webrick')
|
51
|
+
config = {
|
52
|
+
:BindAddress => config[:Host],
|
53
|
+
:Port => config[:Port],
|
54
|
+
:Logger => Log,
|
55
|
+
:AccessLog => [
|
56
|
+
[Log, ::WEBrick::AccessLog::COMMON_LOG_FORMAT],
|
57
|
+
[Log, ::WEBrick::AccessLog::REFERER_LOG_FORMAT]]
|
58
|
+
}
|
59
|
+
|
60
|
+
handler.run(app, config)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Thin shouldn't give excessive output, especially not to $stdout
|
64
|
+
|
65
|
+
def start_thin(app, config)
|
66
|
+
require 'thin'
|
67
|
+
handler = Rack::Handler.get('thin')
|
68
|
+
::Thin::Logging.silent = true
|
69
|
+
handler.run(app, config)
|
70
|
+
end
|
71
|
+
|
72
|
+
# swiftcore has its own handler outside of rack
|
73
|
+
|
74
|
+
def start_emongrel(app, config)
|
75
|
+
require 'swiftcore/evented_mongrel'
|
76
|
+
handler = Rack::Handler.get('emongrel')
|
77
|
+
handler.run(app, config)
|
78
|
+
end
|
79
|
+
|
80
|
+
# swiftcore has its own handler outside of rack
|
81
|
+
|
82
|
+
def start_smongrel(app, config)
|
83
|
+
require 'swiftcore/swiftiplied_mongrel'
|
84
|
+
handler = Rack::Handler.get('smongrel')
|
85
|
+
handler.run(app, config)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/innate/cache.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
module Innate
|
2
|
+
# Cache manager and wrapper.
|
3
|
+
#
|
4
|
+
# Provides a convenient wrapper around caches to keep method name confusion
|
5
|
+
# at a minimum while still having short and meaningful method names for every
|
6
|
+
# cache instance.
|
7
|
+
#
|
8
|
+
# The default caching is specified in lib/innate.rb in the config section.
|
9
|
+
# At the time of writing it defaults to Innate::Cache::Memory but can be
|
10
|
+
# changed easily.
|
11
|
+
#
|
12
|
+
# Configuration has to be done before Innate::setup_dependencies is being
|
13
|
+
# called.
|
14
|
+
#
|
15
|
+
# Configuration:
|
16
|
+
#
|
17
|
+
# Innate.options.cache do |cache|
|
18
|
+
# cache.names = [:session, :user]
|
19
|
+
# cache.session = Innate::Cache::Marshal
|
20
|
+
# cache.user = Innate::Cache::YAML
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# Usage for storing:
|
24
|
+
#
|
25
|
+
# # Storing with a time to live (10 seconds)
|
26
|
+
# Innate::Cache.user.store(:manveru, "Michael Fellinger", :ttl => 10)
|
27
|
+
#
|
28
|
+
# # Storing indefinitely
|
29
|
+
# Innate::Cache.user[:Pistos] = "unknown"
|
30
|
+
# # or without :ttl argument
|
31
|
+
# Innate::Cache.user.store(:Pistos, "unknown")
|
32
|
+
#
|
33
|
+
# Usage for retrieving:
|
34
|
+
#
|
35
|
+
# # we stored this one for 10 seconds
|
36
|
+
# Innate::Cache.user.fetch(:manveru, 'not here anymore')
|
37
|
+
# # => "Michael Fellinger"
|
38
|
+
# sleep 11
|
39
|
+
# Innate::Cache.user.fetch(:manveru, 'not here anymore')
|
40
|
+
# # => "not here anymore"
|
41
|
+
#
|
42
|
+
# Innate::Cache.user[:Pistos]
|
43
|
+
# # => "unknown"
|
44
|
+
# Innate::Cache.user.fetch(:Pistos)
|
45
|
+
# # => "unknown"
|
46
|
+
#
|
47
|
+
#
|
48
|
+
# For more details and to find out how to implement your own cache please
|
49
|
+
# read the documentation of Innate::Cache::API
|
50
|
+
#
|
51
|
+
# NOTE:
|
52
|
+
# * Some caches might expose their contents for everyone else on the same
|
53
|
+
# system, or even on connected systems. The rule as usual is, not to
|
54
|
+
# cache sensitive information.
|
55
|
+
|
56
|
+
class Cache
|
57
|
+
autoload :API, 'innate/cache/api'
|
58
|
+
autoload :DRb, 'innate/cache/drb'
|
59
|
+
autoload :YAML, 'innate/cache/yaml'
|
60
|
+
autoload :Memory, 'innate/cache/memory'
|
61
|
+
autoload :Marshal, 'innate/cache/marshal'
|
62
|
+
autoload :FileBased, 'innate/cache/file_based'
|
63
|
+
|
64
|
+
attr_reader :name, :instance
|
65
|
+
|
66
|
+
def initialize(name, klass = nil)
|
67
|
+
@name = name.to_s.dup.freeze
|
68
|
+
|
69
|
+
options = Innate.options
|
70
|
+
|
71
|
+
klass ||= options[:cache, @name.to_sym]
|
72
|
+
@instance = klass.new
|
73
|
+
|
74
|
+
@instance.cache_setup(
|
75
|
+
options.env.host,
|
76
|
+
options.env.user,
|
77
|
+
options.app.name,
|
78
|
+
@name
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.setup
|
83
|
+
Innate.options.cache.names.each{|name| add(name) }
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.register(cache)
|
87
|
+
key = cache.name
|
88
|
+
source = "def self.%s() @%s; end
|
89
|
+
def self.%s=(o) @%s = o; end" % [key, key, key, key]
|
90
|
+
self.class_eval(source, __FILE__, __LINE__)
|
91
|
+
|
92
|
+
self.send("#{key}=", cache)
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.add(name)
|
96
|
+
register(new(name))
|
97
|
+
end
|
98
|
+
|
99
|
+
def clear
|
100
|
+
instance.cache_clear
|
101
|
+
end
|
102
|
+
|
103
|
+
def delete(*keys)
|
104
|
+
instance.cache_delete(*keys)
|
105
|
+
end
|
106
|
+
|
107
|
+
def fetch(key, default = nil)
|
108
|
+
instance.cache_fetch(key, default)
|
109
|
+
end
|
110
|
+
alias [] fetch
|
111
|
+
|
112
|
+
def store(key, value, options = {})
|
113
|
+
instance.cache_store(key, value, options)
|
114
|
+
end
|
115
|
+
alias []= store
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Innate
|
2
|
+
class Cache
|
3
|
+
|
4
|
+
# This is the API every Cache has to conform to.
|
5
|
+
#
|
6
|
+
# The default behaviour is tailored for the Memory cache, override any
|
7
|
+
# behaviour as you need.
|
8
|
+
#
|
9
|
+
# +key+ may be a String or Symbol
|
10
|
+
# +value+ is a Hash of serializable (as according to Marshal) objects
|
11
|
+
#
|
12
|
+
# Every cache instance has to respond to:
|
13
|
+
#
|
14
|
+
# ::new()
|
15
|
+
# #cache_setup(hostname, username, appname, cachename)
|
16
|
+
# #cache_clear()
|
17
|
+
# #cache_delete(*keys)
|
18
|
+
# #cache_fetch(key, default = nil)
|
19
|
+
# #cache_store(key, value, options = {})
|
20
|
+
#
|
21
|
+
# We are prefixing cache_ to make the intent clear and implementation
|
22
|
+
# easier, as there may be existing behaviour associated with the
|
23
|
+
# non-prefixed version.
|
24
|
+
#
|
25
|
+
# Also note that we create one instance per cache name-space.
|
26
|
+
module API
|
27
|
+
# Executed after #initialize and before any other method.
|
28
|
+
#
|
29
|
+
# Some parameters identifying the current process will be passed so
|
30
|
+
# caches that act in one global name-space can use them as a prefix.
|
31
|
+
#
|
32
|
+
# Treat all arguments as Strings.
|
33
|
+
#
|
34
|
+
# +hostname+ the hostname of the machine.
|
35
|
+
# +username+ user executing this process.
|
36
|
+
# +appname+ identifier for the application being executed.
|
37
|
+
# +cachename+ name-space of the cache, like 'session' or 'action'
|
38
|
+
def cache_setup(hostname, username, appname, cachename)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Remove all key/value pairs from the cache.
|
42
|
+
# Should behave as if #delete had been called with all +keys+ as argument.
|
43
|
+
def cache_clear
|
44
|
+
clear
|
45
|
+
end
|
46
|
+
|
47
|
+
# Remove the corresponding key/value pair for each key passed.
|
48
|
+
# If removing is not an option it should set the corresponding value to nil.
|
49
|
+
#
|
50
|
+
# If only one key was deleted, answer with the corresponding value.
|
51
|
+
# If multiple keys were deleted, answer with an Array containing the values.
|
52
|
+
def cache_delete(key, *keys)
|
53
|
+
if keys.empty?
|
54
|
+
if value = yield(key)
|
55
|
+
value[:value]
|
56
|
+
end
|
57
|
+
else
|
58
|
+
[key, *keys].map{|k| cache_delete(k) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Answer with the value associated with the +key+, +nil+ if not found or
|
63
|
+
# expired.
|
64
|
+
def cache_fetch(key, default = nil)
|
65
|
+
value = default
|
66
|
+
|
67
|
+
if entry = yield(key)
|
68
|
+
if expires = entry[:expires]
|
69
|
+
if expires > Time.now
|
70
|
+
value = entry[:value]
|
71
|
+
else
|
72
|
+
cache_delete(key)
|
73
|
+
end
|
74
|
+
else
|
75
|
+
value = entry[:value]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
return value
|
80
|
+
end
|
81
|
+
|
82
|
+
# Set +key+ to +value+.
|
83
|
+
#
|
84
|
+
# +options+ may be one of:
|
85
|
+
# :ttl => time to live in seconds if given in Numeric
|
86
|
+
# infinite or maximum if not given
|
87
|
+
#
|
88
|
+
# Usage:
|
89
|
+
# Cache.value.store(:num, 3, :ttl => 20)
|
90
|
+
# Cache.value.fetch(:num) # => 3
|
91
|
+
# sleep 21
|
92
|
+
# Cache.value.fetch(:num) # => nil
|
93
|
+
#
|
94
|
+
def cache_store(key, value, options = {})
|
95
|
+
ttl = options[:ttl]
|
96
|
+
|
97
|
+
value_hash = {:value => value}
|
98
|
+
value_hash[:expires] = Time.now + ttl if ttl
|
99
|
+
|
100
|
+
yield(key, value_hash)
|
101
|
+
|
102
|
+
return value
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'drb'
|
2
|
+
|
3
|
+
module Innate
|
4
|
+
module Cache
|
5
|
+
|
6
|
+
# Cache utilizing a DRb server.
|
7
|
+
#
|
8
|
+
# You will need to run a corresponding DRb server to use this cache. The
|
9
|
+
# example below is using a normal Hash, but it is recommended to use a
|
10
|
+
# thread-safe alternative like SyncHash.
|
11
|
+
#
|
12
|
+
# Example for DRb server:
|
13
|
+
#
|
14
|
+
# require 'drb'
|
15
|
+
#
|
16
|
+
# URI = "druby://127.0.0.1:9069"
|
17
|
+
# CACHE = {}
|
18
|
+
#
|
19
|
+
# $SAFE = 1 # disable eval and friends
|
20
|
+
#
|
21
|
+
# DRb.start_service(URI, CACHE)
|
22
|
+
# DRb.thread.join
|
23
|
+
#
|
24
|
+
# Usage for all caches:
|
25
|
+
#
|
26
|
+
# Innate.options.cache.default = Innate::Cache::DRb
|
27
|
+
#
|
28
|
+
# Usage for sessions only:
|
29
|
+
#
|
30
|
+
# Innate.options.cache.session = Innate::Cache::DRb
|
31
|
+
class DRb
|
32
|
+
include Cache::API
|
33
|
+
|
34
|
+
OPTIONS = {:address => '127.0.0.1', :port => 9069}
|
35
|
+
|
36
|
+
def cache_setup(*args)
|
37
|
+
address, port = OPTIONS.values_at(:address, :port)
|
38
|
+
@store = DRbObject.new(nil, "druby://#{address}:#{port}")
|
39
|
+
end
|
40
|
+
|
41
|
+
def cache_clear
|
42
|
+
@store.clear
|
43
|
+
end
|
44
|
+
|
45
|
+
def cache_store(*args)
|
46
|
+
super{|key, value| @store[key] = value }
|
47
|
+
end
|
48
|
+
|
49
|
+
def cache_fetch(*args)
|
50
|
+
super{|key| @store[key] }
|
51
|
+
end
|
52
|
+
|
53
|
+
def cache_delete(*args)
|
54
|
+
super{|key| @store.delete(key) }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Innate
|
2
|
+
class Cache
|
3
|
+
|
4
|
+
# Used by caches that serialize their contents to the filesystem.
|
5
|
+
module FileBased
|
6
|
+
def cache_setup(*args)
|
7
|
+
@prefix = args.compact.join('-')
|
8
|
+
|
9
|
+
@dir = File.join(Dir.tmpdir, self.class::DIR)
|
10
|
+
FileUtils.mkdir_p(@dir)
|
11
|
+
|
12
|
+
@filename = File.join(@dir, @prefix + self.class::EXT)
|
13
|
+
@store = self.class::STORE.new(@filename)
|
14
|
+
end
|
15
|
+
|
16
|
+
def cache_clear
|
17
|
+
FileUtils.mkdir_p(@dir)
|
18
|
+
FileUtils.rm_f(@filename)
|
19
|
+
@store = self.class::STORE.new(@filename)
|
20
|
+
end
|
21
|
+
|
22
|
+
def cache_store(*args)
|
23
|
+
super{|key, value| transaction{|store| store[key] = value } }
|
24
|
+
end
|
25
|
+
|
26
|
+
def cache_fetch(*args)
|
27
|
+
super{|key| transaction{|store| store[key] } }
|
28
|
+
end
|
29
|
+
|
30
|
+
def cache_delete(*args)
|
31
|
+
super{|key| transaction{|store| store.delete(key) } }
|
32
|
+
end
|
33
|
+
|
34
|
+
def transaction(&block)
|
35
|
+
Innate.sync{ @store.transaction(&block) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'pstore'
|
2
|
+
|
3
|
+
module Innate
|
4
|
+
class Cache
|
5
|
+
# Keeps every cache in a separate file like this:
|
6
|
+
#
|
7
|
+
# /tmp/innate-cache-marshal/delta-manveru-session.marshal
|
8
|
+
class Marshal
|
9
|
+
include Cache::API
|
10
|
+
include Cache::FileBased
|
11
|
+
|
12
|
+
STORE = ::PStore
|
13
|
+
DIR = 'innate-cache-marshal'
|
14
|
+
EXT = '.marshal'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Innate
|
2
|
+
class Cache
|
3
|
+
# Memory cache is simply a Hash with the Cache::API, it's the reference
|
4
|
+
# implementation for every other cache.
|
5
|
+
|
6
|
+
class Memory < Hash
|
7
|
+
include Cache::API
|
8
|
+
|
9
|
+
def cache_store(*args)
|
10
|
+
super{|key, value| self[key] = value }
|
11
|
+
end
|
12
|
+
|
13
|
+
def cache_fetch(*args)
|
14
|
+
super{|key| self[key] }
|
15
|
+
end
|
16
|
+
|
17
|
+
def cache_delete(*args)
|
18
|
+
super{|key| delete(key) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|