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.
- data/LICENSE +20 -0
- data/README.rdoc +113 -0
- data/Rakefile +143 -0
- data/bin/skates +6 -0
- data/lib/skates.rb +108 -0
- data/lib/skates/base/controller.rb +116 -0
- data/lib/skates/base/stanza.rb +23 -0
- data/lib/skates/base/view.rb +58 -0
- data/lib/skates/client_connection.rb +210 -0
- data/lib/skates/component_connection.rb +87 -0
- data/lib/skates/generator.rb +139 -0
- data/lib/skates/router.rb +101 -0
- data/lib/skates/router/dsl.rb +61 -0
- data/lib/skates/runner.rb +137 -0
- data/lib/skates/xmpp_connection.rb +172 -0
- data/lib/skates/xmpp_parser.rb +117 -0
- data/lib/skates/xpath_helper.rb +13 -0
- data/spec/bin/babylon_spec.rb +0 -0
- data/spec/em_mock.rb +42 -0
- data/spec/lib/babylon/base/controller_spec.rb +205 -0
- data/spec/lib/babylon/base/stanza_spec.rb +15 -0
- data/spec/lib/babylon/base/view_spec.rb +92 -0
- data/spec/lib/babylon/client_connection_spec.rb +304 -0
- data/spec/lib/babylon/component_connection_spec.rb +135 -0
- data/spec/lib/babylon/generator_spec.rb +10 -0
- data/spec/lib/babylon/router/dsl_spec.rb +72 -0
- data/spec/lib/babylon/router_spec.rb +189 -0
- data/spec/lib/babylon/runner_spec.rb +213 -0
- data/spec/lib/babylon/xmpp_connection_spec.rb +197 -0
- data/spec/lib/babylon/xmpp_parser_spec.rb +275 -0
- data/spec/lib/babylon/xpath_helper_spec.rb +25 -0
- data/spec/spec_helper.rb +34 -0
- data/templates/skates/app/controllers/controller.rb +7 -0
- data/templates/skates/app/stanzas/stanza.rb +6 -0
- data/templates/skates/app/views/view.rb +6 -0
- data/templates/skates/config/boot.rb +16 -0
- data/templates/skates/config/config.yaml +24 -0
- data/templates/skates/config/dependencies.rb +1 -0
- data/templates/skates/config/routes.rb +22 -0
- data/templates/skates/log/development.log +0 -0
- data/templates/skates/log/production.log +0 -0
- data/templates/skates/log/test.log +0 -0
- data/templates/skates/script/component +36 -0
- data/templates/skates/tmp/pids/README +2 -0
- data/test/skates_test.rb +7 -0
- data/test/test_helper.rb +10 -0
- 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
|