apipie-rails 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +3 -0
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/.travis.yml +5 -0
- data/APACHE-LICENSE-2.0 +202 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +115 -0
- data/MIT-LICENSE +20 -0
- data/NOTICE +4 -0
- data/README.rdoc +365 -0
- data/Rakefile +13 -0
- data/apipie-rails.gemspec +27 -0
- data/app/controllers/apipie/apipies_controller.rb +60 -0
- data/app/public/apipie/javascripts/apipie.js +6 -0
- data/app/public/apipie/javascripts/bundled/bootstrap-collapse.js +138 -0
- data/app/public/apipie/javascripts/bundled/bootstrap.js +1726 -0
- data/app/public/apipie/javascripts/bundled/jquery-1.7.2.js +9404 -0
- data/app/public/apipie/javascripts/bundled/prettify.js +28 -0
- data/app/public/apipie/stylesheets/application.css +7 -0
- data/app/public/apipie/stylesheets/bundled/bootstrap-responsive.min.css +12 -0
- data/app/public/apipie/stylesheets/bundled/bootstrap.min.css +689 -0
- data/app/public/apipie/stylesheets/bundled/prettify.css +30 -0
- data/app/views/apipie/apipies/_params.html.erb +22 -0
- data/app/views/apipie/apipies/_params_plain.html.erb +16 -0
- data/app/views/apipie/apipies/index.html.erb +36 -0
- data/app/views/apipie/apipies/method.html.erb +63 -0
- data/app/views/apipie/apipies/plain.html.erb +70 -0
- data/app/views/apipie/apipies/resource.html.erb +82 -0
- data/app/views/apipie/apipies/static.html.erb +101 -0
- data/app/views/layouts/apipie/apipie.html.erb +37 -0
- data/lib/apipie-rails.rb +12 -0
- data/lib/apipie/apipie_module.rb +105 -0
- data/lib/apipie/application.rb +225 -0
- data/lib/apipie/client/generator.rb +105 -0
- data/lib/apipie/client/template/Gemfile.tt +5 -0
- data/lib/apipie/client/template/README.tt +3 -0
- data/lib/apipie/client/template/base.rb.tt +33 -0
- data/lib/apipie/client/template/bin.rb.tt +110 -0
- data/lib/apipie/client/template/cli.rb.tt +25 -0
- data/lib/apipie/client/template/cli_command.rb.tt +129 -0
- data/lib/apipie/client/template/client.rb.tt +10 -0
- data/lib/apipie/client/template/resource.rb.tt +17 -0
- data/lib/apipie/dsl_definition.rb +139 -0
- data/lib/apipie/error_description.rb +21 -0
- data/lib/apipie/extractor.rb +143 -0
- data/lib/apipie/extractor/collector.rb +113 -0
- data/lib/apipie/extractor/recorder.rb +122 -0
- data/lib/apipie/extractor/writer.rb +356 -0
- data/lib/apipie/helpers.rb +24 -0
- data/lib/apipie/markup.rb +45 -0
- data/lib/apipie/method_description.rb +150 -0
- data/lib/apipie/param_description.rb +87 -0
- data/lib/apipie/railtie.rb +9 -0
- data/lib/apipie/resource_description.rb +83 -0
- data/lib/apipie/routing.rb +13 -0
- data/lib/apipie/static_dispatcher.rb +60 -0
- data/lib/apipie/validator.rb +292 -0
- data/lib/apipie/version.rb +3 -0
- data/lib/tasks/apipie.rake +156 -0
- data/rel-eng/packages/.readme +3 -0
- data/rel-eng/tito.props +5 -0
- data/rubygem-apipie-rails.spec +72 -0
- data/spec/controllers/apipies_controller_spec.rb +132 -0
- data/spec/controllers/users_controller_spec.rb +390 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +6 -0
- data/spec/dummy/app/controllers/twitter_example_controller.rb +302 -0
- data/spec/dummy/app/controllers/users_controller.rb +223 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +45 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +21 -0
- data/spec/dummy/config/environment.rb +8 -0
- data/spec/dummy/config/environments/development.rb +25 -0
- data/spec/dummy/config/environments/production.rb +49 -0
- data/spec/dummy/config/environments/test.rb +35 -0
- data/spec/dummy/config/initializers/apipie.rb +64 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +21 -0
- data/spec/dummy/doc/apipie_examples.yml +28 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +26 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/public/javascripts/application.js +2 -0
- data/spec/dummy/public/javascripts/controls.js +965 -0
- data/spec/dummy/public/javascripts/dragdrop.js +974 -0
- data/spec/dummy/public/javascripts/effects.js +1123 -0
- data/spec/dummy/public/javascripts/prototype.js +6001 -0
- data/spec/dummy/public/javascripts/rails.js +202 -0
- data/spec/dummy/public/stylesheets/.gitkeep +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/spec_helper.rb +32 -0
- metadata +312 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
module Apipie
|
2
|
+
module Helpers
|
3
|
+
def markup_to_html(text)
|
4
|
+
Apipie.configuration.markup.to_html(text.strip_heredoc)
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_accessor :url_prefix
|
8
|
+
|
9
|
+
def full_url(path)
|
10
|
+
unless @url_prefix
|
11
|
+
@url_prefix = ""
|
12
|
+
if rails_prefix = ENV["RAILS_RELATIVE_URL_ROOT"]
|
13
|
+
@url_prefix << rails_prefix
|
14
|
+
end
|
15
|
+
@url_prefix << Apipie.configuration.doc_base_url
|
16
|
+
end
|
17
|
+
path = path.sub(/^\//,"")
|
18
|
+
ret = "#{@url_prefix}/#{path}"
|
19
|
+
ret.insert(0,"/") unless ret =~ /\A[.\/]/
|
20
|
+
ret.sub!(/\/*\Z/,"")
|
21
|
+
ret
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Apipie
|
2
|
+
|
3
|
+
module Markup
|
4
|
+
|
5
|
+
class RDoc
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
require 'rdoc'
|
9
|
+
require 'rdoc/markup/to_html'
|
10
|
+
@rdoc ||= ::RDoc::Markup::ToHtml.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_html(text)
|
14
|
+
@rdoc.convert(text)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
class Markdown
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
require 'redcarpet'
|
23
|
+
@redcarpet ||= ::Redcarpet::Markdown.new(::Redcarpet::Render::HTML.new)
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_html(text)
|
27
|
+
@redcarpet.render(text)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
class Textile
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
require 'RedCloth'
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_html(text)
|
39
|
+
RedCloth.new(text).to_html
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'set'
|
2
|
+
module Apipie
|
3
|
+
|
4
|
+
class MethodDescription
|
5
|
+
|
6
|
+
class Api
|
7
|
+
|
8
|
+
attr_accessor :short_description, :api_url, :http_method
|
9
|
+
|
10
|
+
def initialize(method, path, desc)
|
11
|
+
@http_method = method.to_s
|
12
|
+
@api_url = create_api_url(path)
|
13
|
+
@short_description = desc
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def create_api_url(path)
|
19
|
+
"#{Apipie.configuration.api_base_url}#{path}"
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :errors, :full_description, :method, :resource, :apis, :examples, :see
|
25
|
+
|
26
|
+
def initialize(method, resource, app)
|
27
|
+
@method = method
|
28
|
+
@resource = resource
|
29
|
+
|
30
|
+
@apis = app.get_api_args
|
31
|
+
@see = app.get_see
|
32
|
+
|
33
|
+
desc = app.get_description || ''
|
34
|
+
@full_description = Apipie.markup_to_html(desc)
|
35
|
+
@errors = app.get_errors
|
36
|
+
@params_ordered = app.get_params
|
37
|
+
@examples = app.get_examples
|
38
|
+
|
39
|
+
@examples += load_recorded_examples
|
40
|
+
|
41
|
+
parent = @resource.controller.superclass
|
42
|
+
if parent != ActionController::Base
|
43
|
+
@parent_resource = parent.controller_name
|
44
|
+
end
|
45
|
+
@resource.add_method(id)
|
46
|
+
end
|
47
|
+
|
48
|
+
def id
|
49
|
+
"#{resource._id}##{method}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def params
|
53
|
+
params_ordered.reduce({}) { |h,p| h[p.name] = p; h }
|
54
|
+
end
|
55
|
+
|
56
|
+
def params_ordered
|
57
|
+
all_params = []
|
58
|
+
# get params from parent resource description
|
59
|
+
if @parent_resource
|
60
|
+
parent = Apipie.get_resource_description(@parent_resource)
|
61
|
+
merge_params(all_params, parent._params_ordered) if parent
|
62
|
+
end
|
63
|
+
|
64
|
+
# get params from actual resource description
|
65
|
+
if @resource
|
66
|
+
merge_params(all_params, resource._params_ordered)
|
67
|
+
end
|
68
|
+
|
69
|
+
merge_params(all_params, @params_ordered)
|
70
|
+
all_params.find_all(&:validator)
|
71
|
+
end
|
72
|
+
|
73
|
+
def doc_url
|
74
|
+
Apipie.full_url("#{@resource._id}/#{@method}")
|
75
|
+
end
|
76
|
+
|
77
|
+
def method_apis_to_json
|
78
|
+
@apis.each.collect do |api|
|
79
|
+
{
|
80
|
+
:api_url => api.api_url,
|
81
|
+
:http_method => api.http_method.to_s,
|
82
|
+
:short_description => api.short_description
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def see_url
|
88
|
+
if @see
|
89
|
+
method_description = Apipie[@see]
|
90
|
+
if method_description.nil?
|
91
|
+
raise ArgumentError.new("Method #{@see} referenced in 'see' does not exist.")
|
92
|
+
end
|
93
|
+
method_description.doc_url
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def see
|
98
|
+
@see
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_json
|
102
|
+
{
|
103
|
+
:doc_url => doc_url,
|
104
|
+
:name => @method,
|
105
|
+
:apis => method_apis_to_json,
|
106
|
+
:full_description => @full_description,
|
107
|
+
:errors => @errors,
|
108
|
+
:params => params_ordered.map(&:to_json).flatten,
|
109
|
+
:examples => @examples,
|
110
|
+
:see => @see,
|
111
|
+
:see_url => see_url
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def merge_params(params, new_params)
|
118
|
+
new_param_names = Set.new(new_params.map(&:name))
|
119
|
+
params.delete_if { |p| new_param_names.include?(p.name) }
|
120
|
+
params.concat(new_params)
|
121
|
+
end
|
122
|
+
|
123
|
+
def load_recorded_examples
|
124
|
+
(Apipie.recorded_examples[id] || []).
|
125
|
+
find_all { |ex| ex["show_in_doc"].to_i > 0 }.
|
126
|
+
sort_by { |ex| ex["show_in_doc"] }.
|
127
|
+
map { |ex| format_example(ex.symbolize_keys) }
|
128
|
+
end
|
129
|
+
|
130
|
+
def format_example_data(data)
|
131
|
+
case data
|
132
|
+
when Array, Hash
|
133
|
+
JSON.pretty_generate(data).gsub(/: \[\s*\]/,": []").gsub(/\{\s*\}/,"{}")
|
134
|
+
else
|
135
|
+
data
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def format_example(ex)
|
140
|
+
example = "#{ex[:verb]} #{ex[:path]}"
|
141
|
+
example << "?#{ex[:query]}" unless ex[:query].blank?
|
142
|
+
example << "\n" << format_example_data(ex[:request_data]).to_s if ex[:request_data]
|
143
|
+
example << "\n" << ex[:code].to_s
|
144
|
+
example << "\n" << format_example_data(ex[:response_data]).to_s if ex[:response_data]
|
145
|
+
example
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Apipie
|
2
|
+
|
3
|
+
# method parameter description
|
4
|
+
#
|
5
|
+
# name - method name (show)
|
6
|
+
# desc - description
|
7
|
+
# required - boolean if required
|
8
|
+
# validator - Validator::BaseValidator subclass
|
9
|
+
class ParamDescription
|
10
|
+
|
11
|
+
attr_reader :name, :desc, :required, :allow_nil, :validator
|
12
|
+
|
13
|
+
attr_accessor :parent
|
14
|
+
|
15
|
+
def initialize(name, *args, &block)
|
16
|
+
|
17
|
+
if args.size > 1 || !args.first.is_a?(Hash)
|
18
|
+
validator_type = args.shift || nil
|
19
|
+
else
|
20
|
+
validator_type = nil
|
21
|
+
end
|
22
|
+
options = args.pop || {}
|
23
|
+
|
24
|
+
@name = name
|
25
|
+
@desc = Apipie.markup_to_html(options[:desc] || '')
|
26
|
+
@required = options[:required] || false
|
27
|
+
@allow_nil = options[:allow_nil] || false
|
28
|
+
|
29
|
+
@validator = nil
|
30
|
+
unless validator_type.nil?
|
31
|
+
@validator =
|
32
|
+
Validator::BaseValidator.find(self, validator_type, options, block)
|
33
|
+
raise "Validator not found." unless validator
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate(value)
|
38
|
+
return true if @allow_nil && value.nil?
|
39
|
+
unless @validator.valid?(value)
|
40
|
+
raise ArgumentError.new(@validator.error)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def full_name
|
45
|
+
name_parts = parents_and_self.map(&:name)
|
46
|
+
return ([name_parts.first] + name_parts[1..-1].map { |n| "[#{n}]" }).join("")
|
47
|
+
end
|
48
|
+
|
49
|
+
# returns an array of all the parents: starting with the root parent
|
50
|
+
# ending with itself
|
51
|
+
def parents_and_self
|
52
|
+
ret = []
|
53
|
+
if self.parent
|
54
|
+
ret.concat(self.parent.parents_and_self)
|
55
|
+
end
|
56
|
+
ret << self
|
57
|
+
ret
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_json
|
61
|
+
if validator.is_a? Apipie::Validator::HashValidator
|
62
|
+
{
|
63
|
+
:name => name.to_s,
|
64
|
+
:full_name => full_name,
|
65
|
+
:description => desc,
|
66
|
+
:required => required,
|
67
|
+
:allow_nil => allow_nil,
|
68
|
+
:validator => validator.to_s,
|
69
|
+
:expected_type => validator.expected_type,
|
70
|
+
:params => validator.hash_params_ordered.map(&:to_json)
|
71
|
+
}
|
72
|
+
else
|
73
|
+
{
|
74
|
+
:name => name.to_s,
|
75
|
+
:full_name => full_name,
|
76
|
+
:description => desc,
|
77
|
+
:required => required,
|
78
|
+
:allow_nil => allow_nil,
|
79
|
+
:validator => validator.to_s,
|
80
|
+
:expected_type => validator.expected_type
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Apipie
|
2
|
+
|
3
|
+
# Resource description
|
4
|
+
#
|
5
|
+
# version - api version (1)
|
6
|
+
# description
|
7
|
+
# path - relative path (/api/articles)
|
8
|
+
# methods - array of keys to Apipie.method_descriptions (array of Apipie::MethodDescription)
|
9
|
+
# name - human readable alias of resource (Articles)
|
10
|
+
# id - resouce name
|
11
|
+
class ResourceDescription
|
12
|
+
|
13
|
+
attr_reader :controller, :_short_description, :_full_description, :_methods, :_id,
|
14
|
+
:_path, :_version, :_name, :_params_ordered
|
15
|
+
|
16
|
+
def initialize(controller, resource_name, &block)
|
17
|
+
@_methods = []
|
18
|
+
@_params_ordered = []
|
19
|
+
|
20
|
+
@controller = controller
|
21
|
+
@_id = resource_name
|
22
|
+
@_version = "1"
|
23
|
+
@_name = @_id.humanize
|
24
|
+
@_full_description = ""
|
25
|
+
@_short_description = ""
|
26
|
+
@_path = ""
|
27
|
+
|
28
|
+
block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
|
29
|
+
end
|
30
|
+
|
31
|
+
def param(param_name, *args, &block)
|
32
|
+
param_description = Apipie::ParamDescription.new(param_name, *args, &block)
|
33
|
+
@_params_ordered << param_description
|
34
|
+
end
|
35
|
+
|
36
|
+
def path(path); @_path = path; end
|
37
|
+
|
38
|
+
def version(version); @_version = version; end
|
39
|
+
|
40
|
+
def name(name); @_name = name; end
|
41
|
+
|
42
|
+
def short(short); @_short_description = short; end
|
43
|
+
alias :short_description :short
|
44
|
+
|
45
|
+
def desc(description)
|
46
|
+
description ||= ''
|
47
|
+
@_full_description = Apipie.markup_to_html(description)
|
48
|
+
end
|
49
|
+
alias :description :desc
|
50
|
+
alias :full_description :desc
|
51
|
+
|
52
|
+
# add description of resource method
|
53
|
+
def add_method(mapi_key)
|
54
|
+
@_methods << mapi_key
|
55
|
+
@_methods.uniq!
|
56
|
+
end
|
57
|
+
|
58
|
+
def doc_url
|
59
|
+
Apipie.full_url(@_id)
|
60
|
+
end
|
61
|
+
|
62
|
+
def api_url; "#{Apipie.configuration.api_base_url}#{@_path}"; end
|
63
|
+
|
64
|
+
def to_json(method_name = nil)
|
65
|
+
|
66
|
+
_methods = if method_name.blank?
|
67
|
+
@_methods.collect { |key| Apipie.method_descriptions[key].to_json }
|
68
|
+
else
|
69
|
+
[Apipie.method_descriptions[[@_id, method_name].join('#')].to_json]
|
70
|
+
end
|
71
|
+
|
72
|
+
{
|
73
|
+
:doc_url => doc_url,
|
74
|
+
:api_url => api_url,
|
75
|
+
:name => @_name,
|
76
|
+
:short_description => @_short_description,
|
77
|
+
:full_description => @_full_description,
|
78
|
+
:version => @_version,
|
79
|
+
:methods => _methods
|
80
|
+
}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Apipie
|
2
|
+
module Routing
|
3
|
+
module MapperExtensions
|
4
|
+
def apipie
|
5
|
+
namespace "apipie", :path => Apipie.configuration.doc_base_url do
|
6
|
+
get("(:resource)/(:method)" => "apipies#index" )
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
ActionDispatch::Routing::Mapper.send :include, Apipie::Routing::MapperExtensions
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Apipie
|
2
|
+
|
3
|
+
class FileHandler
|
4
|
+
def initialize(root)
|
5
|
+
@root = root.chomp('/')
|
6
|
+
@compiled_root = /^#{Regexp.escape(root)}/
|
7
|
+
@file_server = ::Rack::File.new(@root)
|
8
|
+
end
|
9
|
+
|
10
|
+
def match?(path)
|
11
|
+
path = path.dup
|
12
|
+
|
13
|
+
full_path = path.empty? ? @root : File.join(@root, ::Rack::Utils.unescape(path))
|
14
|
+
paths = "#{full_path}#{ext}"
|
15
|
+
|
16
|
+
matches = Dir[paths]
|
17
|
+
match = matches.detect { |m| File.file?(m) }
|
18
|
+
if match
|
19
|
+
match.sub!(@compiled_root, '')
|
20
|
+
match
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(env)
|
25
|
+
@file_server.call(env)
|
26
|
+
end
|
27
|
+
|
28
|
+
def ext
|
29
|
+
@ext ||= begin
|
30
|
+
ext = ::ActionController::Base.page_cache_extension
|
31
|
+
"{,#{ext},/index#{ext}}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class StaticDispatcher
|
37
|
+
# Dispatches the statis files. Simillar to ActionDispatch::Static, but
|
38
|
+
# it supports different baseurl configurations
|
39
|
+
def initialize(app, path, baseurl)
|
40
|
+
@app = app
|
41
|
+
@baseurl = baseurl
|
42
|
+
@file_handler = Apipie::FileHandler.new(path)
|
43
|
+
end
|
44
|
+
|
45
|
+
def call(env)
|
46
|
+
case env['REQUEST_METHOD']
|
47
|
+
when 'GET', 'HEAD'
|
48
|
+
path = env['PATH_INFO'].sub("#{@baseurl}/","/apipie/").chomp('/')
|
49
|
+
path.sub!("#{ENV["RAILS_RELATIVE_URL_ROOT"]}",'')
|
50
|
+
|
51
|
+
if match = @file_handler.match?(path)
|
52
|
+
env["PATH_INFO"] = match
|
53
|
+
return @file_handler.call(env)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
@app.call(env)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|