radamanthus-skates 0.3.5

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.
@@ -0,0 +1,21 @@
1
+ class Array
2
+ def in_groups_of(number, fill_with = nil)
3
+ if fill_with == false
4
+ collection = self
5
+ else
6
+ # size % number gives how many extra we have;
7
+ # subtracting from number gives how many to add;
8
+ # modulo number ensures we don't add group of just fill.
9
+ padding = (number - size % number) % number
10
+ collection = dup.concat([fill_with] * padding)
11
+ end
12
+
13
+ if block_given?
14
+ collection.each_slice(number) { |slice| yield(slice) }
15
+ else
16
+ returning [] do |groups|
17
+ collection.each_slice(number) { |group| groups << group }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,142 @@
1
+ module Skates
2
+ module Generator
3
+ extend Templater::Manifold
4
+
5
+ desc <<-DESC
6
+ Skates is a framework to generate XMPP Applications in Ruby.
7
+ DESC
8
+
9
+ ##
10
+ # Generates a Skates Application
11
+ class ApplicationGenerator < Templater::Generator
12
+ desc <<-DESC
13
+ Generates the file architecture for a Skates Application. To run, you MUST provide an application name"
14
+ DESC
15
+
16
+ first_argument :application_name, :required => true, :desc => "Your application name."
17
+
18
+ def self.source_root
19
+ File.join(File.dirname(__FILE__), '../../templates/skates')
20
+ end
21
+
22
+ # Create all subsdirectories
23
+ empty_directory :app_directory do |d|
24
+ d.destination = "#{application_name}/app"
25
+ end
26
+ empty_directory :controllers_directory do |d|
27
+ d.destination = "#{application_name}/app/controllers"
28
+ end
29
+ empty_directory :views_directory do |d|
30
+ d.destination = "#{application_name}/app/views"
31
+ end
32
+ empty_directory :views_directory do |d|
33
+ d.destination = "#{application_name}/app/stanzas"
34
+ end
35
+ empty_directory :models_directory do |d|
36
+ d.destination = "#{application_name}/app/models"
37
+ end
38
+ empty_directory :initializers_directory do |d|
39
+ d.destination = "#{application_name}/config/initializers"
40
+ end
41
+ empty_directory :destructors_directory do |d|
42
+ d.destination = "#{application_name}/config/destructors"
43
+ end
44
+ empty_directory :tmp_directory do |d|
45
+ d.destination = "#{application_name}/tmp"
46
+ end
47
+ empty_directory :log_directory do |d|
48
+ d.destination = "#{application_name}/log"
49
+ end
50
+ empty_directory :pid_directory do |d|
51
+ d.destination = "#{application_name}/tmp/pids"
52
+ end
53
+
54
+ # And now add the critical files
55
+ file :boot_file do |f|
56
+ f.source = "#{source_root}/config/boot.rb"
57
+ f.destination = "#{application_name}/config/boot.rb"
58
+ end
59
+ file :config_file do |f|
60
+ f.source = "#{source_root}/config/config.yaml"
61
+ f.destination = "#{application_name}/config/config.yaml"
62
+ end
63
+ file :dependencies_file do |f|
64
+ f.source = "#{source_root}/config/dependencies.rb"
65
+ f.destination = "#{application_name}/config/dependencies.rb"
66
+ end
67
+ file :dependencies_file do |f|
68
+ f.source = "#{source_root}/config/routes.rb"
69
+ f.destination = "#{application_name}/config/routes.rb"
70
+ end
71
+ template :component_file do |f|
72
+ f.source = "#{source_root}/script/component"
73
+ f.destination = "#{application_name}/script/component"
74
+ end
75
+
76
+ end
77
+
78
+ ##
79
+ # Generates a new controller, with the corresponding stanzas and routes.
80
+ class ControllerGenerator < Templater::Generator
81
+ desc <<-DESC
82
+ Generates a new controller for the current Application. It also adds the corresponding routes and actions, based on a Xpath and priority. \nSyntax: skates controller <controller_name> [<action_name>:<priority>:<xpath>],[...]"
83
+ DESC
84
+
85
+ first_argument :controller_name, :required => true, :desc => "Name of the Controller."
86
+ second_argument :actions_arg, :required => true, :as => :array, :default => [], :desc => "Actions implemented by this controller. Use the following syntax : name:priority:xpath"
87
+
88
+ def self.source_root
89
+ File.join(File.dirname(__FILE__), '../../templates/skates/app/controllers')
90
+ end
91
+
92
+ def controller_actions
93
+ @controller_actions ||= actions_arg.map { |a| a.split(":") }
94
+ end
95
+
96
+ def controller_class_name
97
+ "#{controller_name.capitalize}Controller"
98
+ end
99
+
100
+ ##
101
+ # This is a hack since Templater doesn't offer any simple way to edit files right now...
102
+ def add_route_for_actions_in_controller(actions, controller)
103
+ sentinel = "Skates.router.draw do"
104
+ router_path = "config/routes.rb"
105
+ actions.each do |action|
106
+ to_inject = "xpath(\"#{action[2]}\").to(:controller => \"#{controller}\", :action => \"#{action[0]}\").priority(#{action[1]})"
107
+ if File.exist?(router_path)
108
+ content = File.read(router_path).gsub(/(#{Regexp.escape(sentinel)})/mi){|match| "#{match}\n\t#{to_inject}"}
109
+ File.open(router_path, 'wb') { |file| file.write(content) }
110
+ end
111
+ end
112
+ end
113
+
114
+ template :controller do |t|
115
+ t.source = "#{source_root}/controller.rb"
116
+ t.destination = "app/controllers/#{controller_name}_controller.rb"
117
+ self.add_route_for_actions_in_controller(controller_actions, controller_name)
118
+ # This is a hack since Templater doesn't offer any simple way to write several files from one...
119
+ FileUtils.mkdir("app/views/#{controller_name}") unless File.exists?("app/views/#{controller_name}")
120
+ controller_actions.each do |action|
121
+ FileUtils.cp("#{source_root}/../views/view.rb", "app/views/#{controller_name}/#{action[0]}.xml.builder")
122
+ end
123
+
124
+ # And now, let's create the stanza files
125
+ controller_actions.each do |action|
126
+ FileUtils.cp("#{source_root}/../stanzas/stanza.rb", "app/stanzas/#{action[0]}.rb")
127
+ # We need to replace
128
+ # "class Stanza < Skates::Base::Stanza" with "class #{action[0]} < Skates::Base::Stanza"
129
+ content = File.read("app/stanzas/#{action[0]}.rb").gsub("class Stanza < Skates::Base::Stanza", "class #{action[0].capitalize} < Skates::Base::Stanza")
130
+ File.open("app/stanzas/#{action[0]}.rb", 'wb') { |file| file.write(content) }
131
+ end
132
+ end
133
+ end
134
+
135
+ # The generators are added to the manifold, and assigned the names 'wiki' and 'blog'.
136
+ # So you can call them <script name> blog merb-blog-in-10-minutes and
137
+ # <script name> blog merb-wiki-in-10-minutes, respectively
138
+ add :application, ApplicationGenerator
139
+ add :controller, ControllerGenerator
140
+
141
+ end
142
+ end
@@ -0,0 +1,123 @@
1
+ require File.dirname(__FILE__)+"/router/dsl"
2
+
3
+ module Skates
4
+ ##
5
+ # Routers are in charge of sending the right stanzas to the right controllers based on user defined Routes.
6
+ # Each application can have only one!
7
+ class StanzaRouter
8
+
9
+ attr_reader :routes, :connection
10
+
11
+ def before_route(&block)
12
+ @before_route = block
13
+ end
14
+
15
+ def initialize
16
+ @routes = []
17
+ end
18
+
19
+ ##
20
+ # Connected is called by the XmppConnection to indicate that the XMPP connection has been established
21
+ def connected(connection)
22
+ @connection = connection
23
+ end
24
+
25
+ ##
26
+ # Look for the first matching route and calls the corresponding action for the corresponding controller.
27
+ # Sends the response on the XMPP stream/
28
+ # If the before_route callback is defined, it is called.
29
+ # If the callback returns true, then, the route is NOT executed.
30
+ # This callback is very useful when an application wants to redirect any stanza it receives before handling it to the routing mechanism.
31
+ def route(xml_stanza)
32
+ abort = if !@before_route.nil?
33
+ begin
34
+ @before_route.call(xml_stanza)
35
+ rescue
36
+ Skates.logger.info {
37
+ "Failed to execute before_route callback. Resuming Routing"
38
+ }
39
+ false
40
+ end
41
+ end
42
+ if !abort
43
+ route = routes.select{ |r| r.accepts?(xml_stanza) }.first
44
+ return false unless route
45
+ execute_route(route.controller, route.action, xml_stanza)
46
+ else
47
+ # The callback triggered abortion of teh routing mechanism.
48
+ return false
49
+ end
50
+ end
51
+
52
+ ##
53
+ # Executes the route for the given xml_stanza, by instantiating the controller_name, calling action_name and sending
54
+ # the result to the connection
55
+ def execute_route(controller_name, action_name, stanza = nil)
56
+ begin
57
+ stanza = Kernel.const_get(action_name.capitalize).new(stanza) if stanza
58
+ Skates.logger.info {
59
+ "ROUTING TO #{controller_name}::#{action_name} with #{stanza.class}"
60
+ }
61
+ rescue NameError
62
+ Skates.logger.info {
63
+ "ROUTING TO #{controller_name}::#{action_name} with #{stanza.class}"
64
+ }
65
+ end
66
+ controller = controller_name.new(stanza)
67
+ controller.perform(action_name)
68
+ connection.send_xml(controller.evaluate)
69
+ end
70
+
71
+ ##
72
+ # Throw away all added routes from this router. Helpful for
73
+ # testing.
74
+ def purge_routes!
75
+ @routes = []
76
+ end
77
+
78
+ ##
79
+ # Run the router DSL.
80
+ def draw(&block)
81
+ dsl = Router::DSL.new
82
+ dsl.instance_eval(&block)
83
+ dsl.routes.each do |route|
84
+ raise("Route lacks destination: #{route.inspect}") unless route.is_a?(Route)
85
+ end
86
+ @routes += dsl.routes
87
+ sort
88
+ end
89
+
90
+ private
91
+
92
+ def sort
93
+ @routes.sort! { |r1,r2| r2.priority <=> r1.priority }
94
+ end
95
+ end
96
+
97
+ ##
98
+ # Route class which associate an XPATH match and a priority to a controller and an action
99
+ class Route
100
+
101
+ attr_accessor :priority, :controller, :action, :xpath
102
+
103
+ ##
104
+ # Creates a new route
105
+ def initialize(params)
106
+ raise("No controller given for route") unless params["controller"]
107
+ raise("No action given for route") unless params["action"]
108
+ raise("No xpath given for route") unless params["xpath"]
109
+ @priority = params["priority"] || 0
110
+ @xpath = params["xpath"]
111
+ @controller = Kernel.const_get("#{params["controller"].capitalize}Controller")
112
+ @action = params["action"]
113
+ end
114
+
115
+ ##
116
+ # Checks that the route matches the stanzas and calls the the action on the controller.
117
+ def accepts?(stanza)
118
+ stanza.xpath(*@xpath).empty? ? false : self
119
+ end
120
+
121
+ end
122
+
123
+ end
@@ -0,0 +1,48 @@
1
+ module Skates
2
+ module Router
3
+ ##
4
+ # We use this class to assert the ordering of the router DSL
5
+ class OutOfOrder < StandardError; end
6
+
7
+ # Creates a simple DSL for stanza routing.
8
+ class DSL
9
+ attr_reader :routes
10
+
11
+ def initialize
12
+ @routes = []
13
+ end
14
+
15
+ # Match an xpath.
16
+ def xpath(path, namespaces = {})
17
+ @routes << {"xpath" => [path, namespaces]}
18
+ self
19
+ end
20
+
21
+ # Set the priority of the last created route.
22
+ def priority(n)
23
+ route = @routes.last
24
+ raise OutOfOrder unless route.is_a?(Route) # check that this is in the right order
25
+ route.priority = n
26
+ self
27
+ end
28
+
29
+ # Map a route to a specific controller and action.
30
+ def to(params)
31
+ last = @routes.pop
32
+ last["controller"] = params[:controller]
33
+ last["action"] = params[:action]
34
+ # We now have all the properties we really need to create a route.
35
+ @routes << Route.new(last)
36
+ self
37
+ end
38
+
39
+ protected
40
+ def disco_for(type, node = nil)
41
+ str = "//iq[@type='get']/*[namespace(., 'query', 'http://jabber.org/protocol/disco##{type.to_s}')"
42
+ str << " and @node = '#{node}'" if node
43
+ str << "]"
44
+ xpath(str)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,164 @@
1
+ module Skates
2
+
3
+ ##
4
+ # Runner is in charge of running the application.
5
+ class Runner
6
+
7
+ PHI = ((1+Math.sqrt(5))/2.0)
8
+
9
+ ##
10
+ # Prepares the Application to run.
11
+ def self.prepare(env)
12
+ # Load the configuration
13
+ Skates.config = YAML.load_file(Skates.config_file)[Skates.environment]
14
+
15
+ Skates.reopen_logs
16
+
17
+ # Requiring all models, stanza, controllers
18
+ ['app/models/*.rb', 'app/stanzas/*.rb', 'app/controllers/*_controller.rb'].each do |dir|
19
+ Runner.require_directory(dir)
20
+ end
21
+
22
+ # Create the router
23
+ Skates.router = Skates::StanzaRouter.new
24
+
25
+ # Evaluate routes defined with the new DSL router.
26
+ require 'config/routes.rb'
27
+
28
+ # Caching views
29
+ Skates.cache_views
30
+
31
+ #Setting failed connection attemts
32
+ @failed_connections = 0
33
+
34
+ end
35
+
36
+ ##
37
+ # Convenience method to require files in a given directory
38
+ def self.require_directory(path)
39
+ Dir.glob(path).each { |f| require f }
40
+ end
41
+
42
+ ##
43
+ # When run is called, it loads the configuration, the routes and add them into the router
44
+ # It then loads the models.
45
+ # Finally it starts the EventMachine and connect the ComponentConnection
46
+ # You can pass an additional block that will be called upon launching, when the eventmachine has been started.
47
+ def self.run(env)
48
+
49
+ Skates.environment = env
50
+
51
+ # Starting the EventMachine
52
+ EventMachine.epoll
53
+ EventMachine.run do
54
+
55
+ Runner.prepare(env)
56
+
57
+ case Skates.config["application_type"]
58
+ when "client"
59
+ Skates::ClientConnection.connect(Skates.config, self)
60
+ else # By default, we assume it's a component
61
+ Skates::ComponentConnection.connect(Skates.config, self)
62
+ end
63
+
64
+ # And finally, let's allow the application to do all it wants to do after we started the EventMachine!
65
+ yield(self) if block_given?
66
+ end
67
+ end
68
+
69
+ ##
70
+ # Returns the list of connection observers
71
+ def self.connection_observers()
72
+ @@observers ||= Array.new
73
+ end
74
+
75
+ ##
76
+ # Adding a connection observer. These observer will receive on_connected and on_disconnected events.
77
+ def self.add_connection_observer(observer)
78
+ @@observers ||= Array.new
79
+ if observer.ancestors.include? Skates::Base::Controller
80
+ Skates.logger.debug {
81
+ "Added #{observer} to the list of Connection Observers"
82
+ }
83
+ @@observers.push(observer) unless @@observers.include? observer
84
+ else
85
+ Skates.logger.error {
86
+ "Observer can only be Skates::Base::Controller"
87
+ }
88
+ false
89
+ end
90
+ end
91
+
92
+ ##
93
+ # Will be called by the connection class once it is connected to the server.
94
+ # It "plugs" the router and then calls on_connected on the various observers.
95
+ def self.on_connected(connection)
96
+ Skates.router.connected(connection)
97
+ connection_observers.each do |observer|
98
+ Skates.router.execute_route(observer, "on_connected")
99
+ end
100
+
101
+ # Connected so reset failed connection attempts
102
+ @failed_connections = 0
103
+ end
104
+
105
+ ##
106
+ # Will be called by the connection class upon disconnection.
107
+ # It stops the event loop and then calls on_disconnected on the various observers.
108
+ def self.on_disconnected()
109
+ connection_observers.each do |conn_obs|
110
+ observer = conn_obs.new
111
+ observer.on_disconnected if observer.respond_to?("on_disconnected")
112
+ end
113
+
114
+ if Skates.config["auto-reconnect"]
115
+ # Increment failed connection attempts and calculate time to next re-connect
116
+ @failed_connections += 1
117
+ reconnect_in = fib(@failed_connections)
118
+
119
+ EventMachine.add_timer( reconnect_in ) {reconnect} if EM.reactor_running?
120
+
121
+ Skates.logger.error {
122
+ "Disconnected - trying to reconnect in #{reconnect_in} seconds."
123
+ }
124
+ else
125
+ EM.stop_event_loop
126
+ end
127
+ end
128
+
129
+ ##
130
+ # Will be called by the connection class when it receives and parses a stanza.
131
+ def self.on_stanza(stanza)
132
+ begin
133
+ Skates.router.route(stanza)
134
+ rescue Skates::NotConnected
135
+ Skates.logger.fatal {
136
+ "#{$!.class} => #{$!.inspect}\n#{$!.backtrace.join("\n")}"
137
+ }
138
+ EventMachine::stop_event_loop
139
+ rescue
140
+ Skates.logger.error {
141
+ "#{$!.class} => #{$!.inspect}\n#{$!.backtrace.join("\n")}"
142
+ }
143
+ end
144
+ end
145
+
146
+ ##
147
+ # Tries to reconnect
148
+ def self.reconnect
149
+ #Try to reconnect
150
+ case Skates.config["application_type"]
151
+ when "client"
152
+ Skates::ClientConnection.connect(Skates.config, self)
153
+ else # By default, we assume it's a component
154
+ Skates::ComponentConnection.connect(Skates.config, self)
155
+ end
156
+ end
157
+
158
+ ##
159
+ # Helper to calculate the fibonnacci number.
160
+ def self.fib(n)
161
+ (Skates::Runner::PHI**n).round
162
+ end
163
+ end
164
+ end