skates 0.1.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +113 -0
  3. data/Rakefile +143 -0
  4. data/bin/skates +6 -0
  5. data/lib/skates.rb +108 -0
  6. data/lib/skates/base/controller.rb +116 -0
  7. data/lib/skates/base/stanza.rb +23 -0
  8. data/lib/skates/base/view.rb +58 -0
  9. data/lib/skates/client_connection.rb +210 -0
  10. data/lib/skates/component_connection.rb +87 -0
  11. data/lib/skates/generator.rb +139 -0
  12. data/lib/skates/router.rb +101 -0
  13. data/lib/skates/router/dsl.rb +61 -0
  14. data/lib/skates/runner.rb +137 -0
  15. data/lib/skates/xmpp_connection.rb +172 -0
  16. data/lib/skates/xmpp_parser.rb +117 -0
  17. data/lib/skates/xpath_helper.rb +13 -0
  18. data/spec/bin/babylon_spec.rb +0 -0
  19. data/spec/em_mock.rb +42 -0
  20. data/spec/lib/babylon/base/controller_spec.rb +205 -0
  21. data/spec/lib/babylon/base/stanza_spec.rb +15 -0
  22. data/spec/lib/babylon/base/view_spec.rb +92 -0
  23. data/spec/lib/babylon/client_connection_spec.rb +304 -0
  24. data/spec/lib/babylon/component_connection_spec.rb +135 -0
  25. data/spec/lib/babylon/generator_spec.rb +10 -0
  26. data/spec/lib/babylon/router/dsl_spec.rb +72 -0
  27. data/spec/lib/babylon/router_spec.rb +189 -0
  28. data/spec/lib/babylon/runner_spec.rb +213 -0
  29. data/spec/lib/babylon/xmpp_connection_spec.rb +197 -0
  30. data/spec/lib/babylon/xmpp_parser_spec.rb +275 -0
  31. data/spec/lib/babylon/xpath_helper_spec.rb +25 -0
  32. data/spec/spec_helper.rb +34 -0
  33. data/templates/skates/app/controllers/controller.rb +7 -0
  34. data/templates/skates/app/stanzas/stanza.rb +6 -0
  35. data/templates/skates/app/views/view.rb +6 -0
  36. data/templates/skates/config/boot.rb +16 -0
  37. data/templates/skates/config/config.yaml +24 -0
  38. data/templates/skates/config/dependencies.rb +1 -0
  39. data/templates/skates/config/routes.rb +22 -0
  40. data/templates/skates/log/development.log +0 -0
  41. data/templates/skates/log/production.log +0 -0
  42. data/templates/skates/log/test.log +0 -0
  43. data/templates/skates/script/component +36 -0
  44. data/templates/skates/tmp/pids/README +2 -0
  45. data/test/skates_test.rb +7 -0
  46. data/test/test_helper.rb +10 -0
  47. metadata +160 -0
@@ -0,0 +1,101 @@
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 initialize
12
+ @routes = []
13
+ end
14
+
15
+ ##
16
+ # Connected is called by the XmppConnection to indicate that the XMPP connection has been established
17
+ def connected(connection)
18
+ @connection = connection
19
+ end
20
+
21
+ ##
22
+ # Look for the first matching route and calls the corresponding action for the corresponding controller.
23
+ # Sends the response on the XMPP stream/
24
+ def route(xml_stanza)
25
+ route = routes.select{ |r| r.accepts?(xml_stanza) }.first
26
+ return false unless route
27
+ execute_route(route.controller, route.action, xml_stanza)
28
+ end
29
+
30
+ ##
31
+ # Executes the route for the given xml_stanza, by instantiating the controller_name, calling action_name and sending
32
+ # the result to the connection
33
+ def execute_route(controller_name, action_name, stanza = nil)
34
+ begin
35
+ stanza = Kernel.const_get(action_name.capitalize).new(stanza) if stanza
36
+ Skates.logger.info {
37
+ "ROUTING TO #{controller_name}::#{action_name} with #{stanza.class}"
38
+ }
39
+ rescue NameError
40
+ Skates.logger.info {
41
+ "ROUTING TO #{controller_name}::#{action_name} with #{stanza.class}"
42
+ }
43
+ end
44
+ controller = controller_name.new(stanza)
45
+ controller.perform(action_name)
46
+ connection.send_xml(controller.evaluate)
47
+ end
48
+
49
+ ##
50
+ # Throw away all added routes from this router. Helpful for
51
+ # testing.
52
+ def purge_routes!
53
+ @routes = []
54
+ end
55
+
56
+ ##
57
+ # Run the router DSL.
58
+ def draw(&block)
59
+ dsl = Router::DSL.new
60
+ dsl.instance_eval(&block)
61
+ dsl.routes.each do |route|
62
+ raise("Route lacks destination: #{route.inspect}") unless route.is_a?(Route)
63
+ end
64
+ @routes += dsl.routes
65
+ sort
66
+ end
67
+
68
+ private
69
+
70
+ def sort
71
+ @routes.sort! { |r1,r2| r2.priority <=> r1.priority }
72
+ end
73
+ end
74
+
75
+ ##
76
+ # Route class which associate an XPATH match and a priority to a controller and an action
77
+ class Route
78
+
79
+ attr_accessor :priority, :controller, :action, :xpath
80
+
81
+ ##
82
+ # Creates a new route
83
+ def initialize(params)
84
+ raise("No controller given for route") unless params["controller"]
85
+ raise("No action given for route") unless params["action"]
86
+ raise("No xpath given for route") unless params["xpath"]
87
+ @priority = params["priority"] || 0
88
+ @xpath = params["xpath"]
89
+ @controller = Kernel.const_get("#{params["controller"].capitalize}Controller")
90
+ @action = params["action"]
91
+ end
92
+
93
+ ##
94
+ # Checks that the route matches the stanzas and calls the the action on the controller.
95
+ def accepts?(stanza)
96
+ stanza.xpath(@xpath, XpathHelper.new).empty? ? false : self
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,61 @@
1
+ module Skates
2
+ module Router
3
+
4
+ # Creates a simple DSL for stanza routing.
5
+ class DSL
6
+ attr_reader :routes
7
+
8
+ def initialize
9
+ @routes = []
10
+ end
11
+
12
+ # Match an xpath.
13
+ def xpath(path)
14
+ @routes << {"xpath" => path}
15
+ self
16
+ end
17
+
18
+ # Set the priority of the last created route.
19
+ def priority(n)
20
+ set(:priority, n)
21
+ self
22
+ end
23
+
24
+ # Match a disco_info query.
25
+ def disco_info(node = nil)
26
+ disco_for(:info, node)
27
+ end
28
+
29
+ # Match a disco_items query.
30
+ def disco_items(node = nil)
31
+ disco_for(:items, node)
32
+ end
33
+
34
+ # Map a route to a specific controller and action.
35
+ def to(params)
36
+ set(:controller, params[:controller])
37
+ set(:action, params[:action])
38
+ # We now have all the properties we really need to create a route.
39
+ route = Route.new(@routes.pop)
40
+ @routes << route
41
+ self
42
+ end
43
+
44
+ protected
45
+ # We do this magic, or crap depending on your perspective, because we don't know whether we're setting values on a
46
+ # Hash or a Route. We can't create the Route until we have a controller and action.
47
+ def set(property, value)
48
+ last = @routes.last
49
+ last[property.to_s] = value if last.is_a?(Hash)
50
+ last.send("#{property.to_s}=", value) if last.is_a?(Route)
51
+ end
52
+
53
+ def disco_for(type, node = nil)
54
+ str = "//iq[@type='get']/*[namespace(., 'query', 'http://jabber.org/protocol/disco##{type.to_s}')"
55
+ str += " and @node = '#{node}'" if node
56
+ str += "]"
57
+ xpath(str)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,137 @@
1
+ module Skates
2
+
3
+ ##
4
+ # Runner is in charge of running the application.
5
+ class Runner
6
+
7
+ ##
8
+ # Prepares the Application to run.
9
+ def self.prepare(env)
10
+ # Load the configuration
11
+ config_file = File.open('config/config.yaml')
12
+ Skates.config = YAML.load(config_file)[Skates.environment]
13
+
14
+ # Add an outputter to the logger
15
+ log_file = Log4r::RollingFileOutputter.new("#{Skates.environment}", :filename => "log/#{Skates.environment}.log", :trunc => false)
16
+ case Skates.environment
17
+ when "production"
18
+ log_file.formatter = Log4r::PatternFormatter.new(:pattern => "%d (#{Process.pid}) [%l] :: %m", :date_pattern => "%d/%m %H:%M")
19
+ when "development"
20
+ log_file.formatter = Log4r::PatternFormatter.new(:pattern => "%d (#{Process.pid}) [%l] :: %m", :date_pattern => "%d/%m %H:%M")
21
+ else
22
+ log_file.formatter = Log4r::PatternFormatter.new(:pattern => "%d (#{Process.pid}) [%l] :: %m", :date_pattern => "%d/%m %H:%M")
23
+ end
24
+ Skates.logger.add(log_file)
25
+
26
+ # Requiring all models, stanza, controllers
27
+ ['app/models/*.rb', 'app/stanzas/*.rb', 'app/controllers/*_controller.rb'].each do |dir|
28
+ Runner.require_directory(dir)
29
+ end
30
+
31
+ # Create the router
32
+ Skates.router = Skates::StanzaRouter.new
33
+
34
+ # Evaluate routes defined with the new DSL router.
35
+ require 'config/routes.rb'
36
+
37
+ # Caching views
38
+ Skates.cache_views
39
+
40
+ end
41
+
42
+ ##
43
+ # Convenience method to require files in a given directory
44
+ def self.require_directory(path)
45
+ Dir.glob(path).each { |f| require f }
46
+ end
47
+
48
+ ##
49
+ # When run is called, it loads the configuration, the routes and add them into the router
50
+ # It then loads the models.
51
+ # Finally it starts the EventMachine and connect the ComponentConnection
52
+ # You can pass an additional block that will be called upon launching, when the eventmachine has been started.
53
+ def self.run(env)
54
+
55
+ Skates.environment = env
56
+
57
+ # Starting the EventMachine
58
+ EventMachine.epoll
59
+ EventMachine.run do
60
+
61
+ Runner.prepare(env)
62
+
63
+ case Skates.config["application_type"]
64
+ when "client"
65
+ Skates::ClientConnection.connect(Skates.config, self)
66
+ else # By default, we assume it's a component
67
+ Skates::ComponentConnection.connect(Skates.config, self)
68
+ end
69
+
70
+ # And finally, let's allow the application to do all it wants to do after we started the EventMachine!
71
+ yield(self) if block_given?
72
+ end
73
+ end
74
+
75
+ ##
76
+ # Returns the list of connection observers
77
+ def self.connection_observers()
78
+ @@observers ||= Array.new
79
+ end
80
+
81
+ ##
82
+ # Adding a connection observer. These observer will receive on_connected and on_disconnected events.
83
+ def self.add_connection_observer(observer)
84
+ @@observers ||= Array.new
85
+ if observer.ancestors.include? Skates::Base::Controller
86
+ Skates.logger.debug {
87
+ "Added #{observer} to the list of Connection Observers"
88
+ }
89
+ @@observers.push(observer) unless @@observers.include? observer
90
+ else
91
+ Skates.logger.error {
92
+ "Observer can only be Skates::Base::Controller"
93
+ }
94
+ false
95
+ end
96
+ end
97
+
98
+ ##
99
+ # Will be called by the connection class once it is connected to the server.
100
+ # It "plugs" the router and then calls on_connected on the various observers.
101
+ def self.on_connected(connection)
102
+ Skates.router.connected(connection)
103
+ connection_observers.each do |observer|
104
+ Skates.router.execute_route(observer, "on_connected")
105
+ end
106
+ end
107
+
108
+ ##
109
+ # Will be called by the connection class upon disconnection.
110
+ # It stops the event loop and then calls on_disconnected on the various observers.
111
+ def self.on_disconnected()
112
+ connection_observers.each do |conn_obs|
113
+ observer = conn_obs.new
114
+ observer.on_disconnected if observer.respond_to?("on_disconnected")
115
+ end
116
+ EventMachine.stop_event_loop
117
+ end
118
+
119
+ ##
120
+ # Will be called by the connection class when it receives and parses a stanza.
121
+ def self.on_stanza(stanza)
122
+ begin
123
+ Skates.router.route(stanza)
124
+ rescue Skates::NotConnected
125
+ Skates.logger.fatal {
126
+ "#{$!.class} => #{$!.inspect}\n#{$!.backtrace.join("\n")}"
127
+ }
128
+ EventMachine::stop_event_loop
129
+ rescue
130
+ Skates.logger.error {
131
+ "#{$!.class} => #{$!.inspect}\n#{$!.backtrace.join("\n")}"
132
+ }
133
+ end
134
+ end
135
+
136
+ end
137
+ end
@@ -0,0 +1,172 @@
1
+ module Skates
2
+
3
+ ##
4
+ # Connection Exception
5
+ class NotConnected < StandardError; end
6
+
7
+ ##
8
+ # xml-not-well-formed Exception
9
+ class XmlNotWellFormed < StandardError; end
10
+
11
+ ##
12
+ # Error when there is no connection to the host and port.
13
+ class NoConnection < StandardError; end
14
+
15
+ ##
16
+ # Authentication Error (wrong password/jid combination). Used for Clients and Components
17
+ class AuthenticationError < StandardError; end
18
+
19
+ ##
20
+ # Raised when the application tries to send a stanza that might be rejected by the server because it's too long.
21
+ class StanzaTooBig < StandardError; end
22
+
23
+ ##
24
+ # This class is in charge of handling the network connection to the XMPP server.
25
+ class XmppConnection < EventMachine::Connection
26
+
27
+ attr_accessor :jid, :host, :port
28
+
29
+ @@max_stanza_size = 65535
30
+
31
+ ##
32
+ # Maximum Stanza size. Default is 65535
33
+ def self.max_stanza_size
34
+ @@max_stanza_size
35
+ end
36
+
37
+ ##
38
+ # Setter for Maximum Stanza size.
39
+ def self.max_stanza_size=(_size)
40
+ @@max_stanza_size = _size
41
+ end
42
+
43
+ ##
44
+ # Connects the XmppConnection to the right host with the right port.
45
+ # It passes itself (as handler) and the configuration
46
+ # This can very well be overwritten by subclasses.
47
+ def self.connect(params, handler)
48
+ Skates.logger.debug {
49
+ "CONNECTING TO #{params["host"]}:#{params["port"]} with #{handler.inspect} as connection handler" # Very low level Logging
50
+ }
51
+ begin
52
+ EventMachine.connect(params["host"], params["port"], self, params.merge({"handler" => handler}))
53
+ rescue RuntimeError
54
+ Skates.logger.error {
55
+ "CONNECTION ERROR : #{$!.class} => #{$!}" # Very low level Logging
56
+ }
57
+ raise NotConnected
58
+ end
59
+ end
60
+
61
+ ##
62
+ # Called when the connection is completed.
63
+ def connection_completed
64
+ @connected = true
65
+ Skates.logger.debug {
66
+ "CONNECTED"
67
+ } # Very low level Logging
68
+ end
69
+
70
+ ##
71
+ # Called when the connection is terminated and stops the event loop
72
+ def unbind()
73
+ @connected = false
74
+ Skates.logger.debug {
75
+ "DISCONNECTED"
76
+ } # Very low level Logging
77
+ begin
78
+ @handler.on_disconnected() if @handler and @handler.respond_to?("on_disconnected")
79
+ rescue
80
+ Skates.logger.error {
81
+ "on_disconnected failed : #{$!}\n#{$!.backtrace.join("\n")}"
82
+ }
83
+ end
84
+ end
85
+
86
+ ##
87
+ # Instantiate the Handler (called internally by EventMachine)
88
+ def initialize(params = {})
89
+ @connected = false
90
+ @jid = params["jid"]
91
+ @password = params["password"]
92
+ @host = params["host"]
93
+ @port = params["port"]
94
+ @handler = params["handler"]
95
+ @buffer = ""
96
+ end
97
+
98
+ ##
99
+ # Attaches a new parser since the network connection has been established.
100
+ def post_init
101
+ @parser = XmppParser.new(method(:receive_stanza))
102
+ end
103
+
104
+ ##
105
+ # Called when a full stanza has been received and returns it to the central router to be sent to the corresponding controller.
106
+ def receive_stanza(stanza)
107
+ Skates.logger.debug {
108
+ "PARSED : #{stanza.to_xml}"
109
+ }
110
+ # If not handled by subclass (for authentication)
111
+ case stanza.name
112
+ when "stream:error"
113
+ if !stanza.children.empty? and stanza.children.first.name == "xml-not-well-formed"
114
+ Skates.logger.error {
115
+ "DISCONNECTED DUE TO MALFORMED STANZA"
116
+ }
117
+ raise XmlNotWellFormed
118
+ end
119
+ # In any case, we need to close the connection.
120
+ close_connection
121
+ else
122
+ begin
123
+ @handler.on_stanza(stanza) if @handler and @handler.respond_to?("on_stanza")
124
+ rescue
125
+ Skates.logger.error {
126
+ "on_stanza failed : #{$!}\n#{$!.backtrace.join("\n")}"
127
+ }
128
+ end
129
+ end
130
+ end
131
+
132
+ ##
133
+ # Sends the Nokogiri::XML data (after converting to string) on the stream. Eventually it displays this data for debugging purposes.
134
+ def send_xml(xml)
135
+ if xml.is_a? Nokogiri::XML::NodeSet
136
+ xml.each do |element|
137
+ send_chunk(element.to_s)
138
+ end
139
+ else
140
+ send_chunk(xml.to_s)
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ def send_chunk(string)
147
+ raise NotConnected unless @connected
148
+ return if string.blank?
149
+ raise StanzaTooBig, "Stanza Too Big (#{string.length} vs. #{XmppConnection.max_stanza_size})\n #{string}" if string.length > XmppConnection.max_stanza_size
150
+ Skates.logger.debug {
151
+ "SENDING : " + string
152
+ }
153
+ send_data string
154
+ end
155
+
156
+ ##
157
+ # receive_data is called when data is received. It is then passed to the parser.
158
+ def receive_data(data)
159
+ begin
160
+ Skates.logger.debug {
161
+ "RECEIVED : #{data}"
162
+ }
163
+ @parser.push(data)
164
+ rescue
165
+ Skates.logger.error {
166
+ "#{$!}\n#{$!.backtrace.join("\n")}"
167
+ }
168
+ end
169
+ end
170
+ end
171
+
172
+ end