strelka 0.0.1pre4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.gemtest +0 -0
  2. data/History.rdoc +4 -0
  3. data/IDEAS.textile +174 -0
  4. data/Manifest.txt +38 -0
  5. data/README.rdoc +66 -0
  6. data/Rakefile +64 -0
  7. data/bin/leash +403 -0
  8. data/data/strelka/apps/strelka-admin +65 -0
  9. data/data/strelka/apps/strelka-setup +26 -0
  10. data/data/strelka/bootstrap-config.rb +34 -0
  11. data/data/strelka/templates/admin/console.tmpl +21 -0
  12. data/data/strelka/templates/layout.tmpl +30 -0
  13. data/lib/strelka/app/defaultrouter.rb +85 -0
  14. data/lib/strelka/app/filters.rb +70 -0
  15. data/lib/strelka/app/parameters.rb +64 -0
  16. data/lib/strelka/app/plugins.rb +205 -0
  17. data/lib/strelka/app/routing.rb +140 -0
  18. data/lib/strelka/app/templating.rb +157 -0
  19. data/lib/strelka/app.rb +175 -0
  20. data/lib/strelka/behavior/plugin.rb +36 -0
  21. data/lib/strelka/constants.rb +53 -0
  22. data/lib/strelka/httprequest.rb +52 -0
  23. data/lib/strelka/logging.rb +241 -0
  24. data/lib/strelka/mixins.rb +143 -0
  25. data/lib/strelka/process.rb +19 -0
  26. data/lib/strelka.rb +40 -0
  27. data/spec/data/layout.tmpl +3 -0
  28. data/spec/data/main.tmpl +1 -0
  29. data/spec/lib/constants.rb +32 -0
  30. data/spec/lib/helpers.rb +134 -0
  31. data/spec/strelka/app/defaultrouter_spec.rb +215 -0
  32. data/spec/strelka/app/parameters_spec.rb +74 -0
  33. data/spec/strelka/app/plugins_spec.rb +167 -0
  34. data/spec/strelka/app/routing_spec.rb +139 -0
  35. data/spec/strelka/app/templating_spec.rb +169 -0
  36. data/spec/strelka/app_spec.rb +160 -0
  37. data/spec/strelka/httprequest_spec.rb +54 -0
  38. data/spec/strelka/logging_spec.rb +72 -0
  39. data/spec/strelka_spec.rb +27 -0
  40. metadata +226 -0
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'strelka' unless defined?( Strelka )
4
+ require 'strelka/app' unless defined?( Strelka::App )
5
+
6
+
7
+ # Request/response filters plugin for Strelka::App.
8
+ module Strelka::App::Filters
9
+ extend Strelka::App::Plugin
10
+
11
+ run_before :routing
12
+
13
+ ### Class methods to add to classes with routing.
14
+ module ClassMethods
15
+
16
+ # Default filters hash
17
+ @filters = { :request => [], :response => [], :both => [] }
18
+
19
+ # The list of filters
20
+ attr_reader :filters
21
+
22
+
23
+ ### Get/set the router class to use for mapping requests to handlers to +newclass.
24
+ def filter( which=:both, &block )
25
+ which = which.to_sym
26
+ raise ArgumentError, "invalid filter stage %p; expected one of: %p" %
27
+ [ which, self.filters.keys ] if !self.filters.key?( which )
28
+ self.filters[ which ] << block
29
+ end
30
+
31
+
32
+ ### Return filters which should be applied to requests, i.e., those with a +which+ of
33
+ ### :request or :both.
34
+ def request_filters
35
+ return self.filters[ :request ] + self.filters[ :both ]
36
+ end
37
+
38
+
39
+ ### Return filters which should be applied to responses, i.e., those with a +which+ of
40
+ ### :response or :both.
41
+ def request_filters
42
+ return self.filters[ :both ] + self.filters[ :response ]
43
+ end
44
+
45
+ end # module ClassMethods
46
+
47
+
48
+ ### Apply filters to the given +request+ before yielding back to the App, then apply
49
+ ### filters to the response that comes back.
50
+ def handler( request )
51
+ self.apply_request_filters( request )
52
+ response = super
53
+ self.apply_response_filters( response )
54
+ end
55
+
56
+
57
+ ### Apply :request and :both filters to +request+.
58
+ def apply_request_filters( request )
59
+ self.class.request_filters.each {|filter| filter.call(request) }
60
+ end
61
+
62
+
63
+ ### Apply :both and :response filters to +response+.
64
+ def apply_response_filters( response )
65
+ self.class.response_filters.each {|filter| filter.call(response) }
66
+ end
67
+
68
+ end # module Strelka::App::Filters
69
+
70
+
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'strelka' unless defined?( Strelka )
4
+ require 'strelka/app' unless defined?( Strelka::App )
5
+
6
+ # Parameter declaration for Strelka::Apps
7
+ module Strelka::App::Parameters
8
+ extend Strelka::App::Plugin
9
+
10
+ ### Class methods to add to classes with routing.
11
+ module ClassMethods
12
+
13
+ # Pattern for matching route parameters
14
+ PARAMETER_PATTERN = %r{/:(?<paramname>[a-z]\w*)}i
15
+
16
+ # Param defaults
17
+ PARAMETER_DEFAULT_OPTIONS = {
18
+ :constraint => //,
19
+ :required => false,
20
+ :untaint => false,
21
+ :description => nil,
22
+ }
23
+
24
+
25
+ # Default parameters hash
26
+ @parameters = {}
27
+
28
+ # The hash of declared parameters
29
+ attr_reader :parameters
30
+
31
+
32
+ ### Declare a parameter with the specified +name+ that will be validated using the given
33
+ ### +regexp+.
34
+ def param( name, regexp=nil, *flags )
35
+ Strelka.log.debug "New param %p" % [ name ]
36
+ name = name.to_sym
37
+
38
+ regexp = Regexp.compile( "(?<#{name}>" + regexp.to_s + ")" ) unless
39
+ regexp.names.include?( name.to_s )
40
+ Strelka.log.debug " param constraint is: %p" % [ regexp ]
41
+
42
+ options = PARAMETER_DEFAULT_OPTIONS.dup
43
+ options[ :constraint ] = regexp
44
+ options[ :required ] = true if flags.include?( :required )
45
+ options[ :untaint ] = true if flags.include?( :untaint )
46
+ Strelka.log.debug " param options are: %p" % [ options ]
47
+
48
+ self.parameters[ name ] = options
49
+ end
50
+
51
+
52
+ ### Inheritance hook -- inheriting classes inherit their parents' parameter
53
+ ### declarations, too.
54
+ def inherited( subclass )
55
+ super
56
+ subclass.instance_variable_set( :@parameters, self.parameters.dup )
57
+ end
58
+
59
+ end # module ClassMethods
60
+
61
+
62
+ end # module Strelka::App::Parameters
63
+
64
+
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'strelka' unless defined?( Strelka )
4
+ require 'strelka/app' unless defined?( Strelka::App )
5
+
6
+ # Pluggable functionality mixin for Strelka::App.
7
+ class Strelka::App
8
+
9
+ # The Hash of loaded plugin modules, keyed by their downcased and symbolified
10
+ # name (e.g., Strelka::App::Templating => :templating)
11
+ class << self; attr_reader :loaded_plugins; end
12
+ @loaded_plugins = {}
13
+
14
+
15
+ # Plugin Module extension -- adds registration, sorting, etc.
16
+ module Plugin
17
+ include Comparable
18
+
19
+ ### Mixin hook -- extend including objects instead.
20
+ def self::included( mod )
21
+ mod.extend( self )
22
+ end
23
+
24
+ ### Extension hook -- Extend the given object with methods for setting it
25
+ ### up as a plugin for Strelka::Apps.
26
+ def self::extend_object( object )
27
+ Strelka.log.debug "Extending %p as a Strelka::App::Plugin" % [ object ]
28
+
29
+ super
30
+ name = object.plugin_name
31
+ object.instance_variable_set( :@load_order, {:before => [], :after => []} )
32
+
33
+ Strelka.log.debug " adding %p (%p) to the plugin registry" % [ name, object ]
34
+ Strelka::App.loaded_plugins[ name ] = object
35
+ end
36
+
37
+
38
+ #############################################################
39
+ ### A P P E N D E D M E T H O D S
40
+ #############################################################
41
+
42
+ # An Array of Arrays that tracks which plugins should be installed before and after
43
+ # itself, in that order.
44
+ attr_reader :load_order
45
+
46
+
47
+ ### Comparable operator -- use the plugin load_order to compare Plugin modules.
48
+ def <=>( other_plugin )
49
+ if self.before?( other_plugin ) || other_plugin.after?( self )
50
+ return -1
51
+ elsif self.after?( other_plugin ) || other_plugin.before?( self )
52
+ return 1
53
+ else
54
+ return 0
55
+ end
56
+ end
57
+
58
+
59
+ ### Returns true if the receiver has specified that it should run before +other_plugin+.
60
+ def before?( other_plugin )
61
+ return self.load_order[ :before ].include?( other_plugin.plugin_name )
62
+ end
63
+
64
+
65
+ ### Returns true if the receiver has specified that it should run after +other_plugin+.
66
+ def after?( other_plugin )
67
+ return self.load_order[ :after ].include?( other_plugin.plugin_name )
68
+ end
69
+
70
+
71
+ ### Return the name of the receiving plugin
72
+ def plugin_name
73
+ name = self.name || "anonymous#{self.object_id}"
74
+ name.sub!( /.*::/, '' )
75
+ return name.downcase.to_sym
76
+ end
77
+
78
+
79
+ ### Register the receiver as needing to be run before +other_plugins+ for requests, and
80
+ ### *after* them for responses.
81
+ def run_before( *other_plugins )
82
+ self.load_order[:before] += other_plugins
83
+ end
84
+
85
+
86
+ ### Register the receiver as needing to be run after +other_plugins+ for requests, and
87
+ ### *before* them for responses.
88
+ def run_after( *other_plugins )
89
+ self.load_order[:after] += other_plugins
90
+ end
91
+
92
+ end # module Plugin
93
+
94
+
95
+ # Plugin system
96
+ module Plugins
97
+
98
+ ### Inclusion callback -- add class methods and instance variables without
99
+ ### needing a separate call to #extend.
100
+ def self::included( klass )
101
+ klass.extend( ClassMethods )
102
+ super
103
+ end
104
+
105
+
106
+ ### Extension callback -- add instance variables to extending objects.
107
+ def self::extended( object )
108
+ super
109
+ object.instance_variable_set( :@plugins, {} )
110
+ end
111
+
112
+
113
+ ### Class methods to add to classes with plugins.
114
+ module ClassMethods
115
+
116
+ ### Load the plugin with the given +name+, or nil if
117
+ def load_plugin( name )
118
+
119
+ # Just return Modules as-is
120
+ return name if name.is_a?( Strelka::App::Plugin )
121
+
122
+ unless mod = Strelka::App.loaded_plugins[ name.to_sym ]
123
+ Strelka.log.debug "Loading plugin from strelka/app/#{name}"
124
+ require "strelka/app/#{name}"
125
+ mod = Strelka::App.loaded_plugins[ name.to_sym ] or
126
+ raise "#{name} plugin didn't load correctly."
127
+ end
128
+
129
+ return mod
130
+ end
131
+
132
+
133
+ ### Install the plugin +mod+ in the receiving class.
134
+ def install_plugin( mod )
135
+ Strelka.log.debug " adding %p to %p" % [ mod, self ]
136
+ include( mod )
137
+
138
+ if mod.const_defined?( :ClassMethods )
139
+ cm_mod = mod.const_get(:ClassMethods)
140
+ Strelka.log.debug " adding class methods from %p" % [ cm_mod ]
141
+
142
+ extend( cm_mod )
143
+ cm_mod.instance_variables.each do |ivar|
144
+ Strelka.log.debug " copying class instance variable %s" % [ ivar ]
145
+ ival = cm_mod.instance_variable_get( ivar )
146
+
147
+ # Don't duplicate modules/classes or immediates
148
+ case ival
149
+ when Module, TrueClass, FalseClass, Symbol, Numeric, NilClass
150
+ instance_variable_set( ivar, ival )
151
+ else
152
+ instance_variable_set( ivar, ival.dup )
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+
159
+ ### Load the plugins with the given +names+ and install them.
160
+ def plugins( *names )
161
+ # Load the associated Plugin Module objects
162
+ mods = names.flatten.collect {|name| self.load_plugin(name) }.sort
163
+
164
+ # Now install them in reverse order, as the ancestry array should have them
165
+ # in LIFO order
166
+ mods.reverse.each {|mod| self.install_plugin(mod) }
167
+ end
168
+ alias_method :plugin, :plugins
169
+
170
+ end # module ClassMethods
171
+
172
+
173
+ #
174
+ # :section: Extension Points
175
+ #
176
+
177
+ ### The main extension-point for the plugin system. Strelka::App supers to this method
178
+ ### with a block that processes the actual request, and the plugins implement this
179
+ ### method to add their own functionality.
180
+ def handle_request( request, &block )
181
+ raise LocalJumpError,
182
+ "no block given; plugin supering without preserving arguments?" unless block
183
+ return block.call( request )
184
+ end
185
+
186
+
187
+ ### An alternate extension-point for the plugin system. Plugins can implement this method
188
+ ### to alter or replace the +request+ before the regular request/response cycle begins.
189
+ def fixup_request( request )
190
+ return request
191
+ end
192
+
193
+
194
+ ### An alternate extension-point for the plugin system. Plugins can implement this method
195
+ ### to alter or replace the +response+ to the specified +request+ after the regular
196
+ ### request/response cycle is finished.
197
+ def fixup_response( request, response )
198
+ return response
199
+ end
200
+
201
+ end # module Plugins
202
+
203
+ end # class Strelka::App
204
+
205
+
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'strelka' unless defined?( Strelka )
4
+ require 'strelka/app' unless defined?( Strelka::App )
5
+ require 'strelka/app/defaultrouter' unless defined?( Strelka::App::DefaultRouter )
6
+
7
+ require 'strelka/app/plugins'
8
+
9
+ # Default routing logic for Strelka::Apps
10
+ module Strelka::App::Routing
11
+ extend Strelka::App::Plugin
12
+ include Strelka::Loggable,
13
+ Strelka::Constants
14
+
15
+ run_after :templating, :filters, :parameters
16
+
17
+
18
+ # Class methods to add to classes with routing.
19
+ module ClassMethods
20
+
21
+ # The list of routes to pass to the Router when the application is created
22
+ attr_reader :routes
23
+ @routes = []
24
+
25
+ # The class of object to instantiate for routing
26
+ attr_accessor :routerclass
27
+ @routerclass = Strelka::App::DefaultRouter
28
+
29
+
30
+ ### Return a Hash of the methods defined by routes.
31
+ def route_methods
32
+ return self.instance_methods.grep( /^#{HTTP::RFC2616_VERB_REGEX}\b/ )
33
+ end
34
+
35
+
36
+ ### Define a route for the GET verb and the given +pattern+.
37
+ def get( pattern='', options={}, &block )
38
+ self.add_route( :GET, pattern, options, &block )
39
+ end
40
+
41
+
42
+ ### Define a route for the POST verb and the given +pattern+.
43
+ def post( pattern='', options={}, &block )
44
+ self.add_route( :POST, pattern, options, &block )
45
+ end
46
+
47
+
48
+ ### Get/set the router class to use for mapping requests to handlers to +newclass.
49
+ def router( newclass=nil )
50
+ if newclass
51
+ Strelka.log.info "%p will use the %p router" % [ self, newclass ]
52
+ self.routerclass = newclass
53
+ end
54
+
55
+ return self.routerclass
56
+ end
57
+
58
+
59
+ ### Define a route method for the specified +verb+ and +pattern+ with the
60
+ ### specified +options+, and the +block+ as its body.
61
+ def add_route( verb, pattern, options={}, &block )
62
+
63
+ # Start the name of the route method with the HTTP verb, then split the
64
+ # route pattern into its components
65
+ methodparts = [ verb.upcase ]
66
+ patternparts = self.split_route_pattern( pattern )
67
+ Strelka.log.debug "Split pattern %p into parts: %p" % [ pattern, patternparts ]
68
+
69
+ # Make a method name from the directories and the named captures of the patterns
70
+ # in the route
71
+ patternparts.each do |part|
72
+ if part.is_a?( Regexp )
73
+ methodparts << '_' + part.names.join( '_' )
74
+ else
75
+ methodparts << part
76
+ end
77
+ end
78
+ Strelka.log.debug " route methodname parts are: %p" % [ methodparts ]
79
+ methodname = methodparts.join( '_' )
80
+
81
+ # Define the method using the block from the route as its body
82
+ Strelka.log.debug " adding route method %p for %p route: %p" % [ methodname, verb, block ]
83
+ define_method( methodname, &block )
84
+
85
+ # Now add all the parts to the routes array for the router created by
86
+ # instances
87
+ self.routes << [ verb, patternparts, self.instance_method(methodname), options ]
88
+ end
89
+
90
+
91
+ ### Split the given +pattern+ into its path components and
92
+ def split_route_pattern( pattern )
93
+ pattern.slice!( 0, 1 ) if pattern.start_with?( '/' )
94
+
95
+ return pattern.split( '/' ).collect do |component|
96
+ # Map patterns to their parameter constraint regex
97
+ if component.start_with?( ':' )
98
+ Strelka.log.debug " searching for a param for %p" % [ component ]
99
+ param = self.parameters[ component[1..-1].to_sym ] or
100
+ raise ScriptError, "no parameter %p defined" % [ component ]
101
+ param[ :constraint ]
102
+ else
103
+ component
104
+ end
105
+ end
106
+ end
107
+
108
+
109
+ ### Inheritance hook -- inheriting classes inherit their parents' routes table.
110
+ def inherited( subclass )
111
+ super
112
+ subclass.instance_variable_set( :@routes, self.routes.dup )
113
+ end
114
+
115
+ end # module ClassMethods
116
+
117
+
118
+ ### Create a new router object for each class with Routing.
119
+ def initialize( * )
120
+ super
121
+ @router ||= self.class.routerclass.new( self.class.routes )
122
+ end
123
+
124
+
125
+ # The App's router object
126
+ attr_reader :router
127
+
128
+
129
+ ### Dispatch the request using the Router.
130
+ def handle_request( request, &block )
131
+ if handler = self.router.route_request( request )
132
+ return handler.bind( self ).call( request )
133
+ else
134
+ finish_with HTTP::NOT_FOUND, "The requested resource was not found on this server."
135
+ end
136
+ end
137
+
138
+ end # module Strelka::App::Routing
139
+
140
+
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'inversion'
4
+
5
+ require 'strelka' unless defined?( Strelka )
6
+ require 'strelka/app' unless defined?( Strelka::App )
7
+
8
+ require 'strelka/app/plugins'
9
+
10
+
11
+ # Templating plugin for Strelka::Apps.
12
+ module Strelka::App::Templating
13
+ include Strelka::Constants
14
+ extend Strelka::App::Plugin
15
+
16
+ run_before :routing, :filters
17
+
18
+
19
+ # Class methods to add to classes with templating.
20
+ module ClassMethods
21
+
22
+ # The map of template names to template file paths.
23
+ @template_map = {}
24
+ attr_reader :template_map
25
+
26
+ @layout_template = nil
27
+ attr_accessor :layout_template
28
+
29
+
30
+ ### Get/set the templates declared for the App.
31
+ def templates( newhash=nil )
32
+ if newhash
33
+ self.template_map.merge!( newhash )
34
+ end
35
+
36
+ return self.template_map
37
+ end
38
+
39
+
40
+ ### Declare a template that will act as a wrapper for all other templates
41
+ def layout( tmplpath=nil )
42
+ self.layout_template = tmplpath if tmplpath
43
+ return self.layout_template
44
+ end
45
+
46
+ end # module ClassMethods
47
+
48
+
49
+ ### Preload any templates registered with the template map.
50
+ def initialize( * )
51
+ super
52
+ @template_map = self.load_template_map
53
+ @layout = self.load_layout_template
54
+ end
55
+
56
+
57
+ ######
58
+ public
59
+ ######
60
+
61
+ # The map of template names to Inversion::Template instances.
62
+ attr_reader :template_map
63
+
64
+ # The layout template (an Inversion::Template), if one was declarted
65
+ attr_reader :layout
66
+
67
+
68
+ ### Return the template keyed by the given +name+.
69
+ ### :TODO: Add auto-reloading,
70
+ def template( name )
71
+ template = self.template_map[ name ] or
72
+ raise ArgumentError, "no %p template registered!" % [ name ]
73
+ template.reload if template.changed?
74
+ return template.dup
75
+ end
76
+
77
+
78
+ ### Load instances for all the template paths specified in the App's class
79
+ ### and return them in a hash keyed by name (Symbol).
80
+ def load_template_map
81
+ return self.class.template_map.inject( {} ) do |map, (name, path)|
82
+ map[ name ] = Inversion::Template.load( path )
83
+ map
84
+ end
85
+ end
86
+
87
+
88
+ ### Load an Inversion::Template for the layout template and return it if one was declared.
89
+ ### If none was declared, returns +nil+.
90
+ def load_layout_template
91
+ return nil unless ( lt_path = self.class.layout_template )
92
+ return Inversion::Template.load( lt_path )
93
+ end
94
+
95
+
96
+ ### Intercept responses on the way back out and turn them into a Mongrel2::HTTPResponse
97
+ ### with a String for its entity body. It will take action if the response is one of:
98
+ ###
99
+ ### 1. A Mongrel2::Response with an Inversion::Template as its body.
100
+ ### 2. An Inversion::Template by itself.
101
+ ### 3. A Symbol that matches one of the keys of the registered templates.
102
+ ###
103
+ ### In all three of these cases, the return value will be a Mongrel2::Request with a
104
+ ### body set to the rendered value of the template in question, and with its status
105
+ ### set to '200 OK' unless it is already set to something else.
106
+ ###
107
+ ### If there is a registered layout template, and any of the three cases is true, the
108
+ ### layout template is loaded, its #body attributes set to the content template,
109
+ ### and its rendered output set as the body of the response instead.
110
+ ###
111
+ ### Every other response is returned without modification.
112
+ def handle_request( request, &block )
113
+ response = super
114
+ self.log.debug "Templating: examining %p response." % [ response.class ]
115
+ template = nil
116
+
117
+ # Response is a template name
118
+ if response.is_a?( Symbol ) && self.template_map.key?( response )
119
+ self.log.debug " response is a template name (Symbol); using the %p template" % [ response ]
120
+ template = self.template( response )
121
+ response = request.response
122
+
123
+ # Template object
124
+ elsif response.is_a?( Inversion::Template )
125
+ self.log.debug " response is an %p; wrapping it in a Response object" % [ response.class ]
126
+ template = response
127
+ response = request.response
128
+
129
+ # Template object already in a Response
130
+ elsif response.is_a?( Mongrel2::Response ) && response.body.is_a?( Inversion::Template )
131
+ template = response.body
132
+ self.log.debug " response is a %p in the body of a %p" % [ template.class, response.class ]
133
+
134
+ # Not templated; returned as-is
135
+ else
136
+ self.log.debug " response isn't templated; returning it as-is"
137
+ return response
138
+ end
139
+
140
+ # Wrap the template in a layout if there is one
141
+ if self.layout
142
+ l_template = self.layout.dup
143
+ self.log.debug " wrapping response in layout %p" % [ l_template ]
144
+ l_template.body = template
145
+ template = l_template
146
+ end
147
+
148
+ self.log.debug " rendering the template into the response body"
149
+ response.body = template.render
150
+ response.status ||= HTTP::OK
151
+
152
+ return response
153
+ end
154
+
155
+ end # module Strelka::App::Templating
156
+
157
+