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
@@ -1,21 +1,61 @@
1
1
  module Waves
2
2
  module Helpers
3
+
4
+ # Common helpers are helpers that are needed for just about any Web page. For example,
5
+ # each page will likely have a layout and a doctype.
6
+
3
7
  module Common
4
8
 
5
- def layout( name, assigns = {}, &block )
6
- assigns[ :content ] = yield
7
- Blog::Views::Layouts.process( request ) do
9
+ DOCTYPES = {
10
+ :html3 => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">',
11
+ :html4_transitional =>
12
+ '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"' <<
13
+ '"http://www.w3.org/TR/html4/loose.dtd">',
14
+ :html4_strict =>
15
+ '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" ' <<
16
+ '"http://www.w3.org/TR/html4/strict.dtd">',
17
+ :html4_frameset =>
18
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN"' <<
19
+ '"http://www.w3.org/TR/html4/frameset.dtd">',
20
+ :xhtml1_transitional =>
21
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"' <<
22
+ '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
23
+ :xhtml1_strict =>
24
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"' <<
25
+ '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
26
+ :xhtml1_frameset =>
27
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"' <<
28
+ '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
29
+ :xhtml2 => '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">'
30
+ }
31
+
32
+ # Invokes a layout view (i.e., a view from the layouts template directory), using
33
+ # the assigns parameter to define instance variables for the view. The block is
34
+ # evaluated and also passed into the view as the +layout_content+ instance variable.
35
+ #
36
+ # You can define a layout just by creating a template and then calling the
37
+ # +layout_content+ accessor when you want to embed the caller's content.
38
+ #
39
+ # == Example
40
+ #
41
+ # doctype :html4_transitional
42
+ # html do
43
+ # title @title # passed as an assigns parameter
44
+ # end
45
+ # body do
46
+ # layout_content
47
+ # end
48
+ #
49
+ def layout( name, assigns = {}, &block )
50
+ assigns[ :layout_content ] = capture(&block)
51
+ self << Waves.application.views[:layouts].process( request ) do
8
52
  send( name, assigns )
9
53
  end
10
54
  end
11
55
 
12
- def doctype(type)
13
- case type
14
- when :html4_strict
15
- '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" ' <<
16
- '"http://www.w3.org/TR/html4/strict.dtd">'
17
- end
18
- end
56
+ # The doctype method simply generates a valid DOCTYPE declaration for your page.
57
+ # Valid options are defined in the +DOCTYPES+ constant.
58
+ def doctype(type) ; self << DOCTYPES[type||:html4_strict] ; end
19
59
 
20
60
  end
21
61
  end
data/lib/helpers/form.rb CHANGED
@@ -2,14 +2,31 @@ module Waves
2
2
 
3
3
  module Helpers
4
4
 
5
+ # Form helpers are used in generating forms. Since Markaby already provides Ruby
6
+ # methods for basic form generation, the focus of this helper is on provide templates
7
+ # to handle things that go beyond the basics. You must define a form template
8
+ # directory with templates for each type of form element you wish to use. The names
9
+ # of the template should match the +type+ option provided in the property method.
10
+ #
11
+ # For example, this code:
12
+ #
13
+ # property :name => 'blog.title', :type => :text, :value => @blog.title
14
+ #
15
+ # will invoke the +text+ form view (the template in +templates/form/text.mab+),
16
+ # passing in the name ('blog.title') and the value (@blog.title) as instance variables.
17
+ #
5
18
  module Form
6
-
19
+
20
+ # This method really is a place-holder for common wrappers around groups of
21
+ # properties. You will usually want to override this. As is, it simply places
22
+ # a DIV element with class 'properties' around the block.
7
23
  def properties(&block)
8
24
  div.properties do
9
25
  yield
10
26
  end
11
27
  end
12
28
 
29
+ # Invokes the form view for the +type+ given in the option.
13
30
  def property( options )
14
31
  self << view( :form, options[:type], options )
15
32
  end
@@ -1,20 +1,28 @@
1
1
  require 'redcloth'
2
2
  module Waves
3
3
  module Helpers
4
+
5
+ # Formatting helpers are used to convert specialized content, like Markaby or
6
+ # Textile, into valid HTML. It also provides common escaping functions.
4
7
  module Formatting
8
+
9
+ # Escape a string as HTML content.
10
+ def escape_html(s); Rack::Utils.escape_html(s); end
11
+
12
+ # Escape a URI, converting quotes and spaces and so on.
13
+ def escape_uri(s); Rack::Utils.escape(s); end
14
+
15
+ # Treat content as Markaby and evaluate (only works within a Markaby template).
16
+ # Used to pull Markaby content from a file or database into a Markaby template.
17
+ def markaby( content ); self << eval( content ); end
5
18
 
6
- # TODO: this won't work inside a erb template!
7
- # but i hate to do a whole new Builder when I'm
8
- # already inside one! test self === Builder?
9
- def mab( content )
10
- eval content
11
- end
12
-
19
+ # Treat content as Textile.
13
20
  def textile( content )
14
- ( ::RedCloth::TEXTILE_TAGS << [ 96.chr, '&8216;'] ).each do |pat,ent|
21
+ return if content.nil? or content.empty?
22
+ ( ::RedCloth::TEXTILE_TAGS << [ 96.chr, '&8216;'] ).each do |pat,ent|
15
23
  content.gsub!( pat, ent.gsub('&','&#') )
16
24
  end
17
- ::RedCloth.new( content ).to_html
25
+ self << ::RedCloth.new( content ).to_html
18
26
  end
19
27
 
20
28
  end
data/lib/helpers/model.rb CHANGED
@@ -1,13 +1,31 @@
1
1
  module Waves
2
2
  module Helpers
3
+
4
+ # Model helpers allow you to directly access a model from within a view.
5
+ # This is useful when creating things like select boxes that need data
6
+ # from anther model. For example, a Markaby select box for authors might look like:
7
+ #
8
+ # select do
9
+ # all(:user).each do |user|
10
+ # option user.full_name, :value => user.id
11
+ # end
12
+ # end
13
+ #
14
+ # You could also use these within a view class to keep model-based logic out
15
+ # of the templates themselves. For example, in the view class you might define
16
+ # a method called +authors+ that returns an array of name / id pairs. This could
17
+ # then be called from the template instead of the model helper.
18
+ #
3
19
  module Model
4
20
 
21
+ # Just like model.all. Returns all the instances of that model.
5
22
  def all( model )
6
- Application.models[ model ].all( domain )
23
+ Waves.application.models[ model ].all( domain )
7
24
  end
8
25
 
26
+ # Finds a specific instance using the name field
9
27
  def find( model, name )
10
- Application.models[ model ].find( domain, name ) rescue nil
28
+ Waves.application.models[ model ][ :name => name ] rescue nil
11
29
  end
12
30
 
13
31
  end
data/lib/helpers/view.rb CHANGED
@@ -1,10 +1,20 @@
1
1
  module Waves
2
2
  module Helpers
3
+
4
+ # View helpers are intended to help reuse views from within other views.
5
+ # Both the +layout+ method in the common helpers and the +property+ method
6
+ # of the form helpers are specialized instance of this.
7
+ #
8
+ # The star of our show here is the +view+ method. This takes a model, view,
9
+ # and assigns hash (which are converted into instance variables in the target
10
+ # view) and returns the result of evaluating the view as content in the current
11
+ # template.
3
12
  module View
4
13
 
14
+ # Invokes the view for the given model, passing the assigns as instance variables.
5
15
  def view( model, view, assigns = {} )
6
- self << Application.views[ model ].process( request ) do
7
- send view, assigns
16
+ self << Waves.application.views[ model ].process( request ) do
17
+ send( view, assigns )
8
18
  end
9
19
  end
10
20
 
@@ -0,0 +1,187 @@
1
+ module Waves
2
+
3
+ # Waves::Mapping is a mixin for defining Waves URI mappings (mapping a request to Ruby code).
4
+ # Mappings can work against the request url, path, and elements of the request (such as the
5
+ # request method or accept header). Mappings may also include before, after, or wrap filters
6
+ # to be run if they match the request. Mappings are created using an appropriate mapping method
7
+ # along with a URL pattern (a string or regular expression), a hash of constraint options, and
8
+ # a block, which is the code to run if the pattern matches.
9
+ #
10
+ # == Examples
11
+ #
12
+ # path %r{^/#{resource}/#{name}/?$} do |resource, name|
13
+ # "Hello from a #{resource} named #{name.capitalize}."
14
+ # end
15
+ #
16
+ # In this example, we are using binding regular expressions defined by +resource+
17
+ # and +name+. The matches are passed into the block as parameters. Thus, this
18
+ # rule, given the URL '/person/john' will return:
19
+ #
20
+ # Hello from a person named John.
21
+ #
22
+ # The given block may simple return a string. The content type is inferred from the request
23
+ # if possible, otherwise it defaults to +text+/+html+.
24
+ #
25
+ # path '/critters', :method => :post do
26
+ # request.content_type
27
+ # end
28
+ #
29
+ # /critters # => 'text/html'
30
+ #
31
+ # In this example, we match against a string and check to make sure that the request is a
32
+ # POST. If so, we return the request content_type. The request (and response) objects are
33
+ # available from within the block implicitly.
34
+ #
35
+ # = Invoking Controllers and Views
36
+ #
37
+ # You may invoking a controller or view method for the primary application by using the
38
+ # corresponding methods, preceded by the +use+ directive.
39
+ #
40
+ # == Examples
41
+ #
42
+ # path %r{^/#{resource}/#{name}/?$} do |resource, name|
43
+ # use( resource ) | controller { find( name ) } |
44
+ # view { | instance | show( resource => instance ) }
45
+ # end
46
+ #
47
+ # In this example, we take the same rule from above but invoke a controller and view method.
48
+ # We use the +use+ directive and the resource parameter to set the MVC instances we're going
49
+ # to use. This is necessary to use the +controller+ or +view+ methods. Each of these take
50
+ # a block as arguments which are evaluated in the context of the instance. The +view+ method
51
+ # can further take an argument which is "piped" from the result of the controller block. This
52
+ # isn't required, but helps to clarify the request processing. Within a view block, a hash
53
+ # may also be passed in, which is converted into instance variables for the view instance. In
54
+ # this example, the +show+ method is assigned to an instance variable with the same name as
55
+ # the resource type.
56
+ #
57
+ # So given the same URL as above - /person/john - what will happen is the +find+ method for
58
+ # the +Person+ controller will be invoked and the result passed to the +Person+ view's +show+
59
+ # method, with +@person+ holding the value returned.
60
+ #
61
+ # Crucially, the controller does not need to know what variables the view depends on. This is
62
+ # the job of the mapping block, to act as the "glue" between the controller and view. The
63
+ # controller and view can thus be completely decoupled and become easier to reuse separately.
64
+ #
65
+ # url 'http://admin.foobar.com:/' do
66
+ # use( admin ) | view { console }
67
+ # end
68
+ #
69
+ # In this example, we are using the +url+ method to map a subdomain of +foobar.com+ to the
70
+ # console method of the Admin view. In this case, we did not need a controller method, so
71
+ # we simply didn't call one.
72
+ #
73
+ # = Mapping Modules
74
+ #
75
+ # You may encapsulate sets of related rules into modules and simply include them into your
76
+ # mapping module. Some rule sets come packaged with Waves, such as PrettyUrls (rules for
77
+ # matching resources using names instead of ids). The simplest way to define such modules for
78
+ # reuse is by defining the +included+ class method for the rules module, and then define
79
+ # the rules using +module_eval+. This will likely be made simpler in a future release, but
80
+ # see the PrettyUrls module for an example of how to do this.
81
+ #
82
+ # *Important:* Using pre-packaged mapping rules does not prevent you from adding to or
83
+ # overriding these rules. However, order does matter, so you should put your own rules
84
+ # ahead of those your may be importing. Also, place rules with constraints (for example,
85
+ # rules that require a POST) ahead of those with no constraints, otherwise the constrainted
86
+ # rules may never be called.
87
+
88
+ module Mapping
89
+
90
+ # If the pattern matches and constraints given by the options hash are satisfied, run the
91
+ # block before running any +path+ or +url+ actions. You can have as many +before+ matches
92
+ # as you want - they will all run, unless one of them calls redirect, generates an
93
+ # unhandled exception, etc.
94
+ def before( pattern, options=nil, &block )
95
+ filters[:before] << [ pattern, options, block ]
96
+ end
97
+
98
+ # Similar to before, except it runs its actions after any matching +url+ or +path+ actions.
99
+ def after( pattern, options=nil, &block )
100
+ filters[:after] << [ pattern, options, block ]
101
+ end
102
+
103
+ # Run the action before and after the matching +url+ or +path+ action.
104
+ def wrap( pattern, options=nil, &block )
105
+ filters[:before] << [ pattern, options, block ]
106
+ filters[:after] << [ pattern, options, block ]
107
+ end
108
+
109
+ # Maps a request to a block. Don't use this method directly unless you know what
110
+ # you're doing. Use +path+ or +url+ instead.
111
+ def map( options, &block )
112
+ pattern = options[:path] || options[:url]
113
+ mapping << [ pattern, options, block ]
114
+ end
115
+
116
+ # Match pattern against the +request.path+, along with satisfying any constraints
117
+ # specified by the options hash. If the pattern matches and the constraints are satisfied,
118
+ # run the block. Only one +path+ or +url+ match will be run (the first one).
119
+ def path( pat, options = {}, &block )
120
+ options[:path] = pat; map( options, &block )
121
+ end
122
+
123
+ # Match pattern against the +request.url+, along with satisfying any constraints
124
+ # specified by the options hash. If the pattern matches and the constraints are satisfied,
125
+ # run the block. Only one +path+ or +url+ match will be run (the first one).
126
+ def url( pat, options = {}, &block )
127
+ options[:url] = pat; map( options, block )
128
+ end
129
+
130
+ # Match the given request against the defined rules. This is typically only called
131
+ # by a dispatcher object, so you shouldn't typically use it directly.
132
+ def []( request )
133
+
134
+ rx = { :before => [], :after => [], :action => nil }
135
+
136
+ ( filters[:before] + filters[:wrap] ).each do | pattern, options, function |
137
+ matches = pattern.match(request.path)
138
+ rx[:before] << [ function, matches[1..-1] ] if matches &&
139
+ ( ! options || satisfy( request, options ))
140
+ end
141
+
142
+ mapping.find do |pattern, options, function|
143
+ matches = pattern.match(request.path)
144
+ rx[:action] = [ function, matches[1..-1] ] if matches &&
145
+ ( ! options || satisfy( request, options ) )
146
+ end
147
+
148
+ ( filters[:after] + filters[:wrap] ).each do | pattern, options, function |
149
+ matches = pattern.match(request.path)
150
+ rx[:after] << [ function, matches[1..-1] ] if matches &&
151
+ ( ! options || satisfy( request, options ))
152
+ end
153
+
154
+ not_found(request) unless rx[:action]
155
+
156
+ return rx
157
+
158
+ end
159
+
160
+ private
161
+
162
+ def mapping; @mapping ||= []; end
163
+
164
+ def filters; @filters ||= { :before => [], :after => [], :wrap => [] }; end
165
+
166
+ def not_found(request)
167
+ raise Waves::Dispatchers::NotFoundError.new( request.url + ' not found.')
168
+ end
169
+
170
+ def satisfy( request, options )
171
+ options.each do |method, param|
172
+ return false unless self.send( method, param, request )
173
+ end
174
+ return true
175
+ end
176
+
177
+ def method( method, request )
178
+ request.method == method
179
+ end
180
+
181
+ # TODO: Add constraint for request accept header
182
+ def accept( format, request )
183
+ end
184
+
185
+ end
186
+
187
+ end
@@ -0,0 +1,95 @@
1
+ module Waves
2
+ module Mapping
3
+
4
+ # A set of pre-packed mapping rules for dealing with pretty URLs (that use names instead
5
+ # of numbers to identify resources). There are two modules.
6
+ # - GetRules, which defines all the GET methods for dealing with named resources
7
+ # - RestRules, which defines add, update, and delete rules using a REST style interface
8
+ #
9
+ module PrettyUrls
10
+
11
+ #
12
+ # GetRules defines the following URL conventions:
13
+ #
14
+ # /resources # => get a list of all instances of resource
15
+ # /resource/name # => get a specific instance of resource with the given name
16
+ # /resource/name/editor # => display an edit page for the given resource
17
+ #
18
+ module GetRules
19
+
20
+ def self.included(target)
21
+
22
+ target.module_eval do
23
+
24
+ extend Waves::Mapping
25
+
26
+ name = '([\w\-\_\.\+\@]+)'; model = '([\w\-]+)'
27
+
28
+ # get all resources for the given model
29
+ path %r{^/#{model}/?$} do | model |
30
+ use( model.singular ) | controller { all } | view { |data| list( model => data ) }
31
+ end
32
+
33
+ # get the given resource for the given model
34
+ path %r{^/#{model}/#{name}/?$} do | model, name |
35
+ use(model) | controller { find( name ) } |
36
+ view { |data| show( model => data ) }
37
+ end
38
+
39
+ # display an editor for the given resource / model
40
+ path %r{^/#{model}/#{name}/editor/?$} do | model, name |
41
+ use(model) | controller { find( name ) } |
42
+ view { |data| editor( model => data ) }
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+
51
+ #
52
+ # RestRules defines the following URL conventions:
53
+ #
54
+ # POST /resources # => add a new resource using the POST variables
55
+ # POST /resource/name # => update the given resource using the POST variables
56
+ # DELETE /resource/name # => delete the given resource
57
+ #
58
+ module RestRules
59
+
60
+ def self.included(target)
61
+
62
+ target.module_eval do
63
+
64
+ extend Waves::Mapping
65
+
66
+ name = '([\w\-\_\.\+\@]+)'; model = '([\w\-]+)'
67
+
68
+ # create a new resource for the given model
69
+ path %r{^/#{model}/?$}, :method => :post do | model |
70
+ use( model.singular )
71
+ instance = controller { create }
72
+ redirect( "/#{model.singular}/#{instance.name}/editor" )
73
+ end
74
+
75
+ # add / update the given resource for the given model
76
+ path %r{^/#{model}/#{name}/?$}, :method => :post do | model, name |
77
+ use(model) | controller { update( name ) }; redirect( url )
78
+ end
79
+
80
+ # delete the given resource for the given model
81
+ path %r{^/#{model}/#{name}/?$}, :method => :delete do | model, name |
82
+ use( model ) | controller { delete( name ) }
83
+ end
84
+
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+
91
+ end
92
+
93
+ end
94
+
95
+ end