mack 0.0.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/README +43 -0
- data/bin/mack +60 -0
- data/bin/templates/Rakefile.template +6 -0
- data/bin/templates/app/controllers/default_controller.rb.template +7 -0
- data/bin/templates/app/helpers/application_helper.rb.template +2 -0
- data/bin/templates/app/views/default/index.html.erb.template +3 -0
- data/bin/templates/app/views/layouts/application.html.erb.template +15 -0
- data/bin/templates/config/app_config/default.yml.template +7 -0
- data/bin/templates/config/app_config/development.yml.template +0 -0
- data/bin/templates/config/app_config/production.yml.template +0 -0
- data/bin/templates/config/app_config/test.yml.template +0 -0
- data/bin/templates/config/boot.rb.template +6 -0
- data/bin/templates/config/database.yml.template +20 -0
- data/bin/templates/config/routes.rb.template +7 -0
- data/bin/templates/config/thin.ru.template +1 -0
- data/bin/templates/config/thin.yml.template +8 -0
- data/bin/templates/public/favicon.ico +0 -0
- data/bin/templates/public/stylesheets/scaffold.css.template +74 -0
- data/core_extensions/hash.rb +9 -0
- data/core_extensions/module.rb +29 -0
- data/core_extensions/nil.rb +8 -0
- data/core_extensions/object.rb +9 -0
- data/core_extensions/string.rb +28 -0
- data/errors/errors.rb +79 -0
- data/initialize/configuration.rb +99 -0
- data/initialize/configure_logging.rb +24 -0
- data/initialize/configure_orm_support.rb +23 -0
- data/initialize/console.rb +13 -0
- data/initialize/initializer.rb +88 -0
- data/initialize/server/simple_server.rb +21 -0
- data/lib/utils/html.rb +88 -0
- data/lib/utils/server.rb +27 -0
- data/mack.rb +124 -0
- data/mack_tasks.rb +16 -0
- data/routing/route_map.rb +268 -0
- data/routing/urls.rb +54 -0
- data/sea_level/controller_base.rb +293 -0
- data/sea_level/cookie_jar.rb +67 -0
- data/sea_level/filter.rb +63 -0
- data/sea_level/helpers/view_helpers/html_helpers.rb +33 -0
- data/sea_level/helpers/view_helpers/orm_helpers.rb +72 -0
- data/sea_level/request.rb +83 -0
- data/sea_level/response.rb +6 -0
- data/sea_level/session.rb +33 -0
- data/sea_level/view_binder.rb +101 -0
- data/tasks/cachetastic_tasks.rake +69 -0
- data/tasks/log_tasks.rake +9 -0
- data/tasks/mack_tasks.rake +15 -0
- data/tasks/rake_helpers.rb +24 -0
- data/tasks/rake_rules.rake +19 -0
- data/tasks/script_tasks.rake +44 -0
- data/tasks/test_tasks.rake +7 -0
- data/test_extensions/test_assertions.rb +47 -0
- data/test_extensions/test_helpers.rb +84 -0
- metadata +173 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
module Mack
|
2
|
+
|
3
|
+
# Examples:
|
4
|
+
# class MyAwesomeController < Mack::Controller::Base
|
5
|
+
# def index
|
6
|
+
# cookies[:id] = 1
|
7
|
+
# render(:text => "Hello!")
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# def show
|
11
|
+
# render(:text => "The id in the cookie is: #{cookies[:id]}")
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
class CookieJar
|
15
|
+
|
16
|
+
attr_reader :all_cookies # :nodoc:
|
17
|
+
attr_reader :request # :nodoc:
|
18
|
+
attr_reader :response # :nodoc:
|
19
|
+
|
20
|
+
def initialize(request, response) # :nodoc:
|
21
|
+
@request = request
|
22
|
+
@response = response
|
23
|
+
@all_cookies = request.cookies
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the value of a cookie as a String, or nil it doesn't exist.
|
27
|
+
# This will check both the incoming cookies on the request, as well as
|
28
|
+
# any cookies that have been set as part of the current action.
|
29
|
+
def [](key)
|
30
|
+
return nil if key.nil?
|
31
|
+
# check both the incoming cookies and the outgoing cookies to see if
|
32
|
+
# the cookie we're looking for exists.
|
33
|
+
c = (self.all_cookies[key.to_s] || self.all_cookies[key.to_sym])
|
34
|
+
return c if c.is_a?(String)
|
35
|
+
return c[:value] if c.is_a?(Hash)
|
36
|
+
return nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# Set a cookie with a specified value.
|
40
|
+
def []=(key, value)
|
41
|
+
key = key.to_s
|
42
|
+
unless value.is_a?(Hash)
|
43
|
+
value = {:value => value}
|
44
|
+
end
|
45
|
+
value = app_config.mack.cookie_values.merge(value)
|
46
|
+
self.all_cookies[key] = value
|
47
|
+
self.response.set_cookie(key, value)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Deletes a cookie.
|
51
|
+
def delete(key)
|
52
|
+
key = key.to_s
|
53
|
+
self.all_cookies.delete(key)
|
54
|
+
self.response.delete_cookie(key)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns both cookies that came in as part of the request, as well as those set
|
58
|
+
# on to the response. This is useful when you set a cookie in a filter or an action
|
59
|
+
# and want to access it in another filter or action before the request/response has
|
60
|
+
# been fully completed.
|
61
|
+
def all
|
62
|
+
self.all_cookies
|
63
|
+
end
|
64
|
+
|
65
|
+
end # CookieJar
|
66
|
+
|
67
|
+
end # Mack
|
data/sea_level/filter.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
module Mack
|
2
|
+
module Controller
|
3
|
+
# A wrapper class to hold calls to filter methods for Controllers.
|
4
|
+
# This class should never be called by itself. Instead there are helper
|
5
|
+
# methods in Mack::Controller::Base to do this for.
|
6
|
+
#
|
7
|
+
# Examples:
|
8
|
+
# class MyAwesomeController < Mack::Controller::Base
|
9
|
+
# # all actions in this controller will have this filter run:
|
10
|
+
# before_filter: authenticate
|
11
|
+
# # only the show and index actions in this controller will have this filter run:
|
12
|
+
# before_filter: load_side_bar, :only => [:show, :index]
|
13
|
+
# # all actions, except for the create action will have this filter run.
|
14
|
+
# after_filter: write_to_log, :except => :create
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# Filter methods need to be scoped to the controller that is to run them.
|
18
|
+
# There are three different filters available: <tt>before</tt>, <tt>after</tt> and <tt>after_render</tt>.
|
19
|
+
#
|
20
|
+
# <tt>before</tt> filters get run before an action is called. This is a great place to set up common
|
21
|
+
# elements needed for your action. Things like authentication should be done here, etc...
|
22
|
+
#
|
23
|
+
# <tt>after</tt> filters get run after an action has been called. This is a great place to set up common
|
24
|
+
# elements for a view, that depend on stuff from inside your action. Because nothing has been 'rendered'
|
25
|
+
# yet, you still can add new instance variables, and alter ones created in the action.
|
26
|
+
#
|
27
|
+
# <tt>after_render</tt> filters get run after the rendering of the action has happened. At this point
|
28
|
+
# there is an instance variable, <tt>@final_rendered_action</tt>, that is available on which work can be done.
|
29
|
+
# This variable will have any layouts rendered to, any ERB will have been processed, etc... It should be the final
|
30
|
+
# String that will get rendered to the screen. This is a great place to do things like write a log, gzip, etc...
|
31
|
+
class Filter
|
32
|
+
|
33
|
+
attr_reader :filter_method
|
34
|
+
attr_reader :action_list
|
35
|
+
|
36
|
+
def initialize(filter_method, action_list = {})
|
37
|
+
@filter_method = filter_method
|
38
|
+
clean_action_list(action_list)
|
39
|
+
end
|
40
|
+
|
41
|
+
def run?(action)
|
42
|
+
return true if action_list.empty?
|
43
|
+
if action_list[:only]
|
44
|
+
return action_list[:only].include?(action)
|
45
|
+
elsif action_list[:except]
|
46
|
+
return !action_list[:except].include?(action)
|
47
|
+
end
|
48
|
+
return false
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def clean_action_list(action_list)
|
53
|
+
if action_list[:only]
|
54
|
+
action_list[:only] = [action_list[:only]].flatten
|
55
|
+
elsif action_list[:except]
|
56
|
+
action_list[:except] = [action_list[:except]].flatten
|
57
|
+
end
|
58
|
+
@action_list = action_list
|
59
|
+
end
|
60
|
+
|
61
|
+
end # Fitler
|
62
|
+
end # Controller
|
63
|
+
end # Mack
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Mack
|
2
|
+
module ViewHelpers
|
3
|
+
module HtmlHelpers
|
4
|
+
|
5
|
+
# This is just an alias to Mack::Utils::Html.
|
6
|
+
#
|
7
|
+
# Examples:
|
8
|
+
# <%= link_to("http://www.mackframework.com") %> # => <a href="http://www.mackframework.com">http://www.mackframework.com</a>
|
9
|
+
# <%= link_to("Mack", "http://www.mackframework.com") %> # => <a href="http://www.mackframework.com">Mack</a>
|
10
|
+
# <%= link_to("Mack", "http://www.mackframework.com", :target => "_blank") %> # => <a href="http://www.mackframework.com" target="_blank">Mack</a>
|
11
|
+
# <%= link_to("Mack", "http://www.mackframework.com", :target => "_blank", :rel => :nofollow) %> # => <a href="http://www.mackframework.com" target="_blank" rel="nofollow">Mack</a>
|
12
|
+
# If you pass in :method as an option it will be a JavaScript form that will post to the specified link with the
|
13
|
+
# methd specified.
|
14
|
+
# <%= link_to("Mack", "http://www.mackframework.com", :method => :delete) %>
|
15
|
+
# If you use the :method option you can also pass in a :confirm option. The :confirm option will generate a
|
16
|
+
# javascript confirmation window. If 'OK' is selected the the form will submit. If 'cancel' is selected, then
|
17
|
+
# nothing will happen. This is extremely useful for 'delete' type of links.
|
18
|
+
# <%= link_to("Mack", "http://www.mackframework.com", :method => :delete, :confirm => "Are you sure?") %>
|
19
|
+
def link_to(link_text, url = link_text, html_options = {})
|
20
|
+
Mack::Utils::Html.href(link_text, url, html_options)
|
21
|
+
end
|
22
|
+
|
23
|
+
# A wrapper method for views that calls out to Mack::Utils::Html.
|
24
|
+
#
|
25
|
+
# Examples:
|
26
|
+
# <%= html.b("hello") %> # => <b>hello</b>
|
27
|
+
def html
|
28
|
+
Mack::Utils::Html
|
29
|
+
end
|
30
|
+
|
31
|
+
end # HtmlHelpers
|
32
|
+
end # ViewHelpers
|
33
|
+
end # Mack
|
@@ -0,0 +1,72 @@
|
|
1
|
+
if using_activerecord?
|
2
|
+
class ActiveRecord::Base # :nodoc:
|
3
|
+
def business_display_name
|
4
|
+
self.class.name#.titlecase
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
if using_data_mapper?
|
10
|
+
class DataMapper::Base # :nodoc:
|
11
|
+
def business_display_name
|
12
|
+
self.class.name#.titlecase
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module Mack
|
18
|
+
module ViewHelpers
|
19
|
+
module OrmHelpers
|
20
|
+
DEFAULT_PARTIAL = %{
|
21
|
+
<div>
|
22
|
+
<div class="errorExplanation" id="errorExplanation">
|
23
|
+
<h2>Oi, there were errors! Fix `em!</h2>
|
24
|
+
<ul>
|
25
|
+
<% for error in errors %>
|
26
|
+
<li><%= error %></li>
|
27
|
+
<% end %>
|
28
|
+
</ul>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
}
|
32
|
+
|
33
|
+
def error_messages_for(object_names = [], view_partial = nil)
|
34
|
+
object_names = [object_names]
|
35
|
+
object_names.flatten!
|
36
|
+
app_errors = []
|
37
|
+
object_names.each do |name|
|
38
|
+
object = instance_variable_get("@#{name}")
|
39
|
+
if object
|
40
|
+
object.errors.each do |key, value|
|
41
|
+
if value.match(/^\^/)
|
42
|
+
app_errors << value[1..value.length]
|
43
|
+
else
|
44
|
+
if key.class == String and key == "base"
|
45
|
+
app_errors << "#{value}"
|
46
|
+
else
|
47
|
+
app_errors << "#{object.business_display_name} #{key.underscore.split('_').join(' ').humanize} #{value}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
File.join(MACK_VIEWS, "application", "_error_messages.html.erb")
|
54
|
+
unless app_errors.empty?
|
55
|
+
if view_partial.nil?
|
56
|
+
if File.exist?(File.join(MACK_VIEWS, "application", "_error_messages.html.erb"))
|
57
|
+
render :partial => "application/error_messages", :locals => {:errors => app_errors}
|
58
|
+
else
|
59
|
+
render :text => DEFAULT_PARTIAL, :locals => {:errors => app_errors}
|
60
|
+
end
|
61
|
+
else
|
62
|
+
render :partial => view_partial, :locals => {:errors => app_errors}
|
63
|
+
end
|
64
|
+
else
|
65
|
+
""
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# self.include_safely_into(Mack::ViewBinder)
|
70
|
+
end # OrmHelpers
|
71
|
+
end # ViewHelpers
|
72
|
+
end # Mack
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Mack
|
2
|
+
class Request < Rack::Request
|
3
|
+
|
4
|
+
def initialize(env) # :nodoc:
|
5
|
+
super(env)
|
6
|
+
@mack_params = {}
|
7
|
+
parse_params(rack_params)
|
8
|
+
end
|
9
|
+
|
10
|
+
alias_method :rack_params, :params # :nodoc:
|
11
|
+
|
12
|
+
# Returns all parameters associated with this request.
|
13
|
+
def all_params
|
14
|
+
@mack_params
|
15
|
+
end
|
16
|
+
|
17
|
+
# Merges another Hash with the parameters for this request.
|
18
|
+
def merge_params(opts = {})
|
19
|
+
parse_params(opts)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Gives access to the session. See Mack::Session for more information.
|
23
|
+
attr_accessor :session
|
24
|
+
|
25
|
+
# Examples:
|
26
|
+
# http://example.org
|
27
|
+
# https://example.org
|
28
|
+
# http://example.org:8080
|
29
|
+
def full_host
|
30
|
+
u = self.scheme.dup
|
31
|
+
u << "://"
|
32
|
+
u << self.host.dup
|
33
|
+
unless self.port == 80 || self.port == 443
|
34
|
+
u << ":#{self.port}"
|
35
|
+
end
|
36
|
+
u
|
37
|
+
end
|
38
|
+
|
39
|
+
# Examples:
|
40
|
+
# http://example.org:80
|
41
|
+
# https://example.org:443
|
42
|
+
# http://example.org:8080
|
43
|
+
def full_host_with_port
|
44
|
+
full_host << ":#{self.port}"
|
45
|
+
end
|
46
|
+
|
47
|
+
# Gives access to the request parameters. This includes 'get' parameters, 'post' parameters
|
48
|
+
# as well as parameters from the routing process. The parameter will also be 'unescaped'
|
49
|
+
# when it is returned.
|
50
|
+
#
|
51
|
+
# Example:
|
52
|
+
# uri: '/users/1?foo=bar'
|
53
|
+
# route: '/users/:id' => {:controller => 'users', :action => 'show'}
|
54
|
+
# parameters: {:controller => 'users', :action => 'show', :id => 1, :foo => "bar"}
|
55
|
+
def params(key)
|
56
|
+
p = (@mack_params[key.to_sym] || @mack_params[key.to_s])
|
57
|
+
unless p.nil?
|
58
|
+
p = p.to_s if p.is_a?(Symbol)
|
59
|
+
if p.is_a?(String)
|
60
|
+
p = Rack::Utils.unescape(p)
|
61
|
+
elsif p.is_a?(Hash)
|
62
|
+
p.each_pair {|k,v| p[k] = Rack::Utils.unescape(v)}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
p
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
def parse_params(ps)
|
70
|
+
ps.each_pair do |k, v|
|
71
|
+
if k.to_s.match(/.+\[.+\]/)
|
72
|
+
nv = k.to_s.match(/.+\[(.+)\]/).captures.first
|
73
|
+
nk = k.to_s.match(/(.+)\[.+\]/).captures.first
|
74
|
+
@mack_params[nk.to_sym] = {} if @mack_params[nk.to_sym].nil?
|
75
|
+
@mack_params[nk.to_sym].merge!(nv.to_sym => v)
|
76
|
+
else
|
77
|
+
@mack_params[k.to_sym] = v
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Mack
|
2
|
+
|
3
|
+
# A holder for the session information. This objects gets stored using the Cachetastic system.
|
4
|
+
# For more information about how Cachetastic works see the RDoc for that gem.
|
5
|
+
# The session cookie name defaults to: _mack_session_id but can be changed using the application_configuration
|
6
|
+
# system like such:
|
7
|
+
# mack::session_id: _my_cool_app_sess_id
|
8
|
+
class Session
|
9
|
+
|
10
|
+
attr_reader :id # The id of the session.
|
11
|
+
|
12
|
+
def initialize(id)
|
13
|
+
@id = id
|
14
|
+
@sess_hash = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Finds what you're looking for in the session, if it exists.
|
18
|
+
# If what you're looking for doesn't exist, it returns nil.
|
19
|
+
def [](key)
|
20
|
+
sess_hash[key.to_sym]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Sets a value into the session.
|
24
|
+
def []=(key, value)
|
25
|
+
sess_hash[key.to_sym] = value
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
attr_reader :sess_hash # :nodoc:
|
30
|
+
|
31
|
+
end # Session
|
32
|
+
|
33
|
+
end # Mack
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# require 'erubis'
|
2
|
+
# This class is used to do all the view level bindings.
|
3
|
+
# It allows for seperation between the controller and the view levels.
|
4
|
+
class Mack::ViewBinder
|
5
|
+
|
6
|
+
attr_accessor :controller # Allows access to the controller.
|
7
|
+
attr_accessor :options # Allows access to any options passed into the Binder.
|
8
|
+
|
9
|
+
def initialize(cont, opts = {})
|
10
|
+
self.controller = cont
|
11
|
+
self.options = {:locals => {}}.merge(opts)
|
12
|
+
transfer_vars(@controller)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the binding for this class.
|
16
|
+
def view_binding
|
17
|
+
binding
|
18
|
+
end
|
19
|
+
|
20
|
+
# If a method can not be found then the :locals key of
|
21
|
+
# the options is used to find the variable.
|
22
|
+
def method_missing(sym, *args)
|
23
|
+
self.options[:locals][sym]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Handles rendering calls both in the controller and in the view.
|
27
|
+
# For full details of render examples see Mack::Controller::Base render.
|
28
|
+
# Although the examples there are all in controllers, they idea is still
|
29
|
+
# the same for views.
|
30
|
+
#
|
31
|
+
# Examples in the view:
|
32
|
+
# <%= render(:text => "Hello") %>
|
33
|
+
# <%= render(:action => "show") %>
|
34
|
+
# <%= render(:partial => :latest_news) %>
|
35
|
+
def render(options = {})
|
36
|
+
if options[:action]
|
37
|
+
begin
|
38
|
+
# Try to render the action:
|
39
|
+
return render_file(options[:action], options)
|
40
|
+
rescue Errno::ENOENT => e
|
41
|
+
begin
|
42
|
+
# If the action doesn't exist on disk, try to render it from the public directory:
|
43
|
+
t = render_file(options[:action], {:dir => MACK_PUBLIC, :ext => ".html", :layout => false}.merge(options))
|
44
|
+
# Because it's being served from public don't wrap a layout around it!
|
45
|
+
# self.controller.instance_variable_get("@render_options").merge!({:layout => false})
|
46
|
+
return t
|
47
|
+
rescue Errno::ENOENT => ex
|
48
|
+
end
|
49
|
+
# Raise the original exception because something bad has happened!
|
50
|
+
raise e
|
51
|
+
end
|
52
|
+
elsif options[:text]
|
53
|
+
return Mack::ViewBinder.render(options[:text], self.controller, options)
|
54
|
+
elsif options[:partial]
|
55
|
+
return render_file(options[:partial], {:is_partial => true}.merge(options))
|
56
|
+
elsif options[:public]
|
57
|
+
t = render_file(options[:public], {:dir => MACK_PUBLIC, :ext => ".html", :layout => false}.merge(options))
|
58
|
+
# self.controller.instance_variable_get("@render_options").merge!({:layout => false})
|
59
|
+
return t
|
60
|
+
else
|
61
|
+
raise Mack::UnknownRenderOption.new(options)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def render_file(f, options = {})
|
67
|
+
options = {:is_partial => false, :ext => ".html.erb", :dir => MACK_VIEWS}.merge(options)
|
68
|
+
partial = f.to_s
|
69
|
+
parts = partial.split("/")
|
70
|
+
if parts.size == 1
|
71
|
+
# it's local to this controller
|
72
|
+
partial = "_" << partial if options[:is_partial]
|
73
|
+
partial = File.join(options[:dir], self.controller.controller_name, partial + options[:ext])
|
74
|
+
else
|
75
|
+
# it's elsewhere
|
76
|
+
parts[parts.size - 1] = "_" << parts.last if options[:is_partial]
|
77
|
+
partial = File.join(options[:dir], parts.join("/") + options[:ext])
|
78
|
+
end
|
79
|
+
return Mack::ViewBinder.render(File.open(partial).read, self.controller, options)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Transfer instance variables from the controller to the view.
|
83
|
+
def transfer_vars(x)
|
84
|
+
x.instance_variables.each do |v|
|
85
|
+
self.instance_variable_set(v, x.instance_variable_get(v))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class << self
|
90
|
+
|
91
|
+
# Creates a Mack::ViewBinder and then passes the io through ERB
|
92
|
+
# and returns a String. The io can be either an IO object or a String.
|
93
|
+
def render(io, controller, options = {})
|
94
|
+
vb = Mack::ViewBinder.new(controller, options)
|
95
|
+
return ERB.new(io).result(vb.view_binding)
|
96
|
+
# return Erubis::Eruby.new(io).result(vb.view_binding)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|