fastr 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  Micro web framework for Ruby. Should be used with an EventMachine rack server.
4
4
 
5
+ == Guide
6
+
7
+ The guide is a work in progress, check it out here: http://chrismoos.com/fastr
8
+
9
+ You can help with the guide by forking this repository: http://github.com/chrismoos/fastr_doc
10
+
11
+ == API Documentation
12
+
13
+ You can see the latest API documentation here: http://rdoc.info/projects/chrismoos/fastr
14
+
5
15
  == Getting Started
6
16
 
7
17
  $ sudo gem install fastr
@@ -24,16 +34,20 @@ The routes are configured in app/config/routes.rb
24
34
 
25
35
  router.draw do |route|
26
36
  route.for '/:controller/:action'
27
- #route.for '/home/:action', :action => '[A-Za-z]+'
28
- #route.for '/test', :to => 'home#index'
37
+ # route.for '/home/:action', :action => '[A-Za-z]+'
38
+ # route.for '/test', :to => 'home#index'
39
+ # route.for '/users/:id', :to => 'users#create', :methods => [:post]
29
40
  end
30
41
 
42
+ By default a route will match against all HTTP methods (GET, POST, etc,.).
43
+
31
44
  == Settings
32
45
 
33
46
  Various settings can be configured in app/config/settings.rb
34
47
 
35
48
  config.log_level = Logger::DEBUG
36
49
  config.cache_templates = true
50
+ config.plugins << MyPluginModule
37
51
 
38
52
  == Controller
39
53
 
@@ -68,15 +82,76 @@ The return for a controller is just a rack response, i.e [200, {"Content-Type" =
68
82
  You can also use the following render methods:
69
83
 
70
84
  render(:text, "My text")
71
-
72
- With HAML, the template is rendered and any instance variables in your controller are available in the template.
73
85
 
74
- render(:haml, :template => "index") # this searches for index.haml in your app/views/ folder
75
-
86
+ Fastr currently has support for HAML and eRuby templates. Any instance variables in your controller are available in the template. The correct template engine will be chosen based on the file extension. By default no template engines will be loaded, you need to explicitly require the engine(s) you will be using in your init.rb file (below the require 'fastr' line):
87
+
88
+ require 'fastr'
89
+ require 'fastr/template/erubis'
90
+ require 'fastr/template/haml'
91
+
92
+ Some rendering examples:
93
+
94
+ render(:template, "users/index.haml") # this path is relative to your app/views/ folder
95
+ render(:template, "users/index.html.erb") # this path is relative to your app/views/ folder
96
+
97
+ You can also specify a hash of data that will be available in the @vars instance variable from your template:
98
+
99
+ render(:template, "users/index.html.erb", {:vars => {:greeting => "Aloha!"}})
100
+
101
+ This is particularly useful when you render a partial:
102
+
103
+ render(:partial, "users/_greeting.html.erb", {:vars => {:message => "Welcome!"}})
104
+
105
+ It's also possible to specify a response code and headers:
106
+
107
+ render(:template, "users/index.html.erb", {:vars => {:greeting => "Aloha!"}, :response_code => 200, :headers => "Content-Type" => "text/html"})
108
+
76
109
  JSON:
77
110
 
78
111
  render(:json, {:status => "ok", :message => "done"})
79
112
 
113
+ == Async Responses
114
+
115
+ You should never block EventMachine. If you're doing any kind of I/O in your controller action you need to render your response asynchronously:
116
+
117
+ class DemoController < Fastr::Controller
118
+ def fast_index
119
+ EM.add_timer(1) do
120
+ async_resp { render(:text, "fast_index\n") }
121
+ end
122
+ render_async
123
+ end
124
+
125
+ def slow_index
126
+ sleep(1)
127
+ render(:text, "slow_index\n")
128
+ end
129
+ end
130
+
131
+ Here's the difference:
132
+
133
+ $ ab -n 10 -c 10 "http://127.0.0.1:4444/demo/fast_index"
134
+ Concurrency Level: 10
135
+ Time taken for tests: 1.010 seconds
136
+ Requests per second: 9.90 [#/sec] (mean)
137
+
138
+ $ ab -n 10 -c 10 "http://127.0.0.1:4444/demo/slow_index"
139
+ Concurrency Level: 10
140
+ Time taken for tests: 10.011 seconds
141
+ Requests per second: 1.00 [#/sec] (mean)
142
+
143
+ If all your actions in a controller are async you can use an after_filter to make things cleaner:
144
+
145
+ class DemoController < Fastr::Controller
146
+ after_filter :render_async
147
+
148
+ def index
149
+ EM.add_timer(1) do
150
+ async_resp { render(:text, "fast_index\n") }
151
+ end
152
+ end
153
+ end
154
+
80
155
  == Deferred Responses
81
156
 
82
157
  fastr also lets you return a deferred response. This is useful if you want to chunk the response back to the client, or have a long running operation that you want to perform without blocking EventMachine.
@@ -112,8 +187,79 @@ The following is an example of a controller action.
112
187
  end
113
188
  end
114
189
 
190
+ == AsyncRecord (experimental database support)
191
+
192
+ One of the greatest things about running on an event-based server is that you can get accelerated performance in database access.
193
+
194
+ Usually there is a lot of time spent blocking for a database query to return. In Fastr, using AsyncRecord, your queries don't block the request. You will receive a callback once the query has completed. This has major performance implications.
195
+
196
+ NOTE: Even though your connections are non-blocking to the database server, the database server is still blocking when accessing IO (disk/memory).
197
+
198
+ To use AsyncRecord, do the following:
199
+
200
+ Setup your init.rb file:
201
+
202
+ require 'async_record'
203
+ conn = AsyncRecord::Connection::MySQL.new(:host => "127.0.0.1", :port => 3306, :user => "root", :database => "database")
204
+ conn.connect
205
+ AsyncRecord::Base.set_connection(conn)
206
+
207
+ Define a model (app/models/user.rb):
208
+
209
+ class User < AsyncRecord::Base
210
+ set_table_name "users"
211
+ end
212
+
213
+ === Controller
214
+
215
+ In your controller, try the following (remember to put the following in a deferred response):
216
+
217
+ === Get all the rows in the table:
218
+
219
+ User.all(:limit => 256) do |users|
220
+ users.each do |user|
221
+ response.send_data("#{user.username}\n")
222
+ end
223
+ response.succeed
224
+ end
225
+
226
+ === Find a row by ID
227
+
228
+ User.find(1) do |user|
229
+ if user.nil?
230
+ response.send_data("User not found")
231
+ else
232
+ response.send_data("User: #{user.username}\n")
233
+ end
234
+ response.succeed
235
+ end
236
+
237
+ === Get the count of rows in the table
238
+
239
+ User.count do |count|
240
+ response.send_data("Count: #{count}")
241
+ response.succeed
242
+ end
243
+
244
+ === Run a custom query
245
+
246
+ User.query("select username from users") do |results|
247
+ response.send_data("Results: #{results.inspect}")
248
+ response.succeed
249
+ end
250
+
251
+ WARNING: AsyncRecord is under heavy development, but its pretty cool :).
252
+
115
253
  == Plugins
116
254
 
255
+ === Loading manually
256
+
257
+ To explicitly load a plugin that doesn't exist in your custom/plugins directory, you can do the following in your *settings.rb* file:
258
+
259
+ config.plugins << MyPluginModule
260
+
261
+ === Loading from custom directory
262
+
117
263
  Fastr searches the custom/plugins directory in your application's root directory for loading plugins.
118
264
 
119
265
  Example structure:
@@ -145,6 +291,36 @@ Here is an example plugin and what is currently supported:
145
291
  end
146
292
  end
147
293
 
294
+ == Filters
295
+
296
+ You can add before and after filters to your controller. The filters are executed before an action is called, and after.
297
+
298
+ === Before Filters
299
+
300
+ before_filter :my_before_filter_noop, :my_before_filter_halt
301
+
302
+ def my_before_filter_noop
303
+ filter_continue # use this if you want the filter chain to continue
304
+ end
305
+
306
+ def my_before_filter_halt
307
+ [200, {}, ["STOP HERE AND RETURN"]] # Return a rack response if you want the chain to halt
308
+ end
309
+
310
+ === After Filters
311
+
312
+ after_filter :my_after_filter
313
+
314
+ def my_after_filter(response)
315
+ # here you can modify the response
316
+ # response is just a rack response, i.e [200, {}, "Hello, Filter!"]
317
+ # This filter adds a custom header
318
+ code, headers, body = *response
319
+ headers['My-Custom-Header'] = 'vall'
320
+
321
+ [code, headers, body]
322
+ end
323
+
148
324
  == Static Files
149
325
 
150
326
  Anything stored in the public folder in your project's root directory will be served as a static file. This directory is checked before the routes. The mime type is set based on the file's extension.
data/bin/fastr CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'rubygems'
4
+ require 'fastr'
3
5
  require 'fileutils'
4
6
 
5
7
  module Fastr
@@ -11,7 +13,8 @@ module Fastr
11
13
 
12
14
  # Directory Structre
13
15
 
14
- dirs = ['app/config', 'app/controllers', 'app/views', 'app/models', 'lib', 'test', 'public', 'custom/plugins']
16
+ dirs = ['app/config', 'app/controllers', 'app/views', 'app/models', 'lib', 'test', 'public', 'custom/plugins',
17
+ 'test/unit']
15
18
  dirs.each do |dir|
16
19
  FileUtils.mkdir_p("#{app_name}/#{dir}")
17
20
  end
@@ -20,8 +23,9 @@ module Fastr
20
23
  File.open("#{app_name}/app/config/routes.rb", "w") do |f|
21
24
  f.write("router.draw do |route|\n")
22
25
  f.write("\troute.for '/:controller/:action'\n")
23
- f.write("\t#route.for '/home/:action', :action => '[A-Za-z]+'\n")
24
- f.write("\t#route.for '/test', :to => 'home#index'\n")
26
+ f.write("\t# route.for '/home/:action', :action => '[A-Za-z]+'\n")
27
+ f.write("\t# route.for '/test', :to => 'home#index'\n")
28
+ f.write("\t# route.for '/users/:id', :to => 'users#create', :methods => [:post]\n")
25
29
  f.write("end")
26
30
  f.close
27
31
  end
@@ -31,7 +35,7 @@ module Fastr
31
35
  f.puts("require 'fastr'")
32
36
  f.puts("EM.kqueue = true if EM.kqueue?")
33
37
  f.puts("EM.epoll = true if EM.epoll?")
34
- f.puts("fastrApp = Fastr::Application.new(File.expand_path(File.dirname(__FILE__)))")
38
+ f.puts("fastrApp = Fastr::Application.new(::File.expand_path(::File.dirname(__FILE__)))")
35
39
  f.puts("app = lambda { |env|")
36
40
  f.puts("\tfastrApp.dispatch(env)")
37
41
  f.puts("}")
@@ -49,14 +53,33 @@ module Fastr
49
53
  # Gemfile
50
54
  File.open("#{app_name}/Gemfile", 'a') do |f|
51
55
  f.puts "source :gemcutter"
52
-
53
- f.puts "gem 'jeweler'"
54
- f.puts "gem 'eventmachine'"
55
- f.puts "gem 'haml'"
56
- f.puts "\ngem 'fastr'"
56
+ f.puts "gem 'fastr'"
57
57
  f.close
58
58
  end
59
59
 
60
+ # init.rb
61
+ File.open("#{app_name}/app/config/init.rb", "w") do |f|
62
+ f.puts('# Enable AsyncRecord support (experimental)')
63
+ f.puts("# you must add \"require 'async_record'\" to your config.ru file ")
64
+ f.puts('#conn = AsyncRecord::Connection::MySQL.new(:host => "127.0.0.1", :port => 3306, :user => "root", :database => "database")')
65
+ f.puts('#conn.connect')
66
+ f.puts('#AsyncRecord::Base.set_connection(conn)')
67
+ end
68
+
69
+ # Rakefile
70
+ File.open("#{app_name}/Rakefile", "w") do |f|
71
+ f.puts("require 'fastr/test/tasks'")
72
+ end
73
+
74
+ # Test Helper
75
+ File.open("#{app_name}/test/test_helper.rb", "w") do |f|
76
+ f.puts("require 'rubygems'")
77
+ f.puts("require 'fastr/test'")
78
+ f.puts("require 'test/unit'")
79
+ f.puts("require 'fastr'")
80
+ f.puts("\nload(File.expand_path('../app/config/init.rb', File.expand_path(File.dirname(__FILE__))))")
81
+ end
82
+
60
83
  puts "#{app_name} initialized!"
61
84
  end
62
85
 
@@ -87,7 +110,20 @@ elsif command == 'generate' and ARGV.length > 2
87
110
 
88
111
  puts "Creating controller: #{path}"
89
112
  File.open(path, "w") do |f|
90
- f.puts("class #{name.capitalize}Controller < Fastr::Controller")
113
+ f.puts("class #{name.camelcase}Controller < Fastr::Controller")
114
+ f.puts("end")
115
+ f.close
116
+ end
117
+
118
+ path = "test/unit/test_#{name}_controller.rb"
119
+ puts "Creating test class: #{path}"
120
+ File.open(path, "w") do |f|
121
+ f.puts("require 'test_helper'\n\n")
122
+ f.puts("class #{name.camelcase}ControllerTest < Test::Unit::TestCase")
123
+ f.puts("\tinclude Fastr::Test::Controller\n\n")
124
+ f.puts("\tdef test_something")
125
+ f.puts("\t\tassert(true)")
126
+ f.puts("\tend")
91
127
  f.puts("end")
92
128
  f.close
93
129
  end
data/lib/fastr.rb CHANGED
@@ -12,4 +12,9 @@ module Fastr
12
12
  autoload :Deferrable, "#{ROOT}/fastr/deferrable"
13
13
  autoload :Settings, "#{ROOT}/fastr/settings"
14
14
  autoload :Plugin, "#{ROOT}/fastr/plugin"
15
+ autoload :Cookie, "#{ROOT}/fastr/cookie"
16
+ autoload :Filter, "#{ROOT}/fastr/filter"
17
+ autoload :Async, "#{ROOT}/fastr/async"
18
+ autoload :HTTP, "#{ROOT}/fastr/http"
19
+ autoload :Dispatch, "#{ROOT}/fastr/dispatch"
15
20
  end
@@ -2,15 +2,34 @@ require 'logger'
2
2
  require 'cgi'
3
3
  require 'mime/types'
4
4
 
5
- module Fastr
5
+ module Fastr
6
+ # This class represents a fastr application.
7
+ # @author Chris Moos
6
8
  class Application
7
9
  include Fastr::Log
8
-
10
+ include Fastr::Dispatch
11
+
12
+ # The file that contains application settings.
9
13
  SETTINGS_FILE = "app/config/settings.rb"
14
+
15
+ # The file that is evaluated when fastr finishes booting.
10
16
  INIT_FILE = "app/config/init.rb"
11
- PUBLIC_FOLDER = "public"
12
17
 
13
- attr_accessor :router, :app_path, :settings, :plugins
18
+ # The router for this application.
19
+ # @return [Fastr::Router]
20
+ attr_accessor :router
21
+
22
+ # The full path the application's path.
23
+ # @return [String]
24
+ attr_accessor :app_path
25
+
26
+ # The settings for this application.
27
+ # @return [Fastr::Settings]
28
+ attr_accessor :settings
29
+
30
+ # The list of plugins enabled for this application.
31
+ # @return [Array]
32
+ attr_accessor :plugins
14
33
 
15
34
  # These are resources we are watching to change.
16
35
  # They will be reloaded upon change.
@@ -28,45 +47,7 @@ module Fastr
28
47
  @booting = true
29
48
  boot
30
49
  end
31
-
32
- # Convenience wrapper for do_dispatch
33
- # This is the heart of the server, called indirectly by a Rack aware server.
34
- def dispatch(env)
35
- return [500, {}, "Server Not Ready"] if @booting
36
-
37
- begin
38
- new_env = plugin_before_dispatch(env)
39
- plugin_after_dispatch(new_env, do_dispatch(new_env))
40
- rescue Exception => e
41
- bt = e.backtrace.join("\n")
42
- [500, {}, "Exception: #{e}\n\n#{bt}"]
43
- end
44
- end
45
-
46
- def plugin_before_dispatch(env)
47
- new_env = env
48
-
49
- self.plugins.each do |plugin|
50
- if plugin.respond_to? :before_dispatch
51
- new_env = plugin.send(:before_dispatch, self, env)
52
- end
53
- end
54
-
55
- new_env
56
- end
57
-
58
- def plugin_after_dispatch(env, response)
59
- new_response = response
60
-
61
- self.plugins.each do |plugin|
62
- if plugin.respond_to? :after_dispatch
63
- new_response = plugin.send(:after_dispatch, self, env, response)
64
- end
65
- end
66
-
67
- new_response
68
- end
69
-
50
+
70
51
  def plugin_after_boot
71
52
  self.plugins.each do |plugin|
72
53
  if plugin.respond_to? :after_boot
@@ -74,97 +55,9 @@ module Fastr
74
55
  end
75
56
  end
76
57
  end
77
-
78
- # Route, instantiate controller, return response from controller's action.
79
- def do_dispatch(env)
80
- path = env['PATH_INFO']
81
-
82
- # Try to serve a public file
83
- ret = dispatch_public(env, path)
84
- return ret if not ret.nil?
85
-
86
- log.debug "Checking for routes that match: #{path}"
87
- route = router.match(env)
88
58
 
89
- if route.has_key? :ok
90
- vars = route[:ok]
91
- controller = vars[:controller]
92
- action = vars[:action]
93
-
94
- raise Fastr::Error.new("Controller and action not present in route") if controller.nil? or action.nil?
95
-
96
-
97
- klass = "#{controller.capitalize}Controller"
98
-
99
- log.info "Routing to controller: #{klass}, action: #{action}"
100
-
101
- obj = Module.const_get(klass).new
102
- setup_controller(obj, env)
103
-
104
- code, hdrs, body = *obj.send(action)
105
-
106
- # Merge headers with anything specified in the controller
107
- hdrs.merge!(obj.headers)
108
-
109
- [code, hdrs, body]
110
- else
111
- [404, {"Content-Type" => "text/plain"}, "404 Not Found: #{path}"]
112
- end
113
- end
114
-
115
59
  private
116
-
117
- def dispatch_public(env, path)
118
- path = "#{self.app_path}/#{PUBLIC_FOLDER}/#{path[1..(path.length - 1)]}"
119
- if not File.directory? path and File.exists? path
120
- f = File.open(path)
121
- hdrs = {}
122
-
123
- type = MIME::Types.type_for(File.basename(path))
124
-
125
- hdrs["Content-Type"] = type.to_s if not type.nil?
126
-
127
- return [200, hdrs, f.read]
128
- else
129
- return nil
130
- end
131
- end
132
-
133
- def setup_controller(controller, env)
134
- controller.env = env
135
- controller.params = {}
136
- controller.headers = {}
137
60
 
138
- CGI::parse(env['QUERY_STRING']).each do |k,v|
139
- if v.length == 1
140
- controller.params[k] = v[0]
141
- else
142
- controller.params[k] = v
143
- end
144
- end
145
-
146
- controller.cookies = get_cookies(env)
147
-
148
-
149
- controller.app = self
150
- end
151
-
152
- def get_cookies(env)
153
- if env.has_key? "HTTP_COOKIE"
154
- cookies = env['HTTP_COOKIE'].split(';')
155
- c = {}
156
- cookies.each do |cookie|
157
- info = cookie.strip.split("=")
158
- if info.length == 2
159
- c[info[0].strip] = info[1].strip
160
- end
161
- end
162
- c
163
- else
164
- {}
165
- end
166
- end
167
-
168
61
  #
169
62
  # This is used to initialize the application.
170
63
  # It runs in a thread because startup depends on EventMachine running
@@ -172,22 +65,21 @@ module Fastr
172
65
  def boot
173
66
  Thread.new do
174
67
  sleep 1 until EM.reactor_running?
175
-
68
+
176
69
  begin
177
70
  log.info "Loading application..."
178
-
71
+ app_init
179
72
  load_settings
180
73
  Fastr::Plugin.load(self)
181
74
  load_app_classes
182
75
  setup_router
183
76
  setup_watcher
184
-
77
+
185
78
  log.info "Application loaded successfully."
186
-
79
+
187
80
  @booting = false
188
-
81
+
189
82
  plugin_after_boot
190
- app_init
191
83
  rescue Exception => e
192
84
  log.error "#{e}"
193
85
  puts e.backtrace
@@ -196,62 +88,77 @@ module Fastr
196
88
  end
197
89
  end
198
90
  end
199
-
91
+
200
92
  # Initializes the router and loads the routes.
201
93
  def setup_router
202
94
  self.router = Fastr::Router.new(self)
203
95
  self.router.load
204
96
  end
205
-
97
+
206
98
  # Loads all application classes. Called on startup.
207
99
  def load_app_classes
208
100
  @@load_paths.each do |name, path|
209
101
  log.debug "Loading #{name} classes..."
210
-
102
+
211
103
  Dir["#{self.app_path}/#{path}"].each do |f|
212
104
  log.debug "Loading: #{f}"
213
105
  load(f)
214
106
  end
215
107
  end
216
108
  end
217
-
109
+
218
110
  def app_init
219
111
  return if not File.exists? INIT_FILE
220
-
112
+
221
113
  init_file = File.open(INIT_FILE)
222
114
  self.instance_eval(init_file.read)
223
115
  end
224
-
116
+
225
117
  def load_settings
226
- return if not File.exists? SETTINGS_FILE
227
-
228
- config_file = File.open(SETTINGS_FILE)
118
+ settings_file = "#{self.app_path}/#{SETTINGS_FILE}"
119
+ return if not File.exists? settings_file
120
+
121
+ config_file = File.open(settings_file)
229
122
  self.instance_eval(config_file.read)
230
123
  end
231
-
124
+
232
125
  # Watch for any file changes in the load paths.
233
126
  def setup_watcher
234
127
  this = self
235
- Handler.send(:define_method, :app) do
128
+ Handler.send(:define_method, :app) do
236
129
  this
237
130
  end
238
-
131
+
239
132
  @@load_paths.each do |name, path|
240
133
  Dir["#{self.app_path}/#{path}"].each do |f|
241
134
  EM.watch_file(f, Handler)
242
135
  end
243
136
  end
244
137
  end
245
-
138
+
246
139
  def config
247
140
  return self.settings
248
141
  end
249
-
142
+
250
143
  module Handler
251
144
  def file_modified
252
145
  app.log.debug "Reloading file: #{path}"
146
+ reload(path)
147
+ end
148
+
149
+ def reload(path)
150
+ filename = File.basename(path)
151
+
152
+ # Is it a controller?
153
+ match = /^((\w+)_controller).rb$/.match(filename)
154
+ reload_controller(match[1]) if not match.nil?
155
+
253
156
  load(path)
254
157
  end
158
+
159
+ def reload_controller(name)
160
+ Object.send(:remove_const, name.camelcase.to_sym)
161
+ end
255
162
  end
256
163
  end
257
- end
164
+ end