tap-server 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/History CHANGED
@@ -1,3 +1,14 @@
1
+ == 0.3.0 / 2009-03-17
2
+
3
+ Significant updates.
4
+
5
+ * added optional REST routes to Tap::Controller
6
+ * added persistence wrapper for session root
7
+ * significant cleanup
8
+ * removed middleware support
9
+ * added bindings for running server from
10
+ Firefox/Ubiquity
11
+
1
12
  == 0.2.0 / 2009-03-07
2
13
 
3
14
  * Reworked server routing to route by ::controller
data/cmd/server.rb CHANGED
@@ -7,13 +7,7 @@ require 'tap/server'
7
7
 
8
8
  env = Tap::Env.instance
9
9
  app = Tap::App.instance
10
-
11
- #
12
- # handle options
13
- #
14
-
15
- config_path = nil
16
- opts = ConfigParser.new do |opts|
10
+ parser = ConfigParser.new do |opts|
17
11
 
18
12
  opts.separator ""
19
13
  opts.separator "options:"
@@ -26,9 +20,7 @@ opts = ConfigParser.new do |opts|
26
20
  exit
27
21
  end
28
22
  end
23
+ argv = parser.parse(ARGV)
29
24
 
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)
25
+ # launch server
26
+ Tap::Server.new(env, app, parser.config).run!
@@ -1,4 +1,5 @@
1
1
  require 'tap/server'
2
+ require 'tap/support/persistence'
2
3
  autoload(:ERB, 'erb')
3
4
 
4
5
  module Tap
@@ -16,13 +17,62 @@ module Tap
16
17
  # * define it private or protected then call public(:method)
17
18
  #
18
19
  class Controller
20
+
21
+ # Adds REST routing (a-la Rails[http://www.b-simple.de/download/restful_rails_en.pdf])
22
+ # to a Tap::Controller.
23
+ #
24
+ # class Projects < Tap::Controller
25
+ # include RestRoutes
26
+ #
27
+ # # GET /projects
28
+ # def index...
29
+ #
30
+ # # GET /projects/*args
31
+ # def show(*args)...
32
+ #
33
+ # # GET /projects/arg;edit/*args
34
+ # def edit(arg, *args)...
35
+ #
36
+ # # POST /projects/*args
37
+ # def create(*args)...
38
+ #
39
+ # # PUT /projects/*args
40
+ # def update(*args)...
41
+ #
42
+ # # DELETE /projects/*args
43
+ # def destroy(*args)...
44
+ # end
45
+ #
46
+ module RestRoutes
47
+ def route
48
+ blank, *args = request.path_info.split("/").collect {|arg| unescape(arg) }
49
+ action = case request.request_method
50
+ when /GET/i
51
+ case
52
+ when args.empty?
53
+ :index
54
+ when args[0] =~ /(.*);edit$/
55
+ args[0] = $1
56
+ :edit
57
+ else
58
+ :show
59
+ end
60
+ when /POST/i then :create
61
+ when /PUT/i then :update
62
+ when /DELETE/i then :destroy
63
+ else raise ServerError.new("unknown request method: #{request.request_method}")
64
+ end
65
+
66
+ [action, args]
67
+ end
68
+ end
69
+
19
70
  class << self
20
71
 
21
72
  # Initialize instance variables on the child and inherit as necessary.
22
73
  def inherited(child) # :nodoc:
23
74
  super
24
75
  child.set(:actions, actions.dup)
25
- child.set(:middleware, middleware.dup)
26
76
  child.set(:default_layout, default_layout)
27
77
  child.set(:define_action, true)
28
78
  end
@@ -31,10 +81,6 @@ module Tap
31
81
  # stored as symbols. Actions are inherited.
32
82
  attr_reader :actions
33
83
 
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
84
  # The default layout rendered when the render option :layout is true.
39
85
  attr_reader :default_layout
40
86
 
@@ -44,30 +90,9 @@ module Tap
44
90
  @name ||= to_s.underscore
45
91
  end
46
92
 
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.
93
+ # Instantiates self and performs call.
65
94
  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)
95
+ new.call(env)
71
96
  end
72
97
 
73
98
  # Sets an instance variable for self, short for:
@@ -113,8 +138,10 @@ module Tap
113
138
  end
114
139
 
115
140
  set :actions, []
116
- set :middleware, []
117
141
  set :default_layout, nil
142
+
143
+ # Ensures methods (even public methods) on Controller will
144
+ # not be actions in subclasses.
118
145
  set :define_action, false
119
146
 
120
147
  include Rack::Utils
@@ -130,12 +157,16 @@ module Tap
130
157
  # the action result and response is ignored.
131
158
  attr_accessor :response
132
159
 
160
+ # The action currently being called by self.
161
+ attr_accessor :action
162
+
133
163
  # Initializes a new instance of self. The input attributes are reset by
134
164
  # call and are only provided for convenience during testing.
135
165
  def initialize(server=nil, request=nil, response=nil)
136
166
  @server = server
137
167
  @request = request
138
168
  @response = response
169
+ @action = nil
139
170
  end
140
171
 
141
172
  def call(env)
@@ -144,14 +175,12 @@ module Tap
144
175
  @response = Rack::Response.new
145
176
 
146
177
  # 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)
178
+ @action, args = route
179
+ unless self.class.actions.include?(@action)
151
180
  raise ServerError.new("404 Error: page not found", 404)
152
181
  end
153
182
 
154
- result = send(action, *args)
183
+ result = send(@action, *args)
155
184
  if result.kind_of?(String)
156
185
  response.write result
157
186
  response.finish
@@ -160,15 +189,23 @@ module Tap
160
189
  end
161
190
  end
162
191
 
192
+ def route
193
+ blank, action, *args = request.path_info.split("/").collect {|arg| unescape(arg) }
194
+ action = "index" if action == nil || action.empty?
195
+ action = action.chomp(File.extname(action)).to_sym
196
+
197
+ [action, args]
198
+ end
199
+
163
200
  def render(path, options={})
164
201
  options, path = path, nil if path.kind_of?(Hash)
165
202
 
166
203
  # lookup template
167
204
  template_path = case
168
205
  when options.has_key?(:template)
169
- server.template_path(options[:template])
206
+ server.search(:views, options[:template])
170
207
  else
171
- server.template_path("#{self.class.name}/#{path}")
208
+ server.search(:views, "#{self.class.name}/#{path}")
172
209
  end
173
210
 
174
211
  unless template_path
@@ -176,7 +213,7 @@ module Tap
176
213
  end
177
214
 
178
215
  # render template
179
- template = server.content(template_path)
216
+ template = File.read(template_path)
180
217
  content = render_erb(template, options)
181
218
 
182
219
  # render layout
@@ -228,6 +265,16 @@ module Tap
228
265
  server.root(session[:id] ||= server.initialize_session)
229
266
  end
230
267
 
268
+ # Returns the file-based controller persistence.
269
+ def persistence
270
+ @persistence ||= Support::Persistence.new(root)
271
+ end
272
+
273
+ # Returns a controller uri.
274
+ def uri(action=nil, params={})
275
+ server.uri(self.class.name, action, params)
276
+ end
277
+
231
278
  # Generates an empty binding to self without any locals assigned.
232
279
  def empty_binding # :nodoc:
233
280
  binding
@@ -13,7 +13,7 @@ module Tap
13
13
  # serve public files before actions
14
14
  server = env['tap.server'] ||= Tap::Server.new
15
15
 
16
- if path = server.public_path(env['PATH_INFO'])
16
+ if path = server.search(:public, env['PATH_INFO'])
17
17
  content = File.read(path)
18
18
  headers = {
19
19
  "Last-Modified" => [File.mtime(path).httpdate],
@@ -35,7 +35,7 @@ module Tap
35
35
 
36
36
  render('index.erb', :locals => {:env => server.env, :env_names => env_names}, :layout => true)
37
37
  end
38
-
38
+
39
39
  def info
40
40
  if request.post?
41
41
  app.info
@@ -0,0 +1,58 @@
1
+ require 'tap/controller'
2
+
3
+ module Tap
4
+ module Controllers
5
+
6
+ # :startdoc::controller remotely controls and monitors server
7
+ #
8
+ # Server provides several uris to control and monitor the server behavior.
9
+ # Importantly, server allows the remote shutdown of a Tap::Server if a
10
+ # shutdown_key is set. This makes it possible to run servers in the
11
+ # background and still have a shutdown handle on them.
12
+ #
13
+ class Server < Tap::Controller
14
+ set :default_layout, 'layout.erb'
15
+
16
+ def index
17
+ render 'index.erb'
18
+ end
19
+
20
+ # Returns 'pong'.
21
+ def ping
22
+ response['Content-Type'] = 'text/plain'
23
+ "pong"
24
+ end
25
+
26
+ # Returns the public server configurations as xml.
27
+ def config
28
+ response['Content-Type'] = 'text/xml'
29
+ %Q{<?xml version="1.0"?>
30
+ <server>
31
+ <uri>#{uri}</uri>
32
+ <shutdown_key>#{shutdown_key}</shutdown_key>
33
+ </server>}
34
+ end
35
+
36
+ # Shuts down the server. Shutdown requires a shutdown key which
37
+ # is setup when the server is launched. If no shutdown key was
38
+ # setup, shutdown does nothing and responds accordingly.
39
+ def shutdown
40
+ if shutdown_key && request['shutdown_key'].to_i == shutdown_key
41
+ # wait a second to shutdown, so the response is sent out.
42
+ Thread.new {sleep 1; server.stop! }
43
+ "shutdown"
44
+ else
45
+ "you do not have permission to shutdown this server"
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ # returns the server shutdown key. the shutdown key is required
52
+ # for shutdown to function, a nil shutdown key disables shutdown.
53
+ def shutdown_key # :nodoc:
54
+ server.shutdown_key
55
+ end
56
+ end
57
+ end
58
+ end
data/lib/tap/server.rb CHANGED
@@ -34,9 +34,9 @@ module Tap
34
34
  # req = Rack::MockRequest.new(server)
35
35
  # req.get('/sample/path/to/resource').body # => "Sample got /sample : /path/to/resource"
36
36
  #
37
- # Server automatically maps unknown keys to a controller by searching
38
- # env.controllers. As a result '/example' maps to the Example controller
39
- # defined in 'lib/example.rb'.
37
+ # Server automatically maps unknown keys to controllers discovered via the
38
+ # env.controllers manifest. The only requirement is that the controller
39
+ # constant is a Rack application. For instance:
40
40
  #
41
41
  # # [lib/example.rb] => %q{
42
42
  # # ::controller
@@ -65,38 +65,116 @@ module Tap
65
65
  #
66
66
  # req.get('/unknown/path/to/resource').body # => "App got : /unknown/path/to/resource"
67
67
  #
68
+ # In development mode, the controller constant is removed and the constant
69
+ # require path is reloaded each time it gets called. This system allows many
70
+ # web frameworks to be hooked into a Tap server.
71
+ #
68
72
  # :::+
69
73
  class Server
74
+ class << self
75
+
76
+ # Instantiates a Server in the specified directory, configured as
77
+ # specified in root/server.yml. If shutdown_key is specified, a
78
+ # random shutdown key will be generated and set on the sever.
79
+ #
80
+ def instantiate(root, shutdown_key=false)
81
+ # setup the server directory
82
+ root = File.expand_path(root)
83
+ FileUtils.mkdir_p(root) unless File.exists?(root)
84
+
85
+ # initialize the server
86
+ app = Tap::App.instance
87
+ env = Tap::Exe.instantiate(root)
88
+ env.activate
89
+ config = Configurable::Utils.load_file(env.root['server.yml'])
90
+
91
+ server = new(env, app, config)
92
+ server.config[:shutdown_key] = rand(1000000) if shutdown_key
93
+ server
94
+ end
95
+
96
+ # Runs the server
97
+ def run(server)
98
+ cookie_server = Rack::Session::Pool.new(server)
99
+ server.run!
100
+ end
101
+ end
102
+
70
103
  include Rack::Utils
71
104
  include Configurable
72
105
 
73
- config :environment, :development
74
- config :server, %w[thin mongrel webrick]
75
- config :host, 'localhost'
76
- config :port, 8080, &c.integer
106
+ config :environment, :development
107
+ config :servers, %w[thin mongrel webrick], &c.list # a list of preferred handlers
108
+ config :host, 'localhost', &c.string # the server host
109
+ config :port, 8080, &c.integer # the server port
77
110
 
78
- config :views_dir, :views
79
- config :public_dir, :public
111
+ # A hash of (key, controller) pairs mapping the controller part of a route
112
+ # to a Rack application. Typically controllers is used to specify aliases
113
+ # when the defaults are not preferable.
80
114
  config :controllers, {}
115
+
116
+ # config :infer_controllers, true, &c.switch
117
+
118
+ # The default controller key used in routes that cannot be directly mapped
119
+ # to a controller
120
+ #--
121
+ # Set to nil to force controller mapping?
81
122
  config :default_controller_key, 'app'
82
123
 
124
+ # Server implements a shutdown key so the server can be shutdown remotely
125
+ # via an HTTP request to the app/shutdown method. Remote shutdown is
126
+ # useful when the user is running a local server (especially from a
127
+ # background process). Under many circumstances remote shutdown is
128
+ # undesirable; specify a nil shutdown key, the default, to turn off
129
+ # shutdown.
130
+ config :shutdown_key, nil, &c.integer_or_nil # specifies a public shutdown key
131
+
83
132
  attr_reader :env
133
+ attr_reader :handler
84
134
 
85
135
  def initialize(env=Env.new, app=Tap::App.instance, config={})
86
136
  @env = env
87
137
  @app = app
88
138
  @cache = {}
139
+ @handler = nil
89
140
  initialize_config(config)
90
141
  end
91
142
 
143
+ # Runs self as configured, on the specified server, host, and port. Use an
144
+ # INT signal to interrupt.
145
+ def run!(handler=rack_handler)
146
+ app.log :run, "#{host}:#{port} (#{handler})"
147
+ handler.run self, :Host => host, :Port => port do |handler_instance|
148
+ @handler = handler_instance
149
+ trap(:INT) { stop! }
150
+ end
151
+ end
152
+
153
+ # Stops the server if running (ie a handler is set). Returns true if the
154
+ # server was stopped, and false otherwise.
155
+ def stop!
156
+ if handler
157
+ # Use thins' hard #stop! if available, otherwise just #stop
158
+ handler.respond_to?(:stop!) ? handler.stop! : handler.stop
159
+ @handler = nil
160
+ false
161
+ else
162
+ true
163
+ end
164
+ end
165
+
92
166
  # Currently a stub for initializing a session. initialize_session returns
93
167
  # an integer session id.
94
168
  def initialize_session
95
169
  id = 0
96
170
  session_app = app(id)
97
- log_path = env.root.prepare(:log, 'server.log')
98
- session_app.logger = Logger.new(log_path)
171
+ session_root = root(id)
99
172
 
173
+ # setup expiration information...
174
+
175
+ # setup a session log
176
+ log_path = session_root.prepare(:log, 'session.log')
177
+ session_app.logger = Logger.new(log_path)
100
178
  session_app.on_complete do |_result|
101
179
  # find the template
102
180
  class_name = _result.key.class.to_s.underscore
@@ -116,28 +194,31 @@ module Tap
116
194
  file << Support::Templater.new(File.read(template)).build(:_result => _result)
117
195
  end
118
196
  end
119
- end
197
+ end unless session_app.on_complete_block
120
198
 
121
199
  id
122
200
  end
123
201
 
124
- # Returns the app provided during initialization. In the future this
125
- # method may be extended to provide a session-specific App, hence it
126
- # has been stubbed with an id input.
202
+ # Returns the session-specific App, or the server app if id is nil.
127
203
  def app(id=nil)
128
204
  @app
129
205
  end
130
206
 
131
- # Returns the env.root provided during initialization. In the future this
132
- # method may be extended to provide a session-specific Root, hence it
133
- # has been stubbed with an id input.
207
+ # Returns the session-specific Root, or the server env.root if id is nil.
134
208
  def root(id=nil)
135
209
  @env.root
136
210
  end
137
211
 
138
- # Returns true if environment is :development.
139
- def development?
140
- environment == :development
212
+ # Returns a uri mapping to the specified controller and action. Parameters
213
+ # may be specified; they are built as a query and attached to the uri as
214
+ # normal.
215
+ #
216
+ # Currenlty uri does not map the controller to a minipath, but in the
217
+ # future it will.
218
+ def uri(controller=nil, action=nil, params={})
219
+ query = build_query(params)
220
+ uri = ["http://#{host}:#{port}", escape(controller), action].compact.join("/")
221
+ query.empty? ? uri : "#{uri}?#{query}"
141
222
  end
142
223
 
143
224
  # The {Rack}[http://rack.rubyforge.org/doc/] interface method.
@@ -173,25 +254,31 @@ module Tap
173
254
  ServerError.response($!)
174
255
  end
175
256
 
176
- #--
177
- # TODO: implement caching for path content
178
- def content(path)
179
- File.read(path)
257
+ # Searches env for the first matching file, directories are not matched.
258
+ def search(dir, path)
259
+ env.search(dir, path) {|file| File.file?(file) }
180
260
  end
181
261
 
182
- #--
183
- # TODO: implement caching for public_paths
184
- def public_path(path)
185
- env.search(public_dir, path) {|public_path| File.file?(public_path) }
186
- end
262
+ protected
187
263
 
188
- #--
189
- # TODO: implement caching for template_paths
190
- def template_path(path)
191
- env.search(views_dir, path) {|template_path| File.file?(template_path) }
264
+ # Returns true if environment is :development.
265
+ def development? # :nodoc:
266
+ environment == :development
192
267
  end
193
268
 
194
- protected
269
+ # Looks up and returns the first available Rack::Handler as listed in the
270
+ # servers configuration. (Note rack_handler returns a handler class, not
271
+ # an instance). Adapted from Sinatra.detect_rack_handler
272
+ def rack_handler # :nodoc:
273
+ servers.each do |server_name|
274
+ begin
275
+ return Rack::Handler.get(server_name)
276
+ rescue LoadError
277
+ rescue NameError
278
+ end
279
+ end
280
+ raise "Server handler (#{servers.join(',')}) not found."
281
+ end
195
282
 
196
283
  # a helper method for routing a key to a controller
197
284
  def lookup(key) # :nodoc:
@@ -213,10 +300,16 @@ module Tap
213
300
  # load the require_path in dev mode so that
214
301
  # controllers will be reloaded each time
215
302
  if development? && const.require_path
216
- if Object.const_defined?(const.const_name)
217
- Object.send(:remove_const, const.const_name)
303
+ parent = if const.nesting.empty?
304
+ Object
305
+ else
306
+ Tap::Support::Constant.constantize(const.nesting) { nil }
218
307
  end
219
-
308
+
309
+ if parent && parent.const_defined?(const.const_name)
310
+ parent.send(:remove_const, const.const_name)
311
+ end
312
+
220
313
  load const.require_path
221
314
  end
222
315
 
@@ -0,0 +1,71 @@
1
+ module Tap
2
+ module Support
3
+
4
+ # A very simple wrapper for root providing a CRUD interface for reading and
5
+ # writing files.
6
+ class Persistence
7
+
8
+ # The Tap::Root for self.
9
+ attr_reader :root
10
+
11
+ # Initializes a new persistence wrapper for the specified root.
12
+ def initialize(root)
13
+ @root = root
14
+ end
15
+
16
+ # Returns the filepath for the specified id. Non-string ids are allowed;
17
+ # they will be converted to strings using to_s.
18
+ def path(id)
19
+ root.subpath(:data, id.to_s)
20
+ end
21
+
22
+ # Returns a list of existing ids.
23
+ def index
24
+ root.glob(:data).select do |path|
25
+ File.file?(path)
26
+ end.collect do |path|
27
+ root.relative_filepath(:data, path)
28
+ end
29
+ end
30
+
31
+ # Creates the file for the specified id. If a block is given, an io to
32
+ # the file will be yielded to it; otherwise the file will be created
33
+ # without content. Returns the path to the persistence file.
34
+ #
35
+ # Raises an error if the file already exists.
36
+ def create(id)
37
+ filepath = path(id)
38
+ raise "already exists: #{filepath}" if File.exists?(filepath)
39
+ root.prepare(filepath) {|io| yield(io) if block_given? }
40
+ end
41
+
42
+ # Reads and returns the data for the specified id, or an empty string if
43
+ # the persistence file doesn't exist.
44
+ def read(id)
45
+ filepath = path(id)
46
+ File.file?(filepath) ? File.read(filepath) : ''
47
+ end
48
+
49
+ # Overwrites the data for the specified id. A block must be given to
50
+ # provide the new content; a persistence file will be created if one
51
+ # does not exist already.
52
+ def update(id)
53
+ root.prepare(path(id)) {|io| yield(io) }
54
+ end
55
+
56
+ # Removes the persistence file for id, if it exists. Returns true if
57
+ # the file was removed.
58
+ def destroy(id)
59
+ filepath = path(id)
60
+
61
+ if File.file?(filepath)
62
+ FileUtils.rm(filepath)
63
+ true
64
+ else
65
+ false
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -40,5 +40,5 @@
40
40
  <% end %>
41
41
  </dd>
42
42
  <% end %>
43
- </dl>
43
+ </dl>
44
44
  </div>
@@ -0,0 +1,3 @@
1
+ <% if shutdown_key %>
2
+ <a href="<%= uri(:shutdown) %>?shutdown_key=<%= shutdown_key %>">shutdown</a>
3
+ <% end %>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tap-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Chiang
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-03-07 00:00:00 -07:00
12
+ date: 2009-03-17 00:00:00 -06:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -47,10 +47,12 @@ files:
47
47
  - lib/tap/controller.rb
48
48
  - lib/tap/controllers/app.rb
49
49
  - lib/tap/controllers/schema.rb
50
+ - lib/tap/controllers/server.rb
50
51
  - lib/tap/server.rb
51
52
  - lib/tap/server_error.rb
52
53
  - lib/tap/tasks/echo.rb
53
54
  - lib/tap/tasks/server.rb
55
+ - lib/tap/support/persistence.rb
54
56
  - public/javascripts/prototype.js
55
57
  - public/javascripts/tap.js
56
58
  - public/stylesheets/tap.css
@@ -70,6 +72,7 @@ files:
70
72
  - views/tap/controllers/schema/preview.erb
71
73
  - views/tap/controllers/schema/round.erb
72
74
  - views/tap/controllers/schema/schema.erb
75
+ - views/tap/controllers/server/index.erb
73
76
  - views/tap/tasks/echo/result.html
74
77
  - README
75
78
  - MIT-LICENSE