radamanthus-skates 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,216 @@
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
+ # This will the host asynscrhonously and calls the block for each IP:Port pair.
33
+ # if the block returns true, no other record will be tried. If it returns false, the block will be called with the next pair.
34
+ def self.resolve(host, &block)
35
+ block.call(false)
36
+ end
37
+
38
+ ##
39
+ # Maximum Stanza size. Default is 65535
40
+ def self.max_stanza_size
41
+ @@max_stanza_size
42
+ end
43
+
44
+ ##
45
+ # Setter for Maximum Stanza size.
46
+ def self.max_stanza_size=(_size)
47
+ @@max_stanza_size = _size
48
+ end
49
+
50
+ ##
51
+ # Connects the XmppConnection to the right host with the right port.
52
+ # It passes itself (as handler) and the configuration
53
+ # This can very well be overwritten by subclasses.
54
+ def self.connect(params, handler)
55
+ if params["host"] =~ /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/
56
+ params["port"] = params["port"] ? params["port"].to_i : 5222
57
+ _connect(params, handler)
58
+ else
59
+ resolve(params["host"]) do |host_info|
60
+ if host_info
61
+ begin
62
+ _connect(params.merge(host_info), handler)
63
+ true # connected! Yay!
64
+ rescue NotConnected
65
+ # It will try the next pair of ip/port
66
+ false
67
+ end
68
+ else
69
+ Skates.logger.error {
70
+ "Sorry, we couldn't resolve #{srv_for_host(params["host"])} to any host that accept XMPP connections. Please provide a params[\"host\"]."
71
+ }
72
+ EM.stop_event_loop
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ ##
79
+ # Called when the connection is completed.
80
+ def connection_completed
81
+ @connected = true
82
+ Skates.logger.debug {
83
+ "CONNECTED"
84
+ } # Very low level Logging
85
+ end
86
+
87
+ ##
88
+ # Called when the connection is terminated and stops the event loop
89
+ def unbind()
90
+ @connected = false
91
+ Skates.logger.debug {
92
+ "DISCONNECTED"
93
+ } # Very low level Logging
94
+ begin
95
+ @handler.on_disconnected() if @handler and @handler.respond_to?("on_disconnected")
96
+ rescue
97
+ Skates.logger.error {
98
+ "on_disconnected failed : #{$!}\n#{$!.backtrace.join("\n")}"
99
+ }
100
+ end
101
+ end
102
+
103
+ ##
104
+ # Instantiate the Handler (called internally by EventMachine)
105
+ def initialize(params = {})
106
+ @connected = false
107
+ @jid = params["jid"]
108
+ @password = params["password"]
109
+ @host = params["host"]
110
+ @port = params["port"]
111
+ @handler = params["handler"]
112
+ @buffer = ""
113
+ end
114
+
115
+ ##
116
+ # Attaches a new parser since the network connection has been established.
117
+ def post_init
118
+ @parser = XmppParser.new(method(:receive_stanza))
119
+ end
120
+
121
+ ##
122
+ # Called when a full stanza has been received and returns it to the central router to be sent to the corresponding controller.
123
+ def receive_stanza(stanza)
124
+ Skates.logger.debug {
125
+ "PARSED : #{stanza.to_xml}"
126
+ }
127
+ # If not handled by subclass (for authentication)
128
+ case stanza.name
129
+ when "stream:error"
130
+ if !stanza.children.empty? and stanza.children.first.name == "xml-not-well-formed"
131
+ Skates.logger.error {
132
+ "DISCONNECTED DUE TO MALFORMED STANZA"
133
+ }
134
+ raise XmlNotWellFormed
135
+ end
136
+ # In any case, we need to close the connection.
137
+ close_connection
138
+ else
139
+ begin
140
+ @handler.on_stanza(stanza) if @handler and @handler.respond_to?("on_stanza")
141
+ rescue
142
+ Skates.logger.error {
143
+ "on_stanza failed : #{$!}\n#{$!.backtrace.join("\n")}"
144
+ }
145
+ end
146
+ end
147
+ end
148
+
149
+ ##
150
+ # Sends the Nokogiri::XML data (after converting to string) on the stream. Eventually it displays this data for debugging purposes.
151
+ def send_xml(xml)
152
+ begin
153
+ if xml.is_a? Nokogiri::XML::NodeSet
154
+ xml.each do |element|
155
+ send_chunk(element.to_s)
156
+ end
157
+ else
158
+ send_chunk(xml.to_s)
159
+ end
160
+ rescue
161
+ Skates.logger.error {
162
+ "SENDING FAILED: #{$!}"
163
+ }
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ def send_chunk(string = "")
170
+ raise NotConnected unless @connected
171
+ return if string == ""
172
+ raise StanzaTooBig, "Stanza Too Big (#{string.length} vs. #{XmppConnection.max_stanza_size})\n #{string}" if string.length > XmppConnection.max_stanza_size
173
+ Skates.logger.debug {
174
+ "SENDING : " + string
175
+ }
176
+ send_data UTF8Cleaner.clean(string)
177
+ end
178
+
179
+ ##
180
+ # receive_data is called when data is received. It is then passed to the parser.
181
+ def receive_data(data)
182
+ data = UTF8Cleaner.clean(data)
183
+ begin
184
+ Skates.logger.debug {
185
+ "RECEIVED : #{data}"
186
+ }
187
+ @parser.push(data)
188
+ rescue
189
+ Skates.logger.error {
190
+ "#{$!}\n#{$!.backtrace.join("\n")}"
191
+ }
192
+ end
193
+ end
194
+
195
+ def self.srv_for_host(host)
196
+ "#{host}"
197
+ end
198
+
199
+ def self._connect(params, handler)
200
+ Skates.logger.debug {
201
+ "CONNECTING TO #{params["host"]}:#{params["port"]} with #{handler.inspect} as connection handler" # Very low level Logging
202
+ }
203
+ begin
204
+ EventMachine.connect(params["host"], params["port"], self, params.merge({"handler" => handler}))
205
+ rescue RuntimeError
206
+ Skates.logger.error {
207
+ "CONNECTION ERROR : #{$!.class} => #{$!}" # Very low level Logging
208
+ }
209
+ raise NotConnected
210
+ end
211
+
212
+ end
213
+
214
+ end
215
+
216
+ end
@@ -0,0 +1,112 @@
1
+ module Skates
2
+
3
+ ##
4
+ # This is the XML SAX Parser that accepts "pushed" content
5
+ class XmppParser < Nokogiri::XML::SAX::Document
6
+
7
+ attr_accessor :elem, :doc, :parser
8
+
9
+ ##
10
+ # Initialize the parser and adds the callback that will be called upon stanza completion
11
+ def initialize(callback)
12
+ @callback = callback
13
+ @buffer = ""
14
+ super()
15
+ reset
16
+ end
17
+
18
+ ##
19
+ # Resets the Pushed SAX Parser.
20
+ def reset
21
+ @parser = Nokogiri::XML::SAX::PushParser.new(self, "UTF-8")
22
+ @elem = @doc = nil
23
+ end
24
+
25
+ ##
26
+ # Pushes the received data to the parser. The parser will then callback the document's methods (start_tag, end_tag... etc)
27
+ def push(data)
28
+ @parser << data
29
+ end
30
+
31
+ ##
32
+ # Adds characters to the current element (being parsed)
33
+ def characters(string)
34
+ @buffer ||= ""
35
+ @buffer << string
36
+ end
37
+
38
+ ##
39
+ # Instantiate a new current Element, adds the corresponding attributes and namespaces.
40
+ # The new element is eventually added to a parent element (if present).
41
+ # If no element is being parsed, then, we create a new document, to which we add this new element as root. (we create one document per stanza to avoid memory problems)
42
+ def start_element(qname, attributes = [])
43
+ clear_characters_buffer
44
+ @doc ||= Nokogiri::XML::Document.new
45
+ @elem ||= @doc # If we have no current element, then, we take the doc
46
+ @elem = @elem.add_child(Nokogiri::XML::Element.new(qname, @doc))
47
+
48
+ add_namespaces_and_attributes_to_current_node(attributes)
49
+
50
+ if @elem.name == "stream:stream"
51
+ # We activate the callback since this element will never end.
52
+ @callback.call(@elem)
53
+ @doc = @elem = nil # Let's prepare for the next stanza
54
+ # And then, we start a new Sax Push Parser
55
+ end
56
+ end
57
+
58
+ ##
59
+ # Clears the characters buffer
60
+ def clear_characters_buffer
61
+ if @buffer && @elem
62
+ @buffer.strip!
63
+ @elem.add_child(Nokogiri::XML::Text.new(@buffer, @doc)) unless @buffer.empty?
64
+ @buffer = nil # empty the buffer
65
+ end
66
+ end
67
+
68
+ ##
69
+ # Terminates the current element and calls the callback
70
+ def end_element(name)
71
+ clear_characters_buffer
72
+ if @elem
73
+ if @elem.parent == @doc
74
+ # If we're actually finishing the stanza (a stanza is always a document's root)
75
+ @callback.call(@elem)
76
+ # We delete the current element and the doc (1 doc per stanza policy)
77
+ @elem = @doc = nil
78
+ else
79
+ @elem = @elem.parent
80
+ end
81
+ else
82
+ # Not sure what to do since it seems we're not processing any element at this time, so how can one end?
83
+ end
84
+ end
85
+
86
+ ##
87
+ # Adds namespaces and attributes. Nokogiri passes them as a array of [[ns_name, ns_url], [ns_name, ns_url]..., key, value, key, value]...
88
+ def add_namespaces_and_attributes_to_current_node(attrs)
89
+ # Namespaces
90
+ attrs.select {|k| k.is_a? Array}.each do |pair|
91
+ set_namespace(pair[0], pair[1])
92
+ # set_normal_attribute(pair[0], pair[1])
93
+ end
94
+ # Attributes
95
+ attrs.select {|k| k.is_a? String}.in_groups_of(2) do |pair|
96
+ set_normal_attribute(pair[0], pair[1])
97
+ end
98
+ end
99
+
100
+ def set_normal_attribute(key, value)
101
+ @elem.set_attribute key, Skates.decode_xml(value)
102
+ end
103
+
104
+ def set_namespace(key, value)
105
+ if key.include? ':'
106
+ @elem.add_namespace(key.split(':').last, value)
107
+ else
108
+ @elem.add_namespace(nil, value)
109
+ end
110
+ end
111
+ end
112
+ end
File without changes
data/spec/em_mock.rb ADDED
@@ -0,0 +1,42 @@
1
+ ##
2
+ # Mock for EventMachine
3
+ module EventMachine
4
+
5
+ def self.mock?
6
+ true
7
+ end
8
+
9
+ ##
10
+ # Mock for the Connection Class
11
+ class Connection
12
+ def self.new(*args)
13
+ allocate.instance_eval do
14
+ # Call a superclass's #initialize if it has one
15
+ initialize(*args)
16
+ # Store signature and run #post_init
17
+ post_init
18
+ self
19
+ end
20
+ end
21
+ end
22
+
23
+ ##
24
+ # Stub for run
25
+ def self.run(proc)
26
+ proc.call
27
+ end
28
+
29
+ ##
30
+ # Stub for epoll
31
+ def self.epoll; end
32
+
33
+ ##
34
+ # Stub! to stop the event loop.
35
+ def self.stop_event_loop; end
36
+
37
+ ##
38
+ # Stub for connect (should return a connection object)
39
+ def self.connect(host, port, handler, params)
40
+ handler.new(params)
41
+ end
42
+ end
@@ -0,0 +1,205 @@
1
+ require File.dirname(__FILE__) + '/../../../spec_helper'
2
+
3
+ describe Skates::Base::Controller do
4
+
5
+ before(:each) do
6
+ Skates.views.stub!(:[]).and_return("") # Stubbing read for view
7
+ end
8
+
9
+ describe ".initialize" do
10
+ before(:each) do
11
+ @params = {:a => "a", :b => 1, :c => {:key => "value"}, :stanza => "<hello>world</hello>"}
12
+ end
13
+
14
+ it "should have a stanza instance" do
15
+ stanza = mock(Object)
16
+ c = Skates::Base::Controller.new(stanza)
17
+
18
+ c.instance_variables.should be_include "@stanza"
19
+ c.instance_variable_get("@stanza").should == stanza
20
+ end
21
+
22
+ it "should not be rendered yet" do
23
+ c = Skates::Base::Controller.new(@params)
24
+ c.rendered.should_not be_true
25
+ end
26
+ end
27
+
28
+ describe ".perform" do
29
+ before(:each) do
30
+ @action = :subscribe
31
+ params = {:stanza => "<hello>world</hello>"}
32
+ @controller = Skates::Base::Controller.new(params)
33
+ @controller.class.send(:define_method, @action) do # Defining the action method
34
+ # Do something
35
+ end
36
+ end
37
+
38
+ it "should setup the action to the param" do
39
+ @controller.perform(@action) do
40
+ # Do something
41
+ end
42
+ @controller.instance_variable_get("@action_name").should == @action
43
+ end
44
+
45
+ it "should call the action" do
46
+ @controller.should_receive(:send).with(@action).and_return()
47
+ @controller.perform(@action) do
48
+ # Do something
49
+ end
50
+ end
51
+
52
+ it "should write an error to the log in case of failure of the action" do
53
+ @controller.stub!(:send).with(@action).and_raise(StandardError)
54
+ Skates.logger.should_receive(:error)
55
+ @controller.perform(@action) do
56
+ # Do something
57
+ end
58
+ end
59
+
60
+ it "should call render" do
61
+ @controller.should_receive(:render)
62
+ @controller.perform(@action) do
63
+ # Do something
64
+ end
65
+ end
66
+ end
67
+
68
+ describe ".render" do
69
+ before(:each) do
70
+ @controller = Skates::Base::Controller.new({})
71
+ @controller.action_name = :subscribe
72
+ end
73
+
74
+ it "should assign a value to view" do
75
+ @controller.render
76
+ @controller.instance_variable_get("@view").should_not be_nil
77
+ end
78
+
79
+ describe "with :nothing option" do
80
+ it "should not render any file" do
81
+ @controller.should_not_receive(:render_for_file)
82
+ @controller.render :nothing => true
83
+ end
84
+ end
85
+
86
+ describe "with no option" do
87
+ it "should call render with default_file_name if no option is provided" do
88
+ @controller.should_receive(:default_template_name)
89
+ @controller.render
90
+ end
91
+ end
92
+
93
+ describe "with an :action option" do
94
+ it "should call render with the file name corresponding to the action given as option" do
95
+ action = :unsubscribe
96
+ @controller.should_receive(:default_template_name).with("#{action}")
97
+ @controller.render(:action => action)
98
+ end
99
+ end
100
+
101
+ describe "with a file option" do
102
+ it "should call render_for_file with the correct path if an option file is provided" do
103
+ file = "myfile"
104
+ @controller.should_receive(:render_for_file)
105
+ @controller.render(:file => file)
106
+ end
107
+ end
108
+
109
+ it "should not render twice when called twice" do
110
+ @controller.render
111
+ @controller.should_not_receive(:render_for_file)
112
+ @controller.render
113
+ end
114
+ end
115
+
116
+ describe ".assigns" do
117
+
118
+ before(:each) do
119
+ @stanza = mock(Skates::Base::Stanza)
120
+ @controller = Skates::Base::Controller.new(@stanza)
121
+ end
122
+
123
+ it "should be a hash" do
124
+ @controller.assigns.should be_an_instance_of(Hash)
125
+ end
126
+
127
+ it "should only contain the @stanza if the action hasn't been called yet" do
128
+ @controller.assigns.should_not be_empty
129
+ @controller.assigns["stanza"].should == @stanza
130
+ end
131
+
132
+ it "should return an hash containing all instance variables defined in the action" do
133
+ vars = {"a" => 1, "b" => "b", "c" => { "d" => 4}}
134
+ class MyController < Skates::Base::Controller
135
+ def do_something
136
+ @a = 1
137
+ @b = "b"
138
+ @c = { "d" => 4 }
139
+ end
140
+ end
141
+ @controller = MyController.new(@stanza)
142
+ @controller.do_something
143
+ @controller.assigns.should == vars.merge("stanza" => @stanza)
144
+ end
145
+ end
146
+
147
+ describe ".evaluate" do
148
+ before(:each) do
149
+ @controller = Skates::Base::Controller.new()
150
+ end
151
+
152
+ it "should evaluate the view" do
153
+ view = mock(Skates::Base::View)
154
+ response = "hello"
155
+ @controller.instance_variable_set("@view", view)
156
+ view.should_receive(:evaluate).and_return(response)
157
+ @controller.evaluate.should == response
158
+ end
159
+ end
160
+
161
+ describe ".view_path" do
162
+ it "should return complete file path to the file given in param" do
163
+ @controller = Skates::Base::Controller.new()
164
+ file_name = "myfile"
165
+ @controller.__send__(:view_path, file_name).should == File.join("app/views", "#{"Skates::Base::Controller".gsub("Controller","").downcase}", file_name)
166
+ end
167
+ end
168
+
169
+ describe ".default_template_name" do
170
+ before(:each) do
171
+ @controller = Skates::Base::Controller.new()
172
+ end
173
+
174
+ it "should return the view file name if a file is given in param" do
175
+ @controller.__send__(:default_template_name, "myaction").should == "myaction.xml.builder"
176
+ end
177
+
178
+ it "should return the view file name based on the action_name if no file has been given" do
179
+ @controller.action_name = "a_great_action"
180
+ @controller.__send__(:default_template_name).should == "a_great_action.xml.builder"
181
+ end
182
+ end
183
+
184
+ describe ".render_for_file" do
185
+
186
+ before(:each) do
187
+ @controller = Skates::Base::Controller.new()
188
+ @block = Proc.new {
189
+ # Do something
190
+ }
191
+ @controller.class.send(:define_method, "action") do # Defining the action method
192
+ # Do something
193
+ end
194
+ @controller.perform(:action, &@block)
195
+ @view = Skates::Base::View.new("path_to_a_file", {})
196
+ end
197
+
198
+ it "should instantiate a new view, with the file provided and the hashed_variables" do
199
+ Skates::Base::View.should_receive(:new).with("path_to_a_file",an_instance_of(Hash)).and_return(@view)
200
+ @controller.__send__(:render_for_file, "path_to_a_file")
201
+ end
202
+
203
+ end
204
+
205
+ end