skates 0.1.11

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 (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