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