tap-server 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/History +5 -0
- data/MIT-LICENSE +19 -0
- data/README +49 -0
- data/cmd/server.rb +34 -0
- data/controllers/app_controller.rb +92 -0
- data/controllers/schema_controller.rb +255 -0
- data/lib/tap/controller.rb +231 -0
- data/lib/tap/server.rb +219 -0
- data/lib/tap/server_error.rb +34 -0
- data/lib/tap/tasks/echo.rb +14 -0
- data/lib/tap/tasks/server.rb +30 -0
- data/public/javascripts/prototype.js +4221 -0
- data/public/javascripts/tap.js +112 -0
- data/public/stylesheets/tap.css +6 -0
- data/tap.yml +0 -0
- data/views/404.erb +9 -0
- data/views/500.erb +7 -0
- data/views/app_controller/index.erb +44 -0
- data/views/app_controller/info.erb +8 -0
- data/views/app_controller/tail.erb +8 -0
- data/views/layout.erb +11 -0
- data/views/schema_controller/config/default.erb +4 -0
- data/views/schema_controller/config/flag.erb +3 -0
- data/views/schema_controller/config/switch.erb +6 -0
- data/views/schema_controller/configurations.erb +27 -0
- data/views/schema_controller/join.erb +4 -0
- data/views/schema_controller/node.erb +47 -0
- data/views/schema_controller/preview.erb +3 -0
- data/views/schema_controller/round.erb +30 -0
- data/views/schema_controller/schema.erb +28 -0
- data/views/tap/tasks/echo/result.html +1 -0
- metadata +109 -0
data/History
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2009, Regents of the University of Colorado.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
4
|
+
software and associated documentation files (the "Software"), to deal in the Software
|
5
|
+
without restriction, including without limitation the rights to use, copy, modify, merge,
|
6
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
7
|
+
to whom the Software is furnished to do so, subject to the following conditions:
|
8
|
+
|
9
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
10
|
+
substantial portions of the Software.
|
11
|
+
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
13
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
14
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
15
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
16
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
17
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
18
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
19
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
= {Tap Server}[http://tap.rubyforge.org/tap-server]
|
2
|
+
|
3
|
+
A web interface for {Tap}[http://tap.rubyforge.org/rdoc].
|
4
|
+
|
5
|
+
== Description
|
6
|
+
|
7
|
+
{Tap Server}[http://tap.rubyforge.org/tap-server] provides a web interface for
|
8
|
+
Tap tasks. The basic interface allows the construction and execution of
|
9
|
+
workflows, rendering of task results, and facilitates distributable
|
10
|
+
controllers. The intention is not to make websites for the masses, but rather
|
11
|
+
a local interface for individuals and small groups.
|
12
|
+
|
13
|
+
{Tap Server}[http://tap.rubyforge.org/tap-server] is a part of the
|
14
|
+
{Tap-Suite}[http://tap.rubyforge.org/tap-suite]. Check out these links for
|
15
|
+
documentation, development, and bug tracking.
|
16
|
+
|
17
|
+
* Website[http://tap.rubyforge.org]
|
18
|
+
* Lighthouse[http://bahuvrihi.lighthouseapp.com/projects/9908-tap-task-application/tickets]
|
19
|
+
* Github[http://github.com/bahuvrihi/tap/tree/master]
|
20
|
+
* {Google Group}[http://groups.google.com/group/ruby-on-tap]
|
21
|
+
|
22
|
+
== Usage
|
23
|
+
|
24
|
+
To get a peek, use the command:
|
25
|
+
|
26
|
+
% tap server
|
27
|
+
|
28
|
+
Then go to the url 'localhost:8080'. Currently tap-server is in alpha and
|
29
|
+
should not be considered stable.
|
30
|
+
|
31
|
+
== Installation
|
32
|
+
|
33
|
+
Tap Server is available as a gem on RubyForge[http://rubyforge.org/projects/tap]. Use:
|
34
|
+
|
35
|
+
% gem install tap-server
|
36
|
+
|
37
|
+
Tap requires an updated version of RubyGems[http://docs.rubygems.org/]
|
38
|
+
(>= 1.2.0). To check the version and update RubyGems:
|
39
|
+
|
40
|
+
% gem --version
|
41
|
+
% gem --update system
|
42
|
+
|
43
|
+
== Info
|
44
|
+
|
45
|
+
Copyright (c) 2009, Regents of the University of Colorado.
|
46
|
+
Developer:: {Simon Chiang}[http://bahuvrihi.wordpress.com], {Biomolecular Structure Program}[http://biomol.uchsc.edu/], {Hansen Lab}[http://hsc-proteomics.uchsc.edu/hansenlab/]
|
47
|
+
Support:: CU Denver School of Medicine Deans Academic Enrichment Fund
|
48
|
+
Licence:: {MIT-Style}[link:files/MIT-LICENSE.html]
|
49
|
+
|
data/cmd/server.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# tap server {options}
|
2
|
+
#
|
3
|
+
# Initializes a tap server.
|
4
|
+
|
5
|
+
require 'tap'
|
6
|
+
require 'tap/server'
|
7
|
+
|
8
|
+
env = Tap::Env.instance
|
9
|
+
app = Tap::App.instance
|
10
|
+
|
11
|
+
#
|
12
|
+
# handle options
|
13
|
+
#
|
14
|
+
|
15
|
+
config_path = nil
|
16
|
+
opts = ConfigParser.new do |opts|
|
17
|
+
|
18
|
+
opts.separator ""
|
19
|
+
opts.separator "options:"
|
20
|
+
opts.add(Tap::Server.configurations)
|
21
|
+
|
22
|
+
# add option to print help
|
23
|
+
opts.on("-h", "--help", "Show this message") do
|
24
|
+
puts Lazydoc.usage(__FILE__)
|
25
|
+
puts opts
|
26
|
+
exit
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# parse!
|
31
|
+
argv = opts.parse(ARGV)
|
32
|
+
server = Tap::Server.new(env, app, opts.config)
|
33
|
+
cookie_server = Rack::Session::Pool.new(server)
|
34
|
+
Rack::Handler::WEBrick.run(cookie_server, :Port => server.port)
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'tap/controller'
|
2
|
+
require 'rack/mime'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
class AppController < Tap::Controller
|
6
|
+
set :default_layout, 'layout.erb'
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
# serve public files before actions
|
10
|
+
server = env['tap.server'] ||= Tap::Server.new
|
11
|
+
|
12
|
+
if path = server.public_path(env['PATH_INFO'])
|
13
|
+
content = File.read(path)
|
14
|
+
headers = {
|
15
|
+
"Last-Modified" => [File.mtime(path).httpdate],
|
16
|
+
"Content-Type" => [Rack::Mime.mime_type(File.extname(path), 'text/plain')],
|
17
|
+
"Content-Length" => [content.size.to_s]
|
18
|
+
}
|
19
|
+
|
20
|
+
[200, headers, [content]]
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def index
|
27
|
+
env_names = {}
|
28
|
+
server.env.minimap.each do |name, environment|
|
29
|
+
env_names[environment] = name
|
30
|
+
end
|
31
|
+
|
32
|
+
render('index.erb', :locals => {:env => server.env, :env_names => env_names}, :layout => true)
|
33
|
+
end
|
34
|
+
|
35
|
+
def info
|
36
|
+
if request.post?
|
37
|
+
app.info
|
38
|
+
else
|
39
|
+
render('info.erb', :locals => {:update => true, :content => app.info}, :layout => true)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
#--
|
44
|
+
# Currently tail is hard-coded to tail the server log only.
|
45
|
+
def tail(id=nil)
|
46
|
+
begin
|
47
|
+
path = app.subpath(:log, 'server.log')
|
48
|
+
raise unless File.exists?(path)
|
49
|
+
rescue
|
50
|
+
raise Tap::ServerError.new("invalid path", 404)
|
51
|
+
end
|
52
|
+
|
53
|
+
pos = request['pos'].to_i
|
54
|
+
if pos > File.size(path)
|
55
|
+
raise Tap::ServerError.new("tail position out of range (try update)", 500)
|
56
|
+
end
|
57
|
+
|
58
|
+
content = File.open(path) do |file|
|
59
|
+
file.pos = pos
|
60
|
+
file.read
|
61
|
+
end
|
62
|
+
|
63
|
+
if request.post?
|
64
|
+
content
|
65
|
+
else
|
66
|
+
render('tail.erb', :locals => {
|
67
|
+
:id => id,
|
68
|
+
:path => File.basename(path),
|
69
|
+
:update => true,
|
70
|
+
:content => content
|
71
|
+
}, :layout => true)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def run
|
76
|
+
Thread.new { app.run }
|
77
|
+
redirect("/app/tail")
|
78
|
+
end
|
79
|
+
|
80
|
+
def stop
|
81
|
+
app.stop
|
82
|
+
redirect("/app/info")
|
83
|
+
end
|
84
|
+
|
85
|
+
def terminate
|
86
|
+
app.terminate
|
87
|
+
redirect("/app/info")
|
88
|
+
end
|
89
|
+
|
90
|
+
def help(key=nil)
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,255 @@
|
|
1
|
+
require 'tap/controller'
|
2
|
+
|
3
|
+
class SchemaController < Tap::Controller
|
4
|
+
set :default_layout, 'layout.erb'
|
5
|
+
|
6
|
+
# Initializes a new schema and redirects to display.
|
7
|
+
def index
|
8
|
+
id = initialize_schema
|
9
|
+
redirect("/schema/display/#{id}")
|
10
|
+
end
|
11
|
+
|
12
|
+
# Loads the schema indicated by id and renders 'schema.erb' with the default
|
13
|
+
# layout.
|
14
|
+
def display(id)
|
15
|
+
schema = load_schema(id)
|
16
|
+
render 'schema.erb', :locals => {
|
17
|
+
:id => id,
|
18
|
+
:schema => schema
|
19
|
+
}, :layout => true
|
20
|
+
end
|
21
|
+
|
22
|
+
# Updates the specified schema with the request parameters. Update forwards
|
23
|
+
# the request to the action ('add' or 'remove') specified in the action
|
24
|
+
# parameter.
|
25
|
+
def update(id)
|
26
|
+
case request['action']
|
27
|
+
when 'add' then add(id)
|
28
|
+
when 'remove' then remove(id)
|
29
|
+
when 'echo' then echo
|
30
|
+
else raise Tap::ServerError, "unknown action: #{request['action']}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Adds nodes or joins to the schema. Parameters:
|
35
|
+
#
|
36
|
+
# nodes[]:: An array of nodes to add to the schema. Each entry is split using
|
37
|
+
# Shellwords to yield an argv; the argv initializes the node. The
|
38
|
+
# index of each new node is added to targets[].
|
39
|
+
# sources[]:: An array of source node indicies used to create a join.
|
40
|
+
# targets[]:: An array of target node indicies used to create a join (note
|
41
|
+
# the indicies of new nodes are added to targets).
|
42
|
+
#
|
43
|
+
# Add creates and pushes new nodes onto schema as specified in nodes, then
|
44
|
+
# creates joins between the sources and targets. The join class is inferred
|
45
|
+
# by Utils.infer_join; if no join can be inferred the join class is
|
46
|
+
# effectively nil, and consistent with that, the node output for sources
|
47
|
+
# and the node input for targets is set to nil.
|
48
|
+
#
|
49
|
+
# === Notes
|
50
|
+
#
|
51
|
+
# The nomenclature for source and target is relative to the join, and may
|
52
|
+
# seem backwards for the node (ex: 'sources[]=0&targets[]=1' makes a join
|
53
|
+
# like '0:1')
|
54
|
+
#
|
55
|
+
def add(id)
|
56
|
+
unless request.post?
|
57
|
+
raise Tap::ServerError, "add must be performed with post"
|
58
|
+
end
|
59
|
+
|
60
|
+
round = (request['round'] || 0).to_i
|
61
|
+
outputs = (request['outputs[]'] || []).collect {|index| index.to_i }
|
62
|
+
inputs = (request['inputs[]'] || []).collect {|index| index.to_i }
|
63
|
+
nodes = request['nodes[]'] || []
|
64
|
+
|
65
|
+
load_schema(id) do |schema|
|
66
|
+
nodes.each do |arg|
|
67
|
+
next unless arg && !arg.empty?
|
68
|
+
|
69
|
+
outputs << schema.nodes.length
|
70
|
+
schema.nodes << Tap::Support::Node.new(Shellwords.shellwords(arg), round)
|
71
|
+
end
|
72
|
+
|
73
|
+
if inputs.empty? || outputs.empty?
|
74
|
+
inputs.each {|index| schema[index].output = nil }
|
75
|
+
outputs.each {|index| schema[index].input = round }
|
76
|
+
else
|
77
|
+
|
78
|
+
# temporary
|
79
|
+
if inputs.length > 1 && outputs.length > 1
|
80
|
+
raise "multi-way join specified"
|
81
|
+
end
|
82
|
+
|
83
|
+
schema.set(Tap::Support::Join, inputs, outputs)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
redirect("/schema/display/#{id}")
|
88
|
+
end
|
89
|
+
|
90
|
+
# Removes nodes or joins from the schema. Parameters:
|
91
|
+
#
|
92
|
+
# sources[]:: An array of source node indicies to remove.
|
93
|
+
# targets[]:: An array of target node indicies to remove.
|
94
|
+
#
|
95
|
+
# Normally remove sets the node.output for each source to nil and the
|
96
|
+
# node.input for each target to nil. However, if a node is indicated in
|
97
|
+
# both sources and targets AND it has no join input/output, then it will
|
98
|
+
# be removed.
|
99
|
+
#
|
100
|
+
# === Notes
|
101
|
+
#
|
102
|
+
# The nomenclature for source and target is relative to the join, and may
|
103
|
+
# seem backwards for the node (ex: for the sequence '0:1:2', 'targets[]=1'
|
104
|
+
# breaks the join '0:1' while 'sources[]=1' breaks the join '1:2'.
|
105
|
+
#
|
106
|
+
def remove(id)
|
107
|
+
unless request.post?
|
108
|
+
raise Tap::ServerError, "remove must be performed with post"
|
109
|
+
end
|
110
|
+
|
111
|
+
round = (request['round'] || 0).to_i
|
112
|
+
outputs = (request['outputs[]'] || []).collect {|index| index.to_i }
|
113
|
+
inputs = (request['inputs[]'] || []).collect {|index| index.to_i }
|
114
|
+
|
115
|
+
load_schema(id) do |schema|
|
116
|
+
# Remove joins. Removed indicies are popped to ensure
|
117
|
+
# that if a join was removed the node will not be.
|
118
|
+
outputs.delete_if do |index|
|
119
|
+
next unless node = schema.nodes[index]
|
120
|
+
if node.input_join
|
121
|
+
node.input = round
|
122
|
+
true
|
123
|
+
else
|
124
|
+
false
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
inputs.delete_if do |index|
|
129
|
+
next unless node = schema.nodes[index]
|
130
|
+
if node.output_join
|
131
|
+
node.output = nil
|
132
|
+
true
|
133
|
+
else
|
134
|
+
false
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Remove nodes. Setting a node to nil causes it's removal during
|
139
|
+
# compact; orphaned joins are removed during compact as well.
|
140
|
+
(inputs & outputs).each do |index|
|
141
|
+
schema.nodes[index] = nil
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
redirect("/schema/display/#{id}")
|
146
|
+
end
|
147
|
+
|
148
|
+
def submit(id)
|
149
|
+
case request['action']
|
150
|
+
when 'save' then save(id)
|
151
|
+
when 'preview' then preview(id)
|
152
|
+
when 'echo' then echo
|
153
|
+
when 'run'
|
154
|
+
dump_schema(id, schema)
|
155
|
+
run(id)
|
156
|
+
else raise Tap::ServerError, "unknown action: #{request['action']}"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def save(id)
|
161
|
+
unless request.post?
|
162
|
+
raise Tap::ServerError, "submit must be performed with post"
|
163
|
+
end
|
164
|
+
|
165
|
+
dump_schema(id, schema)
|
166
|
+
redirect("/schema/display/#{id}")
|
167
|
+
end
|
168
|
+
|
169
|
+
def preview(id)
|
170
|
+
response.headers['Content-Type'] = 'text/plain'
|
171
|
+
render('preview.erb', :locals => {:id => id, :schema => schema})
|
172
|
+
end
|
173
|
+
|
174
|
+
def run(id)
|
175
|
+
unless request.post?
|
176
|
+
raise Tap::ServerError, "run must be performed with post"
|
177
|
+
end
|
178
|
+
|
179
|
+
# it would be nice to someday put all this on a separate thread...
|
180
|
+
schema = load_schema(id)
|
181
|
+
tasks = server.env.tasks
|
182
|
+
schema.build(app) do |(key, *args)|
|
183
|
+
if const = tasks.search(key)
|
184
|
+
const.constantize.parse(args, app) do |help|
|
185
|
+
raise "help not implemented"
|
186
|
+
#redirect("/app/help/#{key}")
|
187
|
+
end
|
188
|
+
else
|
189
|
+
raise ArgumentError, "unknown task: #{key}"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
Thread.new { app.run }
|
194
|
+
redirect("/app/tail")
|
195
|
+
end
|
196
|
+
|
197
|
+
protected
|
198
|
+
|
199
|
+
# Parses a Tap::Support::Schema from the request.
|
200
|
+
def schema
|
201
|
+
argv = request['argv[]'] || []
|
202
|
+
argv.delete_if {|arg| arg.empty? }
|
203
|
+
Tap::Support::Schema.parse(argv)
|
204
|
+
end
|
205
|
+
|
206
|
+
def initialize_schema
|
207
|
+
current = app.glob(:schema, "*").collect {|path| File.basename(path).chomp(".yml") }
|
208
|
+
|
209
|
+
id = random_key(current.length)
|
210
|
+
while current.include?(id)
|
211
|
+
id = random_key(current.length)
|
212
|
+
end
|
213
|
+
|
214
|
+
dump_schema(id, schema)
|
215
|
+
id
|
216
|
+
end
|
217
|
+
|
218
|
+
def load_schema(id)
|
219
|
+
unless path = app.filepath(:schema, "#{id}.yml")
|
220
|
+
raise ServerError, "no schema for id: #{id}"
|
221
|
+
end
|
222
|
+
schema = Tap::Support::Schema.load_file(path)
|
223
|
+
|
224
|
+
if block_given?
|
225
|
+
result = yield(schema)
|
226
|
+
dump_schema(id, schema)
|
227
|
+
result
|
228
|
+
else
|
229
|
+
schema
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def dump_schema(id, schema=nil)
|
234
|
+
app.prepare(:schema, "#{id}.yml") do |file|
|
235
|
+
file << schema.dump.to_yaml if schema
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def instantiate(*argv)
|
240
|
+
key = argv.shift
|
241
|
+
tasc = server.env.tasks.search(key).constantize
|
242
|
+
tasc.parse(argv)
|
243
|
+
end
|
244
|
+
|
245
|
+
# helper to echo requests back... good for debugging
|
246
|
+
def echo # :nodoc:
|
247
|
+
"<pre>#{request.params.to_yaml}</pre>"
|
248
|
+
end
|
249
|
+
|
250
|
+
# Generates a random integer key.
|
251
|
+
def random_key(length) # :nodoc:
|
252
|
+
length = 1 if length < 1
|
253
|
+
rand(length * 10000).to_s
|
254
|
+
end
|
255
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'tap/server'
|
2
|
+
autoload(:ERB, 'erb')
|
3
|
+
|
4
|
+
module Tap
|
5
|
+
|
6
|
+
# === Declaring Actions
|
7
|
+
# By default all public methods in subclasses are declared as actions. You
|
8
|
+
# can declare a private or protected method as an action by:
|
9
|
+
#
|
10
|
+
# * manually adding it directly to actions
|
11
|
+
# * defining it as a public method and then call private(:method) or protected(:method)
|
12
|
+
#
|
13
|
+
# Similarly, public method can be made non-action by actions by:
|
14
|
+
#
|
15
|
+
# * manually deleting it from actions
|
16
|
+
# * define it private or protected then call public(:method)
|
17
|
+
#
|
18
|
+
class Controller
|
19
|
+
class << self
|
20
|
+
|
21
|
+
# Initialize instance variables on the child and inherit as necessary.
|
22
|
+
def inherited(child) # :nodoc:
|
23
|
+
super
|
24
|
+
child.set(:actions, actions.dup)
|
25
|
+
child.set(:middleware, middleware.dup)
|
26
|
+
child.set(:default_layout, default_layout)
|
27
|
+
child.set(:define_action, true)
|
28
|
+
end
|
29
|
+
|
30
|
+
# An array of methods that can be called as actions. Actions must be
|
31
|
+
# stored as symbols. Actions are inherited.
|
32
|
+
attr_reader :actions
|
33
|
+
|
34
|
+
# An array of Rack middleware that will be applied when handing requests
|
35
|
+
# through the class call method. Middleware is inherited.
|
36
|
+
attr_reader :middleware
|
37
|
+
|
38
|
+
# The default layout rendered when the render option :layout is true.
|
39
|
+
attr_reader :default_layout
|
40
|
+
|
41
|
+
# The base path prepended to render paths (ie render(<path>) renders
|
42
|
+
# <templates_dir/name/path>).
|
43
|
+
def name
|
44
|
+
@name ||= to_s.underscore
|
45
|
+
end
|
46
|
+
|
47
|
+
# Adds the specified middleware. Middleware classes are initialized
|
48
|
+
# with the specified args and block, and applied to in the order in
|
49
|
+
# which they are declared (ie first use processes requests first).
|
50
|
+
#
|
51
|
+
# Middleware is applied through the class call method, and on a per-call
|
52
|
+
# basis... middleware like Rack::Session::Pool that is supposed to
|
53
|
+
# persist for the life of an application will not work properly.
|
54
|
+
#
|
55
|
+
# Middleware is inherited.
|
56
|
+
def use(middleware, *args, &block)
|
57
|
+
@middleware << [middleware, args, block]
|
58
|
+
end
|
59
|
+
|
60
|
+
# Instantiates self and performs call. Middleware is applied in the
|
61
|
+
# order in which it was declared.
|
62
|
+
#--
|
63
|
+
# Note that middleware needs to be initialized in reverese, so that
|
64
|
+
# the first declared middleware runs first.
|
65
|
+
def call(env)
|
66
|
+
app = new
|
67
|
+
middleware.reverse_each do |(m, args, block)|
|
68
|
+
app = m.new(app, *args, &block)
|
69
|
+
end
|
70
|
+
app.call(env)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Sets an instance variable for self, short for:
|
74
|
+
#
|
75
|
+
# instance_variable_set(:@attribute, input)
|
76
|
+
#
|
77
|
+
# Typically only these variables should be set:
|
78
|
+
#
|
79
|
+
# actions:: sets actions
|
80
|
+
# name:: the name of the controller
|
81
|
+
# default_layout:: the default layout (used by render)
|
82
|
+
#
|
83
|
+
def set(variable, input)
|
84
|
+
instance_variable_set("@#{variable}", input)
|
85
|
+
end
|
86
|
+
|
87
|
+
protected
|
88
|
+
|
89
|
+
# Overridden so that if declare_action is set, new methods
|
90
|
+
# are added to actions.
|
91
|
+
def method_added(sym) # :nodoc:
|
92
|
+
actions << sym if @define_action
|
93
|
+
super
|
94
|
+
end
|
95
|
+
|
96
|
+
# Turns on declare_action when changing method context.
|
97
|
+
def public(*symbols) # :nodoc:
|
98
|
+
@define_action = true if symbols.empty?
|
99
|
+
super
|
100
|
+
end
|
101
|
+
|
102
|
+
# Turns off declare_action when changing method context.
|
103
|
+
def protected(*symbols) # :nodoc:
|
104
|
+
@define_action = false if symbols.empty?
|
105
|
+
super
|
106
|
+
end
|
107
|
+
|
108
|
+
# Turns off declare_action when changing method context.
|
109
|
+
def private(*symbols) # :nodoc:
|
110
|
+
@define_action = false if symbols.empty?
|
111
|
+
super
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
set :actions, []
|
116
|
+
set :middleware, []
|
117
|
+
set :default_layout, nil
|
118
|
+
set :define_action, false
|
119
|
+
|
120
|
+
include Rack::Utils
|
121
|
+
|
122
|
+
# Accesses the 'tap.server' specified in env, set during call.
|
123
|
+
attr_accessor :server
|
124
|
+
|
125
|
+
# A Rack::Request wrapping env, set during call.
|
126
|
+
attr_accessor :request
|
127
|
+
|
128
|
+
# A Rack::Response. If the action returns a string, it will be written to
|
129
|
+
# response and response will be returned by call. Otherwise, call returns
|
130
|
+
# the action result and response is ignored.
|
131
|
+
attr_accessor :response
|
132
|
+
|
133
|
+
# Initializes a new instance of self. The input attributes are reset by
|
134
|
+
# call and are only provided for convenience during testing.
|
135
|
+
def initialize(server=nil, request=nil, response=nil)
|
136
|
+
@server = server
|
137
|
+
@request = request
|
138
|
+
@response = response
|
139
|
+
end
|
140
|
+
|
141
|
+
def call(env)
|
142
|
+
@server = env['tap.server'] || Tap::Server.new
|
143
|
+
@request = Rack::Request.new(env)
|
144
|
+
@response = Rack::Response.new
|
145
|
+
|
146
|
+
# route to an action
|
147
|
+
blank, action, *args = request.path_info.split("/").collect {|arg| unescape(arg) }
|
148
|
+
action = "index" if action == nil || action.empty?
|
149
|
+
|
150
|
+
unless self.class.actions.include?(action.to_sym)
|
151
|
+
raise ServerError.new("404 Error: page not found", 404)
|
152
|
+
end
|
153
|
+
|
154
|
+
result = send(action, *args)
|
155
|
+
if result.kind_of?(String)
|
156
|
+
response.write result
|
157
|
+
response.finish
|
158
|
+
else
|
159
|
+
result
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def render(path, options={})
|
164
|
+
options, path = path, nil if path.kind_of?(Hash)
|
165
|
+
|
166
|
+
# lookup template
|
167
|
+
template_path = case
|
168
|
+
when options.has_key?(:template)
|
169
|
+
server.template_path(options[:template])
|
170
|
+
else
|
171
|
+
server.template_path("#{self.class.name}/#{path}")
|
172
|
+
end
|
173
|
+
|
174
|
+
unless template_path
|
175
|
+
raise "could not find template for: #{path}"
|
176
|
+
end
|
177
|
+
|
178
|
+
# render template
|
179
|
+
template = server.content(template_path)
|
180
|
+
content = render_erb(template, options)
|
181
|
+
|
182
|
+
# render layout
|
183
|
+
layout = options[:layout]
|
184
|
+
layout = self.class.default_layout if layout == true
|
185
|
+
if layout
|
186
|
+
render(:template => layout, :locals => {:content => content})
|
187
|
+
else
|
188
|
+
content
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def render_erb(template, options={})
|
193
|
+
# assign locals to the render binding
|
194
|
+
# this almost surely may be optimized...
|
195
|
+
locals = options[:locals]
|
196
|
+
binding = empty_binding
|
197
|
+
|
198
|
+
locals.each_pair do |key, value|
|
199
|
+
@assignment_value = value
|
200
|
+
eval("#{key} = remove_instance_variable(:@assignment_value)", binding)
|
201
|
+
end if locals
|
202
|
+
|
203
|
+
ERB.new(template, nil, "<>").result(binding)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Redirects to the specified uri.
|
207
|
+
def redirect(uri, status=302, headers={}, body="")
|
208
|
+
response.status = status
|
209
|
+
response.headers.merge!(headers)
|
210
|
+
response.body = body
|
211
|
+
|
212
|
+
response['Location'] = uri
|
213
|
+
response.finish
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns a session hash.
|
217
|
+
def session
|
218
|
+
request.env['rack.session'] ||= {}
|
219
|
+
end
|
220
|
+
|
221
|
+
# Returns the app for the current session.
|
222
|
+
def app
|
223
|
+
server.app(session[:id] ||= server.initialize_session)
|
224
|
+
end
|
225
|
+
|
226
|
+
# Generates an empty binding to self without any locals assigned.
|
227
|
+
def empty_binding # :nodoc:
|
228
|
+
binding
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|