strelka 0.0.1pre4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+