waves 0.6.7 → 0.6.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/app/Rakefile +1 -1
  2. data/app/configurations/default.rb.erb +1 -1
  3. data/app/configurations/mapping.rb.erb +6 -37
  4. data/app/controllers/default.rb.erb +10 -20
  5. data/app/lib/tasks/cluster.rb +8 -10
  6. data/app/lib/tasks/generate.rb +15 -0
  7. data/app/lib/tasks/schema.rb +6 -5
  8. data/app/schema/{migration → migrations}/templates/empty.rb.erb +0 -0
  9. data/app/templates/errors/{not_found.mab → not_found_404.mab} +0 -0
  10. data/app/templates/errors/{server_error.mab → server_error_500.mab} +0 -0
  11. data/app/templates/layouts/default.mab +1 -1
  12. data/bin/waves +1 -1
  13. data/bin/waves-console +21 -4
  14. data/lib/controllers/mixin.rb +85 -16
  15. data/lib/dispatchers/base.rb +26 -15
  16. data/lib/dispatchers/default.rb +25 -4
  17. data/lib/helpers/common.rb +50 -10
  18. data/lib/helpers/form.rb +18 -1
  19. data/lib/helpers/formatting.rb +17 -9
  20. data/lib/helpers/model.rb +20 -2
  21. data/lib/helpers/view.rb +12 -2
  22. data/lib/mapping/mapping.rb +187 -0
  23. data/lib/mapping/pretty_urls.rb +95 -0
  24. data/lib/renderers/erubis.rb +3 -1
  25. data/lib/renderers/markaby.rb +6 -4
  26. data/lib/renderers/mixin.rb +12 -0
  27. data/lib/runtime/application.rb +33 -23
  28. data/lib/runtime/configuration.rb +131 -9
  29. data/lib/runtime/console.rb +2 -2
  30. data/lib/runtime/logger.rb +42 -18
  31. data/lib/runtime/mime_types.rb +8 -4
  32. data/lib/runtime/request.rb +14 -5
  33. data/lib/runtime/response.rb +15 -1
  34. data/lib/runtime/response_mixin.rb +29 -47
  35. data/lib/runtime/server.rb +60 -35
  36. data/lib/runtime/session.rb +14 -1
  37. data/lib/utilities/integer.rb +7 -1
  38. data/lib/utilities/kernel.rb +28 -2
  39. data/lib/utilities/module.rb +3 -0
  40. data/lib/utilities/object.rb +5 -1
  41. data/lib/utilities/string.rb +7 -2
  42. data/lib/utilities/symbol.rb +2 -0
  43. data/lib/views/mixin.rb +55 -1
  44. data/lib/waves.rb +9 -2
  45. metadata +10 -8
  46. data/lib/runtime/mapping.rb +0 -82
data/app/Rakefile CHANGED
@@ -1,4 +1,4 @@
1
1
  require 'waves'
2
2
  Waves::Console.load
3
- %w( schema ).each { |task| require "lib/tasks/#{task}.rb" }
3
+ %w( schema cluster generate ).each { |task| require "lib/tasks/#{task}.rb" }
4
4
 
@@ -1,7 +1,7 @@
1
1
  module <%= name %>
2
2
  module Configurations
3
3
  class Default < Waves::Configurations::Default
4
- database :host => host, :adapter => 'mysql', :database => 'blog',
4
+ database :host => host, :adapter => 'mysql', :database => '<%= name.downcase %>',
5
5
  :user => 'root', :password => ''
6
6
  end
7
7
  end
@@ -4,43 +4,12 @@ module <%= name %>
4
4
 
5
5
  module Mapping
6
6
 
7
- extend Waves::Mapping
8
-
9
- name = '([\w\-\_\.\+\@]+)'; model = '([\w\-]+)'
10
-
11
- path %r{^/$} { redirect('/entries') }
12
-
13
- # get all resources for the given model
14
- path %r{^/#{model}/?$} do | model |
15
- use( model ) | controller { all } | view { |data| list( model.plural => data ) }
16
- end
17
-
18
- # get all resources identifiers for the given model in json format
19
- path %r{^/#{model}/?$}, :accept => /json/ do | model |
20
- use( model ) | controller { all } | view { |data| list( model.plural => data ) }
21
- end
22
-
23
- # get the given resource for the given model
24
- path %r{^/#{model}/#{name}/?$} do | model, name |
25
- use(model) | controller { find( name ) } |
26
- view { |data| show( model => data ) }
27
- end
28
-
29
- # display an editor for the given resource / model
30
- path %r{^/#{model}/#{name}/editor/?$} do | model, name |
31
- use(model) | controller { find( name ) } |
32
- view { |data| editor( model => data ) }
33
- end
34
-
35
- # add / update the given resource for the given model
36
- path %r{^/#{model}/#{name}/?$}, :method => :post do | model, name |
37
- use(model) | controller { update( name ) }; redirect( url )
38
- end
39
-
40
- # delete the given resource for the given model
41
- path %r{^/#{model}/#{name}/?$}, :method => :delete do | model, name |
42
- use(model) | controller { delete( name ) }; redirect( "/#{model}/" )
43
- end
7
+ module Mapping
8
+ extend Waves::Mapping
9
+ # your custom rules go here
10
+ include Waves::Mapping::PrettyUrls::RestRules
11
+ include Waves::Mapping::PrettyUrls::GetRules
12
+ end
44
13
 
45
14
  end
46
15
 
@@ -6,31 +6,21 @@ module <%= name %>
6
6
 
7
7
  include Waves::Controllers::Mixin
8
8
 
9
- def all
10
- model.all
11
- end
9
+ def attributes; params[model_name.singular.intern]; end
10
+
11
+ def all; model.all; end
12
12
 
13
- def find( name )
14
- model[ name ]
15
- end
13
+ def find( name ); model[ :name => name ] or not_found; end
16
14
 
17
- def create( attrs = {} )
18
- model.new( attrs )
19
- end
15
+ def create; model.create( attributes ); end
20
16
 
21
- def update
22
- attrs = params[model_name.singular]
23
- if instance = find( attrs[:name] )
24
- instance.set( attrs )
25
- instance.save
26
- else
27
- model.create( attrs )
28
- end
17
+ def update( name )
18
+ instance = find( name )
19
+ instance.set( attributes )
20
+ instance.save_changes
29
21
  end
30
22
 
31
- def delete
32
- model[ name ].destroy
33
- end
23
+ def delete( name ); find( name ).destroy; end
34
24
 
35
25
  end
36
26
 
@@ -1,21 +1,19 @@
1
1
  namespace :cluster do
2
2
 
3
3
  desc 'Start a cluster of waves applications.'
4
- task :start => :stop do |task|
5
- pids = []; path = :log / :pids
6
- Waves::Console.config.ports.each do |port|
7
- pid = fork { exec "waves_server -p #{port} #{ENV['mode']} -d" }
8
- Process.detach( pid ); pids << pid
4
+ task :start do |task|
5
+ ( Waves::Console.config.ports || [ Waves::Console.config.port ] ).each do |port|
6
+ cmd = "waves-server -p #{port} -c #{ENV['mode']||'development'} -d"
7
+ puts cmd ; `#{cmd}`
9
8
  end
10
- File.write( path, pids.join(',') )
11
9
  end
12
10
 
13
11
  desc 'Stop a cluster of waves applications.'
14
12
  task :stop do |task|
15
- path = :log / :pids
16
- if File.exist? path
17
- File.read( path ).split(',').each { |pid| Process.kill( 'INT', pid.to_i ) rescue nil }
18
- FileUtils.rm( path )
13
+ Dir[ :log / '*.pid' ].each do |path|
14
+ pid = File.basename(path,'.pid').to_i
15
+ puts "Stopping process #{pid} ..."
16
+ Process.kill( 'INT', pid ) rescue nil
19
17
  end
20
18
  end
21
19
 
@@ -0,0 +1,15 @@
1
+ namespace :generate do
2
+ desc 'Generate a new model'
3
+ task :model do |task|
4
+ template = File.read :models / 'default.rb'
5
+ File.write( :models / ENV['name'] + '.rb',
6
+ template.gsub('class Default < Sequel::Model',
7
+ "class #{ENV['name'].camel_case} < Sequel::Model(:#{ENV['name'].plural})") )
8
+ end
9
+ desc 'Generate a new controller'
10
+ task :controller do |task|
11
+ template = File.read :controllers / 'default.rb'
12
+ File.write( :controllers / ENV['name'] + '.rb',
13
+ template.gsub('class Default',"class #{ENV['name'].camel_case}") )
14
+ end
15
+ end
@@ -4,15 +4,16 @@ namespace :schema do
4
4
  desc 'Create a new Sequel Migration using name.'
5
5
  task :migration do |task|
6
6
 
7
- version = ( ENV['version'].to_i ||
8
- Sequel::Migrator.get_current_migration_version( Blog.database ) ) + 1
7
+ version = ( ENV['version'].nil? ?
8
+ Sequel::Migrator.get_current_migration_version( Blog.database ) :
9
+ ENV['version'].to_i ) + 1
9
10
 
10
11
  name = ENV['name'] || 'migration'
11
12
  class_name = name.camel_case
12
13
 
13
14
  template = ( ENV['template'] || 'empty' ) + '.rb.erb'
14
- source = :schema / :migration / :templates / template
15
- destination = :schema / :migration / "#{'%03d' % version}_#{name}.rb"
15
+ source = :schema / :migrations / :templates / template
16
+ destination = :schema / :migrations / "#{'%03d' % version}_#{name}.rb"
16
17
  code = Erubis::Eruby.new( File.read( source ) ).result( binding )
17
18
  File.write( destination, code )
18
19
 
@@ -21,7 +22,7 @@ namespace :schema do
21
22
  desc 'Performs migration from version, to version.'
22
23
  task :migrate do |task|
23
24
  version = ENV['version']; version = version.to_i unless version.nil?
24
- Sequel::Migrator.apply( Waves.application.database, :schema / :migration , version )
25
+ Sequel::Migrator.apply( Waves.application.database, :schema / :migrations , version )
25
26
  end
26
27
 
27
28
  end
@@ -1,6 +1,6 @@
1
1
  doctype :html4_strict
2
2
 
3
- html
3
+ html do
4
4
 
5
5
  head do
6
6
  title @title
data/bin/waves CHANGED
@@ -18,7 +18,7 @@ Dir["#{app_name}/**/EMPTY"].each { |path| system "rm #{path}" }
18
18
 
19
19
  # next, process all template files ...
20
20
  Dir["#{app_name}/**/*.erb"].each do |path|
21
- unless path =~ %r{^#{app_name}/(schema/migration/templates|templates)}
21
+ unless path =~ %r{^#{app_name}/(schema/migrations/templates|templates)}
22
22
  name = app_name.camel_case
23
23
  File.write( path.gsub(/\.erb$/,''),
24
24
  Erubis::Eruby.new( File.read( path ) ).result( binding ) )
data/bin/waves-console CHANGED
@@ -1,8 +1,25 @@
1
1
  require 'waves'
2
+ require 'choice'
2
3
 
3
- if ARGV.length > 1
4
- puts STDERR, "Usage: console [mode]"; exit 1
4
+ Choice.options do
5
+ header 'Run a waves application server.'
6
+ header ''
7
+ option :mode do
8
+ short '-c'
9
+ long '--config=CONFIG'
10
+ desc 'Configuration to use.'
11
+ desc 'Defaults to development.'
12
+ cast Symbol
13
+ end
14
+ separator ''
15
+ option :directory do
16
+ short '-D'
17
+ long '--dir=DIR'
18
+ desc 'Directory containing the application.'
19
+ desc 'Defaults to the current directory.'
20
+ end
21
+ separator ''
5
22
  end
6
-
7
- Waves::Console.load( ARGV[0] ? ARGV[0].intern : :development )
23
+
24
+ Waves::Console.load( Choice.choices )
8
25
  require 'irb'; IRB.start
@@ -1,36 +1,105 @@
1
1
  module Waves
2
+
3
+ #
4
+ # Controllers in Waves are simply classes that provide a request / response context for operating on a model. While models are essentially just a way to manage data in an application, controllers manage data in response to a request. For example, a controller updates a model instance using parameters from the request.
5
+ #
6
+ # Controller methods simply return data (a resource), if necessary, that can be then used by views to determine how to render that data. Controllers do not determine which view will be invoked. They are independent of the view; one controller method might be suitable for several different views. In some cases, controllers can choose to directly modify the response and possibly even short-circuit the view entirely. A good example of this is a redirect.
7
+ #
8
+ # Controllers, like Views and Mappings, use the Waves::ResponseMixin to provide a rich context for working with the request and response objects. They can even call other controllers or views using the controllers method. In addition, they provide some basic reflection (access to the model and model_name that corresponds to the class name for the given model) and automatic parameter destructuring. This allows controller methods to access the request parameters as a hash, so that a POST variable named <tt>entry.title</tt> is accessed as <tt>params[:entry][:title]</tt>.
9
+ #
10
+ # Controllers often do not have to be explicitly defined. Instead, one or more default controllers can be defined that are used as exemplars for a given model. By default, the +waves+ command generates a single default, placed in the application's <tt>controllers/default.rb</tt> file. This can be modified to change the default behavior for all controllers. Alternatively, the <tt>rake generate:controller</tt> command can be used to explicitly define a controller.
11
+ #
12
+ # As an example, the code for the default controller is below for the Blog application.
13
+ #
14
+ # module Blog
15
+ # module Controllers
16
+ # class Default
17
+ #
18
+ # # Pick up the default controller methods, like param, url, etc.
19
+ # include Waves::Controllers::Mixin
20
+ #
21
+ # # This gets me the parameters associated with this model
22
+ # def attributes; params[model_name.singular.intern]; end
23
+ #
24
+ # # A simple generic delegator to the model
25
+ # def all; model.all; end
26
+ #
27
+ # # Find a specific instance based on a name or raise a 404
28
+ # def find( name ); model[ :name => name ] or not_found; end
29
+ #
30
+ # # Create a new instance based on the request params
31
+ # def create; model.create( attributes ); end
32
+ #
33
+ # # Update an existing record. find will raise a 404 if not found.
34
+ # def update( name )
35
+ # instance = find( name )
36
+ # instance.set( attributes )
37
+ # instance.save_changes
38
+ # end
39
+ #
40
+ # # Find and delete - or will raise a 404 if it doesn't exist
41
+ # def delete( name ); find( name ).destroy; end
42
+ #
43
+ # end
44
+ # end
45
+ # end
46
+ #
47
+ # Since the mapping file handles "glueing" controllers to views, controllers don't have to be at all concerned with views. They don't have to set instance variables, layouts, or contain logic to select the appropriate view based on the request. All they do is worry about updating the model when necessary based on the request.
2
48
 
3
49
  module Controllers
4
50
 
51
+ #
52
+ # This mixin provides some handy methods for Waves controllers. You will probably
53
+ # want to include it in any controllers you define for your application. The default
54
+ # controllers generated using the +wave+ command already do this.
55
+ #
56
+ # Basically, what the mixin does is adapt the class so that it can be used within
57
+ # mappings (see Waves::Mapping); add some simple reflection to allow controller methods
58
+ # to be written generically (i.e., without reference to a specific model); and provide
59
+ # parameter destructuring for the request parameters.
60
+ #
61
+
5
62
  module Mixin
6
-
63
+
7
64
  attr_reader :request
8
65
 
9
66
  include Waves::ResponseMixin
10
67
 
68
+ # When you include this Mixin, a +process+ class method is added to your class,
69
+ # which accepts a request object and a block. The request object is used to initialize
70
+ # the controller and the block is evaluated using +instance_eval+. This allows the
71
+ # controller to be used within a mapping file.
72
+
11
73
  def self.included( c )
12
74
  def c.process( request, &block )
13
75
  self.new( request ).instance_eval( &block )
14
76
  end
15
77
  end
16
78
 
17
- def initialize( request )
18
- @request = request
19
- end
79
+ def initialize( request ); @request = request; end
20
80
 
81
+ # The params variable is taken from the request object and "destructured", so that
82
+ # a parameter named 'blog.title' becomes:
83
+ #
84
+ # params['blog']['title']
85
+ #
86
+ # If you want to access the original parameters object, you can still do so using
87
+ # +request.parameters+ instead of simply +params+.
88
+ def params; @params ||= destructure(request.params); end
21
89
 
22
- # override to convert 'foo.bar' to 'foo' => 'bar' => value
23
- def params
24
- @params ||= destructure(request.params)
25
- end
26
-
27
- def model_name
28
- self.class.basename.snake_case
29
- end
30
-
31
- def model
32
- Waves.application.models[ model_name.intern ]
33
- end
90
+ # You can access the name of the model related to this controller using this method.
91
+ # It simply takes the basename of the module and converts it to snake case, so if the
92
+ # model uses a different plurality, this won't, in fact, be the model name.
93
+ def model_name; self.class.basename.snake_case; end
94
+
95
+ # This uses the model_name method to attempt to identify the model corresponding to this
96
+ # controller. This allows you to write generic controller methods such as:
97
+ #
98
+ # model.find( name )
99
+ #
100
+ # to find an instance of a given model. Again, the plurality of the controller and
101
+ # model must be the same for this to work.
102
+ def model; Waves.application.models[ model_name.intern ]; end
34
103
 
35
104
  private
36
105
 
@@ -11,29 +11,40 @@ module Waves
11
11
  end
12
12
  end
13
13
 
14
+ # The Base dispatcher simply makes it easier to write dispatchers by inheriting
15
+ # from it. It creates a Waves request, ensures the request processing is done
16
+ # within a mutex, benchmarks the request processing, logs it, and handles common
17
+ # exceptions and redirects. Derived classes need only process the request within
18
+ # their +safe+ method, which takes a Waves::Request and returns a Waves::Response.
19
+
14
20
  class Base
15
21
 
22
+ # Like any Rack application, Waves' dispatchers must provide a call method
23
+ # taking an +env+ parameter.
16
24
  def call( env )
17
25
  Waves::Server.synchronize do
18
26
  request = Waves::Request.new( env )
19
27
  response = request.response
20
- begin
21
- safe( request )
22
- rescue Dispatchers::Redirect => redirect
23
- response.status = '302'
24
- response.location = redirect.path
25
- rescue Dispatchers::NotFoundError => e
26
- html = Waves.application.views[:errors].process( request ) do
27
- not_found_404( :error => e )
28
- end
29
- response.status = '404'
30
- response.content_type = 'text/html'
31
- response.write( html )
32
- end
28
+ t = Benchmark.realtime do
29
+ begin
30
+ safe( request )
31
+ rescue Dispatchers::Redirect => redirect
32
+ response.status = '302'
33
+ response.location = redirect.path
34
+ rescue Dispatchers::NotFoundError => e
35
+ html = Waves.application.views[:errors].process( request ) do
36
+ not_found_404( :error => e )
37
+ end
38
+ response.status = '404'
39
+ response.content_type = 'text/html'
40
+ response.write( html )
41
+ end
42
+ end
43
+ Waves::Logger.info "#{request.method}: #{request.url} handled in #{(t*1000).round} ms."
33
44
  response.finish
34
45
  end
35
- end
36
-
46
+ end
47
+
37
48
  end
38
49
 
39
50
  end
@@ -2,13 +2,34 @@ module Waves
2
2
 
3
3
  module Dispatchers
4
4
 
5
+ #
6
+ # The default dispatcher essentially checks the application's mapping to see
7
+ # what to do with the request URL. It checks before and after filters (wrap
8
+ # filters are just a combination of both) as well as path and url mappings.
9
+ #
10
+ # The default dispatcher also attempts to set the content type based on the
11
+ # MIME type implied by any file extension used in the request URL using Mongrel's
12
+ # MIME types YAML file.
13
+ #
14
+ # You can write your own dispatcher and use it in your application's configuration
15
+ # file. By default, they use the default dispatcher, like this:
16
+ #
17
+ # application do
18
+ # use Rack::ShowExceptions
19
+ # run Waves::Dispatchers::Default.new
20
+ # end
21
+ #
22
+
5
23
  class Default < Base
6
24
 
25
+ # All dispatchers using the Dispatchers::Base to provide thread-safety, logging, etc.
26
+ # must provide a +safe+ method to handle creating a response from a request.
27
+ # Takes a Waves::Request and returns a Waves::Reponse
7
28
  def safe( request )
8
29
 
9
30
  response = request.response
10
-
11
- Waves::Server.reset if Waves::Server.debug?
31
+
32
+ Waves::Server.reload if Waves::Server.debug?
12
33
  response.content_type = Waves::Server.config.mime_types[ request.path ] || 'text/html'
13
34
 
14
35
  mapping = Waves::Server.mapping[ request ]
@@ -16,14 +37,14 @@ module Waves
16
37
  mapping[:before].each do | block, args |
17
38
  ResponseProxy.new(request).instance_exec(*args,&block)
18
39
  end
19
-
40
+
20
41
  block, args = mapping[:action]
21
42
  response.write( ResponseProxy.new(request).instance_exec(*args, &block) )
22
43
 
23
44
  mapping[:after].each do | block, args |
24
45
  ResponseProxy.new(request).instance_exec(*args,&block)
25
46
  end
26
-
47
+
27
48
  end
28
49
 
29
50
  end