blather 0.4.8 → 0.4.10

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -148,6 +148,17 @@ Command line options:
148
148
  -h, --help Show this message
149
149
  -v, --version Show version
150
150
 
151
+ ## Health warning:
152
+
153
+ Some parts of Blather will allow you to do stupid things that don't conform to XMPP
154
+ spec. You should exercise caution and read the relevant specifications (indicated in
155
+ the preamble to most relevant classes).
156
+
157
+ # Contributions
158
+
159
+ All contributions are welcome, even encouraged. However, contributions must be
160
+ well tested. If you send me a branch name to merge that'll get my attention faster
161
+ than a change set made directly on master.
151
162
 
152
163
  # Author
153
164
 
@@ -155,7 +166,9 @@ Command line options:
155
166
 
156
167
  ### Contributors
157
168
 
158
- [Nolan Darilek](http://github.com/thewordnerd)
169
+ * [Nolan Darilek](http://github.com/thewordnerd)
170
+ * [Tim Carey-Smith](http://github.com/halorgium)
171
+ * [Ben Langfeld](http://github.com/benlangfeld)
159
172
 
160
173
  # Copyright
161
174
 
data/lib/blather.rb CHANGED
@@ -4,6 +4,7 @@
4
4
  eventmachine
5
5
  nokogiri
6
6
  digest/md5
7
+ digest/sha1
7
8
  logger
8
9
 
9
10
  blather/core_ext/active_support
@@ -21,6 +22,7 @@
21
22
  blather/stanza
22
23
  blather/stanza/iq
23
24
  blather/stanza/iq/query
25
+ blather/stanza/iq/command
24
26
  blather/stanza/iq/roster
25
27
  blather/stanza/disco
26
28
  blather/stanza/disco/disco_info
@@ -46,6 +48,8 @@
46
48
  blather/stanza/pubsub_owner/delete
47
49
  blather/stanza/pubsub_owner/purge
48
50
 
51
+ blather/stanza/x
52
+
49
53
  blather/stream
50
54
  blather/stream/client
51
55
  blather/stream/component
@@ -22,9 +22,11 @@ module XML
22
22
  # and namespace designation
23
23
  def xpath(*paths)
24
24
  paths[0] = paths[0].to_s
25
+
25
26
  if paths.size > 1 && (namespaces = paths.pop).is_a?(Hash)
26
27
  paths << namespaces.inject({}) { |h,v| h[v[0].to_s] = v[1]; h }
27
28
  end
29
+
28
30
  nokogiri_xpath *paths
29
31
  end
30
32
  alias_method :find, :xpath
@@ -0,0 +1,305 @@
1
+ module Blather
2
+ class Stanza
3
+ class Iq
4
+
5
+ # # Command Stanza
6
+ #
7
+ # [XEP-0050 Ad-Hoc Commands](http://xmpp.org/extensions/xep-0050.html)
8
+ #
9
+ # This is a base class for any command based Iq stanzas. It provides a base set
10
+ # of methods for working with command stanzas
11
+ #
12
+ # @handler :command
13
+ class Command < Iq
14
+ VALID_ACTIONS = [:cancel, :execute, :complete, :next, :prev].freeze
15
+ VALID_STATUS = [:executing, :completed, :canceled].freeze
16
+ VALID_NOTE_TYPES = [:info, :warn, :error].freeze
17
+
18
+ register :command, :command, 'http://jabber.org/protocol/commands'
19
+
20
+ # Overrides the parent method to ensure a command node is created
21
+ #
22
+ # @param [:get, :set, :result, :error, nil] type the IQ type
23
+ # @param [String] node the name of the node
24
+ # @param [:cancel, :execute, :complete, :next, :prev, nil] action the command's action
25
+ # @return [Command] a new Command stanza
26
+ def self.new(type = :set, node = nil, action = :execute)
27
+ new_node = super type
28
+ new_node.command
29
+ new_node.node = node
30
+ new_node.action = action
31
+ new_node.new_sessionid!
32
+ new_node
33
+ end
34
+
35
+ # Overrides the parent method to ensure the current command node is destroyed
36
+ #
37
+ # @see Blather::Stanza::Iq#inherit
38
+ def inherit(node)
39
+ command.remove
40
+ super
41
+ end
42
+
43
+ # Command node accessor
44
+ # If a command node exists it will be returned.
45
+ # Otherwise a new node will be created and returned
46
+ #
47
+ # @return [Blather::XMPPNode]
48
+ def command
49
+ c = if self.class.registered_ns
50
+ find_first('command_ns:command', :command_ns => self.class.registered_ns)
51
+ else
52
+ find_first('command')
53
+ end
54
+
55
+ unless c
56
+ (self << (c = XMPPNode.new('command', self.document)))
57
+ c.namespace = self.class.registered_ns
58
+ end
59
+ c
60
+ end
61
+
62
+ # Get the name of the node
63
+ #
64
+ # @return [String, nil]
65
+ def node
66
+ command[:node]
67
+ end
68
+
69
+ # Set the name of the node
70
+ #
71
+ # @param [String, nil] node the new node name
72
+ def node=(node)
73
+ command[:node] = node
74
+ end
75
+
76
+ # Get the sessionid of the command
77
+ #
78
+ # @return [String, nil]
79
+ def sessionid
80
+ command[:sessionid]
81
+ end
82
+
83
+ # Set the sessionid of the command
84
+ #
85
+ # @param [String, nil] sessionid the new sessionid
86
+ def sessionid=(sessionid)
87
+ command[:sessionid] = Digest::SHA1.hexdigest(sessionid)
88
+ end
89
+
90
+ # Generate a new session ID (SHA-1 hash)
91
+ def new_sessionid!
92
+ self.sessionid = "commandsession-"+id
93
+ end
94
+
95
+ # Get the action of the command
96
+ #
97
+ # @return [Symbol, nil]
98
+ def action
99
+ command.read_attr :action, :to_sym
100
+ end
101
+
102
+ # Check if the command action is :cancel
103
+ #
104
+ # @return [true, false]
105
+ def cancel?
106
+ self.action == :cancel
107
+ end
108
+
109
+ # Check if the command action is :execute
110
+ #
111
+ # @return [true, false]
112
+ def execute?
113
+ self.action == :execute
114
+ end
115
+
116
+ # Check if the command action is :complete
117
+ #
118
+ # @return [true, false]
119
+ def complete?
120
+ self.action == :complete
121
+ end
122
+
123
+ # Check if the command action is :next
124
+ #
125
+ # @return [true, false]
126
+ def next?
127
+ self.action == :next
128
+ end
129
+
130
+ # Check if the command action is :prev
131
+ #
132
+ # @return [true, false]
133
+ def prev?
134
+ self.action == :prev
135
+ end
136
+
137
+ # Set the action of the command
138
+ #
139
+ # @param [:cancel, :execute, :complete, :next, :prev] action the new action
140
+ def action=(action)
141
+ if action && !VALID_ACTIONS.include?(action.to_sym)
142
+ raise ArgumentError, "Invalid Action (#{action}), use: #{VALID_ACTIONS*' '}"
143
+ end
144
+ command[:action] = action
145
+ end
146
+
147
+ # Get the status of the command
148
+ #
149
+ # @return [Symbol, nil]
150
+ def status
151
+ command.read_attr :status, :to_sym
152
+ end
153
+
154
+ # Check if the command status is :executing
155
+ #
156
+ # @return [true, false]
157
+ def executing?
158
+ self.status == :executing
159
+ end
160
+
161
+ # Check if the command status is :completed
162
+ #
163
+ # @return [true, false]
164
+ def completed?
165
+ self.status == :completed
166
+ end
167
+
168
+ # Check if the command status is :canceled
169
+ #
170
+ # @return [true, false]
171
+ def canceled?
172
+ self.status == :canceled
173
+ end
174
+
175
+ # Set the status of the command
176
+ #
177
+ # @param [:executing, :completed, :canceled] status the new status
178
+ def status=(status)
179
+ if status && !VALID_STATUS.include?(status.to_sym)
180
+ raise ArgumentError, "Invalid Action (#{statusn}), use: #{VALID_STATUS*' '}"
181
+ end
182
+ command[:status] = status
183
+ end
184
+
185
+ # Command actions accessor
186
+ # If a command actions element exists it will be returned.
187
+ # Otherwise a new actions element will be created and returned
188
+ #
189
+ # @return [Blather::XMPPNode]
190
+ def actions
191
+ a = find_first('actions')
192
+
193
+ unless a
194
+ (self << (a = XMPPNode.new('actions', self.document)))
195
+ end
196
+ a
197
+ end
198
+
199
+ # Get the command's allowed actions
200
+ #
201
+ # @return [[Symbol]]
202
+ def allowed_actions
203
+ a = []
204
+ a << :execute
205
+ actions.children.each do |action|
206
+ a << action.name.to_sym
207
+ end
208
+ a
209
+ end
210
+
211
+ # Add allowed actions to the command
212
+ #
213
+ # @param [[:prev, :next, :complete]] allowed_actions the new allowed actions
214
+ def add_allowed_actions(allowed_actions)
215
+ [allowed_actions].flatten.each do |action|
216
+ if action && !VALID_ACTIONS.include?(action.to_sym)
217
+ raise ArgumentError, "Invalid Action (#{action}), use: #{VALID_ACTIONS*' '}"
218
+ end
219
+ actions << "<#{action.to_s}/>"
220
+ end
221
+ end
222
+
223
+ # Remove allowed actions from the command
224
+ #
225
+ # @param [[:prev, :next, :complete]] disallowed_actions the allowed actions to remove
226
+ def remove_allowed_actions(disallowed_actions)
227
+ [disallowed_actions].flatten.each do |action|
228
+ actions.remove_children action.to_sym
229
+ end
230
+ end
231
+
232
+ # Command note accessor
233
+ # If a command note exists it will be returned.
234
+ # Otherwise a new note will be created and returned
235
+ #
236
+ # @return [Blather::XMPPNode]
237
+ def note
238
+ n = find_first('note')
239
+
240
+ unless n
241
+ (self << (n = XMPPNode.new('note', self.document)))
242
+ end
243
+ n
244
+ end
245
+
246
+ # Get the note_type of the command
247
+ #
248
+ # @return [Symbol, nil]
249
+ def note_type
250
+ note.read_attr :type, :to_sym
251
+ end
252
+
253
+ # Check if the command status is :info
254
+ #
255
+ # @return [true, false]
256
+ def info?
257
+ self.note_type == :info
258
+ end
259
+
260
+ # Check if the command status is :warn
261
+ #
262
+ # @return [true, false]
263
+ def warn?
264
+ self.status == :warn
265
+ end
266
+
267
+ # Check if the command status is :error
268
+ #
269
+ # @return [true, false]
270
+ def error?
271
+ self.status == :error
272
+ end
273
+
274
+ # Set the note_type of the command
275
+ #
276
+ # @param [:executing, :completed, :canceled] note_type the new note_type
277
+ def note_type=(note_type)
278
+ if note_type && !VALID_NOTE_TYPES.include?(note_type.to_sym)
279
+ raise ArgumentError, "Invalid Action (#{note_type}), use: #{VALID_NOTE_TYPES*' '}"
280
+ end
281
+ note[:type] = note_type
282
+ end
283
+
284
+ # Get the text of the command's note
285
+ def note_text
286
+ content_from "note"
287
+ end
288
+
289
+ # Set the command's note text
290
+ #
291
+ # @param [String] note_text the command's new note text
292
+ def note_text=(note_text)
293
+ set_content_for "note", note_text
294
+ end
295
+
296
+ # Returns the command's x:data form child
297
+ def form
298
+ X.new command.find_first('//ns:x', :ns => X.registered_ns)
299
+ end
300
+
301
+ end #Command
302
+
303
+ end #Iq
304
+ end #Stanza
305
+ end
@@ -0,0 +1,351 @@
1
+ module Blather
2
+ class Stanza
3
+ # # X Stanza
4
+ #
5
+ # [XEP-0004 Data Forms](http://xmpp.org/extensions/xep-0004.html)
6
+ #
7
+ # Data Form node that allows for semi-structured data exchange
8
+ #
9
+ # @handler :x
10
+ class X < XMPPNode
11
+ register :x, 'jabber:x:data'
12
+
13
+ VALID_TYPES = [:cancel, :form, :result, :submit].freeze
14
+
15
+ # Create a new X node
16
+ # @param [:cancel, :form, :result, :submit, nil] type the x:form type
17
+ # @param [Array<Array, X::Field>, nil] fields a list of fields.
18
+ # These are passed directly to X::Field.new
19
+ # @return [X] a new X stanza
20
+ def self.new(type = nil, fields = [])
21
+ new_node = super :x
22
+
23
+ case type
24
+ when Nokogiri::XML::Node
25
+ new_node.inherit type
26
+ when Hash
27
+ new_node.type = type[:type]
28
+ new_node.add_fields([type[:fields]])
29
+ else
30
+ new_node.type = type
31
+ new_node.add_fields([fields])
32
+ end
33
+ new_node
34
+ end
35
+
36
+ # The Form's type
37
+ # @return [Symbol]
38
+ def type
39
+ read_attr :type, :to_sym
40
+ end
41
+
42
+ # Set the Form's type
43
+ # @param [:cancel, :form, :result, :submit] type the new type for the form
44
+ def type=(type)
45
+ if type && !VALID_TYPES.include?(type.to_sym)
46
+ raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}"
47
+ end
48
+ write_attr :type, type
49
+ end
50
+
51
+ # List of field objects
52
+ # @return [Blather::Stanza::X::Field]
53
+ def fields
54
+ self.find('ns:field', :ns => self.class.registered_ns).map do |f|
55
+ Field.new f
56
+ end
57
+ end
58
+
59
+ # Add an array of fields to form
60
+ # @param fields the array of fields, passed directly to Field.new
61
+ def add_fields(fields = [])
62
+ [fields].flatten.each { |f| self << Field.new(f) }
63
+ end
64
+
65
+ # Check if the x is of type :cancel
66
+ #
67
+ # @return [true, false]
68
+ def cancel?
69
+ self.type == :cancel
70
+ end
71
+
72
+ # Check if the x is of type :form
73
+ #
74
+ # @return [true, false]
75
+ def form?
76
+ self.type == :form
77
+ end
78
+
79
+ # Check if the x is of type :result
80
+ #
81
+ # @return [true, false]
82
+ def result?
83
+ self.type == :result
84
+ end
85
+
86
+ # Check if the x is of type :submit
87
+ #
88
+ # @return [true, false]
89
+ def submit?
90
+ self.type == :submit
91
+ end
92
+
93
+ # Retrieve the form's instructions
94
+ #
95
+ # @return [String]
96
+ def instructions
97
+ if i = self.find_first('ns:instructions', :ns => self.class.registered_ns)
98
+ i.children.inner_text
99
+ end
100
+ end
101
+
102
+ # Set the form's instructions
103
+ #
104
+ # @param [String] instructions the form's instructions
105
+ def instructions=(instructions)
106
+ self.remove_children :instructions
107
+ self << "<instructions>#{instructions}</instructions>"
108
+ end
109
+
110
+ # Retrieve the form's title
111
+ #
112
+ # @return [String]
113
+ def title
114
+ if t = self.find_first('ns:title', :ns => self.class.registered_ns)
115
+ t.children.inner_text
116
+ end
117
+ end
118
+
119
+ # Set the form's title
120
+ #
121
+ # @param [String] title the form's title
122
+ def title=(title)
123
+ self.remove_children :title
124
+ self << "<title>#{title}</title>"
125
+ end
126
+
127
+ class Field < XMPPNode
128
+ VALID_TYPES = [:boolean, :fixed, :hidden, :"jid-multi", :"jid-single", :"list-multi", :"list-single", :"text-multi", :"text-private", :"text-single"].freeze
129
+
130
+ # Create a new X Field
131
+ # @overload new(node)
132
+ # Imports the XML::Node to create a Field object
133
+ # @param [XML::Node] node the node object to import
134
+ # @overload new(opts = {})
135
+ # Creates a new Field using a hash of options
136
+ # @param [Hash] opts a hash of options
137
+ # @option opts [:boolean, :fixed, :hidden, :"jid-multi", :"jid-single", :"list-multi", :"list-single", :"text-multi", :"text-private", :"text-single"] :type the type of the field
138
+ # @option opts [String] :var the variable for the field
139
+ # @option opts [String] :label the label for the field
140
+ # @overload new(type, var = nil, label = nil)
141
+ # Create a new Field by name
142
+ # @param [:boolean, :fixed, :hidden, :"jid-multi", :"jid-single", :"list-multi", :"list-single", :"text-multi", :"text-private", :"text-single"] type the type of the field
143
+ # @param [String, nil] var the variable for the field
144
+ # @param [String, nil] label the label for the field
145
+ def self.new(type, var = nil, label = nil)
146
+ new_node = super :field
147
+
148
+ case type
149
+ when Nokogiri::XML::Node
150
+ new_node.inherit type
151
+ when Hash
152
+ new_node.type = type[:type]
153
+ new_node.var = type[:var]
154
+ new_node.label = type[:label]
155
+ else
156
+ new_node.type = type
157
+ new_node.var = var
158
+ new_node.label = label
159
+ end
160
+ new_node
161
+ end
162
+
163
+ # The Field's type
164
+ # @return [String]
165
+ def type
166
+ read_attr :type
167
+ end
168
+
169
+ # Set the Field's type
170
+ # @param [#to_sym] type the new type for the field
171
+ def type=(type)
172
+ if type && !VALID_TYPES.include?(type.to_sym)
173
+ raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}"
174
+ end
175
+ write_attr :type, type
176
+ end
177
+
178
+ # The Field's var
179
+ # @return [String]
180
+ def var
181
+ read_attr :var
182
+ end
183
+
184
+ # Set the Field's var
185
+ # @param [String] var the new var for the field
186
+ def var=(var)
187
+ write_attr :var, var
188
+ end
189
+
190
+ # The Field's label
191
+ # @return [String]
192
+ def label
193
+ read_attr :label
194
+ end
195
+
196
+ # Set the Field's label
197
+ # @param [String] label the new label for the field
198
+ def label=(label)
199
+ write_attr :label, label
200
+ end
201
+
202
+ # Get the field's value
203
+ #
204
+ # @param [String]
205
+ def value
206
+ if v = self.find_first('value')
207
+ v.children.inner_text
208
+ end
209
+ end
210
+
211
+ # Set the field's value
212
+ #
213
+ # @param [String] value the field's value
214
+ def value=(value)
215
+ self.remove_children :value
216
+ self << "<value>#{value}</value>"
217
+ end
218
+
219
+ # Get the field's description
220
+ #
221
+ # @param [String]
222
+ def desc
223
+ if d = self.find_first('desc')
224
+ d.children.inner_text
225
+ end
226
+ end
227
+
228
+ # Set the field's description
229
+ #
230
+ # @param [String] description the field's description
231
+ def desc=(description)
232
+ self.remove_children :desc
233
+ self << "<desc>#{description}</desc>"
234
+ end
235
+
236
+ # Get the field's required flag
237
+ #
238
+ # @param [true, false]
239
+ def required?
240
+ self.find_first('required') ? true : false
241
+ end
242
+
243
+ # Set the field's required flag
244
+ #
245
+ # @param [true, false] required the field's required flag
246
+ def required!(required = true)
247
+ if self.find_first('required')
248
+ if required==false
249
+ self.remove_children :required
250
+ end
251
+ else
252
+ if required==true
253
+ self << "<required/>"
254
+ end
255
+ end
256
+ end
257
+
258
+ # Extract list of option objects
259
+ #
260
+ # @return [Blather::Stanza::X::Field::Option]
261
+ def options
262
+ self.find('option').map do |f|
263
+ Option.new f
264
+ end
265
+ end
266
+
267
+ # Add an array of options to field
268
+ # @param options the array of options, passed directly to Option.new
269
+ def add_options(options = [])
270
+ [options].flatten.each { |o| self << Option.new(o) }
271
+ end
272
+
273
+ # Compare two Field objects by type, var and label
274
+ # @param [X::Field] o the Field object to compare against
275
+ # @return [true, false]
276
+ def eql?(o)
277
+ unless o.is_a?(self.class)
278
+ raise "Cannot compare #{self.class} with #{o.class}"
279
+ end
280
+
281
+ o.type == self.type &&
282
+ o.var == self.var &&
283
+ o.label == self.label &&
284
+ o.desc == self.desc &&
285
+ o.required? == self.required? &&
286
+ o.value == self.value
287
+ end
288
+ alias_method :==, :eql?
289
+
290
+ class Option < XMPPNode
291
+ # Create a new X Field Option
292
+ # @overload new(node)
293
+ # Imports the XML::Node to create a Field option object
294
+ # @param [XML::Node] node the node object to import
295
+ # @overload new(opts = {})
296
+ # Creates a new Field option using a hash of options
297
+ # @param [Hash] opts a hash of options
298
+ # @option opts [String] :value the value of the field option
299
+ # @option opts [String] :label the human readable label for the field option
300
+ # @overload new(value, label = nil)
301
+ # Create a new Field option by name
302
+ # @param [String] value the value of the field option
303
+ # @param [String, nil] label the human readable label for the field option
304
+ def self.new(value, label = nil)
305
+ new_node = super :option
306
+
307
+ case value
308
+ when Nokogiri::XML::Node
309
+ new_node.inherit value
310
+ when Hash
311
+ new_node.value = value[:value]
312
+ new_node.label = value[:label]
313
+ else
314
+ new_node.value = value
315
+ new_node.label = label
316
+ end
317
+ new_node
318
+ end
319
+
320
+ # The Field Option's value
321
+ # @return [String]
322
+ def value
323
+ if v = self.find_first('value')
324
+ v.children.inner_text
325
+ end
326
+ end
327
+
328
+ # Set the Field Option's value
329
+ # @param [String] value the new value for the field option
330
+ def value=(v)
331
+ self.remove_children :value
332
+ self << "<value>#{v}</value>"
333
+ end
334
+
335
+ # The Field Option's label
336
+ # @return [String]
337
+ def label
338
+ read_attr :label
339
+ end
340
+
341
+ # Set the Field Option's label
342
+ # @param [String] label the new label for the field option
343
+ def label=(label)
344
+ write_attr :label, label
345
+ end
346
+ end # Option
347
+ end # Field
348
+ end # X
349
+
350
+ end #Stanza
351
+ end
@@ -0,0 +1,159 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. .. spec_helper])
2
+
3
+ def command_xml
4
+ <<-XML
5
+ <iq type='result'
6
+ from='catalog.shakespeare.lit'
7
+ to='romeo@montague.net/orchard'
8
+ id='form2'>
9
+ <command xmlns='http://jabber.org/protocol/commands'
10
+ node='node1'
11
+ action='execute'
12
+ sessionid='dqjiodmqlmakm'>
13
+ <x xmlns='jabber:x:data' type='form'>
14
+ <field var='field-name' type='text-single' label='description' />
15
+ </x>
16
+ </command>
17
+ </iq>
18
+ XML
19
+ end
20
+
21
+ describe Blather::Stanza::Iq::Command do
22
+ it 'registers itself' do
23
+ Blather::XMPPNode.class_from_registration(:command, 'http://jabber.org/protocol/commands').must_equal Blather::Stanza::Iq::Command
24
+ end
25
+
26
+ it 'must be importable' do
27
+ Blather::XMPPNode.import(parse_stanza(command_xml).root).must_be_instance_of Blather::Stanza::Iq::Command
28
+ end
29
+
30
+ it 'ensures a command node is present on create' do
31
+ c = Blather::Stanza::Iq::Command.new
32
+ c.xpath('xmlns:command', :xmlns => Blather::Stanza::Iq::Command.registered_ns).wont_be_empty
33
+ end
34
+
35
+ it 'ensures a command node exists when calling #command' do
36
+ c = Blather::Stanza::Iq::Command.new
37
+ c.remove_children :command
38
+ c.xpath('ns:command', :ns => Blather::Stanza::Iq::Command.registered_ns).must_be_empty
39
+
40
+ c.command.wont_be_nil
41
+ c.xpath('ns:command', :ns => Blather::Stanza::Iq::Command.registered_ns).wont_be_empty
42
+ end
43
+
44
+ Blather::Stanza::Iq::Command::VALID_ACTIONS.each do |valid_action|
45
+ it "provides a helper (#{valid_action}?) for action #{valid_action}" do
46
+ Blather::Stanza::Iq::Command.new.must_respond_to :"#{valid_action}?"
47
+ end
48
+ end
49
+
50
+ Blather::Stanza::Iq::Command::VALID_STATUS.each do |valid_status|
51
+ it "provides a helper (#{valid_status}?) for status #{valid_status}" do
52
+ Blather::Stanza::Iq::Command.new.must_respond_to :"#{valid_status}?"
53
+ end
54
+ end
55
+
56
+ Blather::Stanza::Iq::Command::VALID_NOTE_TYPES.each do |valid_note_type|
57
+ it "provides a helper (#{valid_note_type}?) for note_type #{valid_note_type}" do
58
+ Blather::Stanza::Iq::Command.new.must_respond_to :"#{valid_note_type}?"
59
+ end
60
+ end
61
+
62
+ [:cancel, :execute, :complete, :next, :prev].each do |action|
63
+ it "action can be set as \"#{action}\"" do
64
+ c = Blather::Stanza::Iq::Command.new nil, nil, action
65
+ c.action.must_equal action
66
+ end
67
+ end
68
+
69
+ [:get, :set, :result, :error].each do |type|
70
+ it "can be set as \"#{type}\"" do
71
+ c = Blather::Stanza::Iq::Command.new type
72
+ c.type.must_equal type
73
+ end
74
+ end
75
+
76
+ it 'sets type to "result" on reply' do
77
+ c = Blather::Stanza::Iq::Command.new
78
+ c.type.must_equal :set
79
+ reply = c.reply.type.must_equal :result
80
+ end
81
+
82
+ it 'sets type to "result" on reply!' do
83
+ c = Blather::Stanza::Iq::Command.new
84
+ c.type.must_equal :set
85
+ c.reply!
86
+ c.type.must_equal :result
87
+ end
88
+
89
+ # TODO: Deal with #reply/#reply! better?
90
+
91
+ it 'can be registered under a namespace' do
92
+ class CommandNs < Blather::Stanza::Iq::Command; register :command_ns, nil, 'command:ns'; end
93
+ Blather::XMPPNode.class_from_registration(:command, 'command:ns').must_equal CommandNs
94
+ c_ns = CommandNs.new
95
+ c_ns.xpath('command').must_be_empty
96
+ c_ns.xpath('ns:command', :ns => 'command:ns').size.must_equal 1
97
+
98
+ c_ns.command
99
+ c_ns.command
100
+ c_ns.xpath('ns:command', :ns => 'command:ns').size.must_equal 1
101
+ end
102
+
103
+ it 'is constructed properly' do
104
+ n = Blather::Stanza::Iq::Command.new :set, "node", :execute
105
+ n.to = 'to@jid.com'
106
+ n.find("/iq[@to='to@jid.com' and @type='set' and @id='#{n.id}']/ns:command[@node='node' and @action='execute']", :ns => Blather::Stanza::Iq::Command.registered_ns).wont_be_empty
107
+ end
108
+
109
+ it 'has an action attribute' do
110
+ n = Blather::Stanza::Iq::Command.new
111
+ n.action.must_equal :execute
112
+ n.action = :cancel
113
+ n.action.must_equal :cancel
114
+ end
115
+
116
+ it 'has a status attribute' do
117
+ n = Blather::Stanza::Iq::Command.new
118
+ n.status.must_equal nil
119
+ n.status = :executing
120
+ n.status.must_equal :executing
121
+ end
122
+
123
+ it 'has a sessionid attribute' do
124
+ n = Blather::Stanza::Iq::Command.new
125
+ n.sessionid.wont_be_nil
126
+ n.sessionid = "somerandomstring"
127
+ n.sessionid.must_equal Digest::SHA1.hexdigest("somerandomstring")
128
+ end
129
+
130
+ it 'has an allowed_actions attribute' do
131
+ n = Blather::Stanza::Iq::Command.new
132
+ n.allowed_actions.must_equal [:execute]
133
+ n.add_allowed_actions [:prev, :next]
134
+ n.allowed_actions.must_equal [:execute, :prev, :next]
135
+ n.remove_allowed_actions :prev
136
+ n.allowed_actions.must_equal [:execute, :next]
137
+ end
138
+
139
+ it 'has a note_type attribute' do
140
+ n = Blather::Stanza::Iq::Command.new
141
+ n.note_type.must_equal nil
142
+ n.note_type = :info
143
+ n.note_type.must_equal :info
144
+ end
145
+
146
+ it 'has a note_text attribute' do
147
+ n = Blather::Stanza::Iq::Command.new
148
+ n.note_text.must_equal nil
149
+ n.note_text = "Some text"
150
+ n.note_text.must_equal "Some text"
151
+ end
152
+
153
+ it 'makes a form child available' do
154
+ n = Blather::XMPPNode.import(parse_stanza(command_xml).root)
155
+ n.form.fields.size.must_equal 1
156
+ n.form.fields.map { |f| f.class }.uniq.must_equal [Blather::Stanza::X::Field]
157
+ n.form.must_be_instance_of Blather::Stanza::X
158
+ end
159
+ end
@@ -69,7 +69,7 @@ describe Blather::Stanza::PubSub::Retract do
69
69
 
70
70
  it 'will iterate over each retraction' do
71
71
  Blather::XMPPNode.import(parse_stanza(retract_xml).root).each do |i|
72
- i.must_include %w[ae890ac52d0df67ed7cfdf51b644e901]
72
+ i.must_include "ae890ac52d0df67ed7cfdf51b644e901"
73
73
  end
74
74
  end
75
75
  end
@@ -0,0 +1,228 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. .. spec_helper])
2
+
3
+ def x_xml
4
+ <<-XML
5
+ <x xmlns='jabber:x:data'
6
+ type='form'>
7
+ <title/>
8
+ <instructions/>
9
+ <field var='field-name'
10
+ type='text-single'
11
+ label='description' />
12
+ <field var='field-name2'
13
+ type='text-single'
14
+ label='description' />
15
+ <field var='field-name3'
16
+ type='text-single'
17
+ label='description' />
18
+ <field var='field-name'
19
+ type='{field-type}'
20
+ label='description'>
21
+ <desc/>
22
+ <required/>
23
+ <value>field-value</value>
24
+ <option label='option-label'><value>option-value</value></option>
25
+ <option label='option-label'><value>option-value</value></option>
26
+ </field>
27
+ </x>
28
+ XML
29
+ end
30
+
31
+ describe Blather::Stanza::X do
32
+
33
+ it 'can be created from an XML string' do
34
+ x = Blather::Stanza::X.new parse_stanza(x_xml).root
35
+ x.type.must_equal :form
36
+ x.must_be_instance_of Blather::Stanza::X
37
+ end
38
+
39
+ [:cancel, :form, :result, :submit].each do |type|
40
+ it "type can be set as \"#{type}\"" do
41
+ x = Blather::Stanza::X.new type
42
+ x.type.must_equal type
43
+ end
44
+ end
45
+
46
+ it 'is constructed properly' do
47
+ n = Blather::Stanza::X.new :form
48
+ n.find("/ns:x[@type='form']", :ns => Blather::Stanza::X.registered_ns).wont_be_empty
49
+ end
50
+
51
+ it 'has an action attribute' do
52
+ n = Blather::Stanza::X.new :form
53
+ n.type.must_equal :form
54
+ n.type = :submit
55
+ n.type.must_equal :submit
56
+ end
57
+
58
+ it 'has a title attribute' do
59
+ n = Blather::Stanza::X.new :form
60
+ n.title.must_equal nil
61
+ n.title = "Hello World!"
62
+ n.title.must_equal "Hello World!"
63
+ n.title = "goodbye"
64
+ n.title.must_equal "goodbye"
65
+ end
66
+
67
+ it 'has an instructions attribute' do
68
+ n = Blather::Stanza::X.new :form
69
+ n.instructions.must_equal nil
70
+ n.instructions = "Please fill in this form"
71
+ n.instructions.must_equal "Please fill in this form"
72
+ n.instructions = "goodbye"
73
+ n.instructions.must_equal "goodbye"
74
+ end
75
+
76
+ it 'inherits a list of fields' do
77
+ n = Blather::Stanza::Iq::Command.new
78
+ n.command << parse_stanza(x_xml).root
79
+ r = Blather::Stanza::X.new.inherit n.form
80
+ r.fields.size.must_equal 4
81
+ r.fields.map { |f| f.class }.uniq.must_equal [Blather::Stanza::X::Field]
82
+ end
83
+
84
+ it 'takes a list of hashes for fields' do
85
+ fields = [
86
+ {:label => 'label', :type => 'text-single', :var => 'var'},
87
+ {:label => 'label1', :type => 'text-single', :var => 'var1'},
88
+ ]
89
+
90
+ control = [ Blather::Stanza::X::Field.new(*%w[text-single var label]),
91
+ Blather::Stanza::X::Field.new(*%w[text-single var1 label1])]
92
+
93
+ di = Blather::Stanza::X.new nil, fields
94
+ di.fields.size.must_equal 2
95
+ di.fields.each { |f| control.include?(f).must_equal true }
96
+ end
97
+
98
+ it 'takes a list of Field objects as fields' do
99
+ control = [ Blather::Stanza::X::Field.new(*%w[text-single var label1]),
100
+ Blather::Stanza::X::Field.new(*%w[text-single var1 label1])]
101
+
102
+ di = Blather::Stanza::X.new nil, control
103
+ di.fields.size.must_equal 2
104
+ di.fields.each { |f| control.include?(f).must_equal true }
105
+ end
106
+
107
+ it 'takes a mix of hashes and field objects as fields' do
108
+ fields = [
109
+ {:label => 'label', :type => 'text-single', :var => 'var'},
110
+ Blather::Stanza::X::Field.new(*%w[text-single var1 label1]),
111
+ ]
112
+
113
+ control = [ Blather::Stanza::X::Field.new(*%w[text-single var label]),
114
+ Blather::Stanza::X::Field.new(*%w[text-single var1 label1])]
115
+
116
+ di = Blather::Stanza::X.new nil, fields
117
+ di.fields.size.must_equal 2
118
+ di.fields.each { |f| control.include?(f).must_equal true }
119
+ end
120
+
121
+ it 'allows adding of fields' do
122
+ di = Blather::Stanza::X.new nil
123
+ di.fields.size.must_equal 0
124
+ di.add_fields [{:label => 'label', :type => 'text-single', :var => 'var'}]
125
+ di.fields.size.must_equal 1
126
+ di.add_fields [Blather::Stanza::X::Field.new(*%w[text-single var1 label1])]
127
+ di.fields.size.must_equal 2
128
+ end
129
+
130
+ end
131
+
132
+ describe Blather::Stanza::X::Field do
133
+ it 'will auto-inherit nodes' do
134
+ n = parse_stanza "<field type='text-single' var='music' label='Music from the time of Shakespeare' />"
135
+ i = Blather::Stanza::X::Field.new n.root
136
+ i.type.must_equal 'text-single'
137
+ i.var.must_equal 'music'
138
+ i.label.must_equal 'Music from the time of Shakespeare'
139
+ end
140
+
141
+ it 'has a type attribute' do
142
+ n = Blather::Stanza::X::Field.new 'text-single'
143
+ n.type.must_equal 'text-single'
144
+ n.type = 'hidden'
145
+ n.type.must_equal 'hidden'
146
+ end
147
+
148
+ it 'has a var attribute' do
149
+ n = Blather::Stanza::X::Field.new 'text-single', 'name'
150
+ n.var.must_equal 'name'
151
+ n.var = 'email'
152
+ n.var.must_equal 'email'
153
+ end
154
+
155
+ it 'has a label attribute' do
156
+ n = Blather::Stanza::X::Field.new 'text-single', 'subject', 'Music from the time of Shakespeare'
157
+ n.label.must_equal 'Music from the time of Shakespeare'
158
+ n.label = 'Books by and about Shakespeare'
159
+ n.label.must_equal 'Books by and about Shakespeare'
160
+ end
161
+
162
+ it 'has a desc attribute' do
163
+ n = Blather::Stanza::X::Field.new 'text-single', 'subject', 'Music from the time of Shakespeare'
164
+ n.desc.must_equal nil
165
+ n.desc = 'Books by and about Shakespeare'
166
+ n.desc.must_equal 'Books by and about Shakespeare'
167
+ n.desc = 'goodbye'
168
+ n.desc.must_equal 'goodbye'
169
+ end
170
+
171
+ it 'has a required? attribute' do
172
+ n = Blather::Stanza::X::Field.new 'text-single', 'subject', 'Music from the time of Shakespeare'
173
+ n.required?.must_equal false
174
+ n.required!
175
+ n.required?.must_equal true
176
+ n.required! false
177
+ n.required?.must_equal false
178
+ end
179
+
180
+ it 'has a value attribute' do
181
+ n = Blather::Stanza::X::Field.new 'text-single', 'subject', 'Music from the time of Shakespeare'
182
+ n.value.must_equal nil
183
+ n.value = 'book1'
184
+ n.value.must_equal 'book1'
185
+ n.value = 'book2'
186
+ n.value.must_equal 'book2'
187
+ end
188
+
189
+ # Option child elements
190
+ it 'allows adding of options' do
191
+ di = Blather::Stanza::X::Field.new nil
192
+ di.options.size.must_equal 0
193
+ di.add_options [{:label => 'Person', :value => 'person'}]
194
+ di.options.size.must_equal 1
195
+ di.add_options [Blather::Stanza::X::Field::Option.new(*%w[person1 Person1])]
196
+ di.options.size.must_equal 2
197
+ end
198
+
199
+ it 'raises an error when compared against a non X::Field' do
200
+ a = Blather::Stanza::X::Field.new('hidden', 'secret_message')
201
+ lambda { a == 'test' }.must_raise RuntimeError
202
+ end
203
+
204
+ it 'can determine equality' do
205
+ a = Blather::Stanza::X::Field.new('text-single', 'subject')
206
+ a.must_equal Blather::Stanza::X::Field.new('text-single', 'subject')
207
+ a.wont_equal Blather::Stanza::X::Field.new('text-single', 'subject1')
208
+ end
209
+ end
210
+
211
+ describe Blather::Stanza::X::Field::Option do
212
+
213
+ it 'has a value attribute' do
214
+ n = Blather::Stanza::X::Field::Option.new 'person1', 'Person 1'
215
+ n.value.must_equal 'person1'
216
+ n.value = 'book1'
217
+ n.value.must_equal 'book1'
218
+ end
219
+
220
+ it 'has a label attribute' do
221
+ n = Blather::Stanza::X::Field::Option.new 'person1', 'Person 1'
222
+ n.label.must_equal 'Person 1'
223
+ n.label = 'Book 1'
224
+ n.label.must_equal 'Book 1'
225
+ n.label = 'Book 2'
226
+ n.label.must_equal 'Book 2'
227
+ end
228
+ end
@@ -45,7 +45,7 @@ describe Blather::Stream::Client do
45
45
 
46
46
  it 'attempts to find the SRV record if a host is not provided' do
47
47
  dns = mock(:sort! => nil, :empty? => false)
48
- dns.expects(:each).yields(mock({
48
+ dns.expects(:detect).yields(mock({
49
49
  :target => 'd',
50
50
  :port => 5222
51
51
  }))
@@ -135,10 +135,11 @@ describe Blather::XMPPNode do
135
135
  c.namespace = 'foo:bar'
136
136
  n << c
137
137
 
138
- n.find(:bar).size.must_equal 2
139
- n.remove_child 'bar', 'foo:bar'
140
138
  n.find(:bar).size.must_equal 1
141
- n.find(:bar).first.namespace.must_be_nil
139
+ n.find('//xmlns:bar', :xmlns => 'foo:bar').size.must_equal 1
140
+ n.remove_child '//xmlns:bar', :xmlns => 'foo:bar'
141
+ n.find(:bar).size.must_equal 1
142
+ n.find('//xmlns:bar', :xmlns => 'foo:bar').size.must_equal 0
142
143
  end
143
144
 
144
145
  it 'will remove all child elements' do
metadata CHANGED
@@ -1,7 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blather
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.8
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 4
9
+ - 10
10
+ version: 0.4.10
5
11
  platform: ruby
6
12
  authors:
7
13
  - Jeff Smick
@@ -9,29 +15,41 @@ autorequire:
9
15
  bindir: bin
10
16
  cert_chain: []
11
17
 
12
- date: 2010-01-17 00:00:00 -08:00
18
+ date: 2010-07-19 00:00:00 -07:00
13
19
  default_executable:
14
20
  dependencies:
15
21
  - !ruby/object:Gem::Dependency
16
22
  name: eventmachine
17
- type: :runtime
18
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
20
26
  requirements:
21
27
  - - ">="
22
28
  - !ruby/object:Gem::Version
29
+ hash: 35
30
+ segments:
31
+ - 0
32
+ - 12
33
+ - 6
23
34
  version: 0.12.6
24
- version:
35
+ type: :runtime
36
+ version_requirements: *id001
25
37
  - !ruby/object:Gem::Dependency
26
38
  name: nokogiri
27
- type: :runtime
28
- version_requirement:
29
- version_requirements: !ruby/object:Gem::Requirement
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
30
42
  requirements:
31
43
  - - ">="
32
44
  - !ruby/object:Gem::Version
45
+ hash: 7
46
+ segments:
47
+ - 1
48
+ - 4
49
+ - 0
33
50
  version: 1.4.0
34
- version:
51
+ type: :runtime
52
+ version_requirements: *id002
35
53
  description: An XMPP DSL for Ruby written on top of EventMachine and Nokogiri
36
54
  email: sprsquish@gmail.com
37
55
  executables: []
@@ -69,6 +87,7 @@ files:
69
87
  - lib/blather/stanza/disco/disco_info.rb
70
88
  - lib/blather/stanza/disco/disco_items.rb
71
89
  - lib/blather/stanza/iq.rb
90
+ - lib/blather/stanza/iq/command.rb
72
91
  - lib/blather/stanza/iq/query.rb
73
92
  - lib/blather/stanza/iq/roster.rb
74
93
  - lib/blather/stanza/message.rb
@@ -90,6 +109,7 @@ files:
90
109
  - lib/blather/stanza/pubsub_owner.rb
91
110
  - lib/blather/stanza/pubsub_owner/delete.rb
92
111
  - lib/blather/stanza/pubsub_owner/purge.rb
112
+ - lib/blather/stanza/x.rb
93
113
  - lib/blather/stream.rb
94
114
  - lib/blather/stream/client.rb
95
115
  - lib/blather/stream/component.rb
@@ -102,6 +122,49 @@ files:
102
122
  - lib/blather/xmpp_node.rb
103
123
  - LICENSE
104
124
  - README.md
125
+ - spec/blather/client/client_spec.rb
126
+ - spec/blather/client/dsl/pubsub_spec.rb
127
+ - spec/blather/client/dsl_spec.rb
128
+ - spec/blather/core_ext/nokogiri_spec.rb
129
+ - spec/blather/errors/sasl_error_spec.rb
130
+ - spec/blather/errors/stanza_error_spec.rb
131
+ - spec/blather/errors/stream_error_spec.rb
132
+ - spec/blather/errors_spec.rb
133
+ - spec/blather/jid_spec.rb
134
+ - spec/blather/roster_item_spec.rb
135
+ - spec/blather/roster_spec.rb
136
+ - spec/blather/stanza/discos/disco_info_spec.rb
137
+ - spec/blather/stanza/discos/disco_items_spec.rb
138
+ - spec/blather/stanza/iq/command_spec.rb
139
+ - spec/blather/stanza/iq/query_spec.rb
140
+ - spec/blather/stanza/iq/roster_spec.rb
141
+ - spec/blather/stanza/iq_spec.rb
142
+ - spec/blather/stanza/message_spec.rb
143
+ - spec/blather/stanza/presence/status_spec.rb
144
+ - spec/blather/stanza/presence/subscription_spec.rb
145
+ - spec/blather/stanza/presence_spec.rb
146
+ - spec/blather/stanza/pubsub/affiliations_spec.rb
147
+ - spec/blather/stanza/pubsub/create_spec.rb
148
+ - spec/blather/stanza/pubsub/event_spec.rb
149
+ - spec/blather/stanza/pubsub/items_spec.rb
150
+ - spec/blather/stanza/pubsub/publish_spec.rb
151
+ - spec/blather/stanza/pubsub/retract_spec.rb
152
+ - spec/blather/stanza/pubsub/subscribe_spec.rb
153
+ - spec/blather/stanza/pubsub/subscription_spec.rb
154
+ - spec/blather/stanza/pubsub/subscriptions_spec.rb
155
+ - spec/blather/stanza/pubsub/unsubscribe_spec.rb
156
+ - spec/blather/stanza/pubsub_owner/delete_spec.rb
157
+ - spec/blather/stanza/pubsub_owner/purge_spec.rb
158
+ - spec/blather/stanza/pubsub_owner_spec.rb
159
+ - spec/blather/stanza/pubsub_spec.rb
160
+ - spec/blather/stanza/x_spec.rb
161
+ - spec/blather/stanza_spec.rb
162
+ - spec/blather/stream/client_spec.rb
163
+ - spec/blather/stream/component_spec.rb
164
+ - spec/blather/stream/parser_spec.rb
165
+ - spec/blather/xmpp_node_spec.rb
166
+ - spec/fixtures/pubsub.rb
167
+ - spec/spec_helper.rb
105
168
  has_rdoc: true
106
169
  homepage: http://github.com/sprsquish/blather
107
170
  licenses: []
@@ -112,21 +175,27 @@ rdoc_options:
112
175
  require_paths:
113
176
  - lib
114
177
  required_ruby_version: !ruby/object:Gem::Requirement
178
+ none: false
115
179
  requirements:
116
180
  - - ">="
117
181
  - !ruby/object:Gem::Version
182
+ hash: 3
183
+ segments:
184
+ - 0
118
185
  version: "0"
119
- version:
120
186
  required_rubygems_version: !ruby/object:Gem::Requirement
187
+ none: false
121
188
  requirements:
122
189
  - - ">="
123
190
  - !ruby/object:Gem::Version
191
+ hash: 3
192
+ segments:
193
+ - 0
124
194
  version: "0"
125
- version:
126
195
  requirements: []
127
196
 
128
197
  rubyforge_project: squishtech
129
- rubygems_version: 1.3.5
198
+ rubygems_version: 1.3.7
130
199
  signing_key:
131
200
  specification_version: 3
132
201
  summary: Simpler XMPP built for speed
@@ -144,6 +213,7 @@ test_files:
144
213
  - spec/blather/roster_spec.rb
145
214
  - spec/blather/stanza/discos/disco_info_spec.rb
146
215
  - spec/blather/stanza/discos/disco_items_spec.rb
216
+ - spec/blather/stanza/iq/command_spec.rb
147
217
  - spec/blather/stanza/iq/query_spec.rb
148
218
  - spec/blather/stanza/iq/roster_spec.rb
149
219
  - spec/blather/stanza/iq_spec.rb
@@ -165,6 +235,7 @@ test_files:
165
235
  - spec/blather/stanza/pubsub_owner/purge_spec.rb
166
236
  - spec/blather/stanza/pubsub_owner_spec.rb
167
237
  - spec/blather/stanza/pubsub_spec.rb
238
+ - spec/blather/stanza/x_spec.rb
168
239
  - spec/blather/stanza_spec.rb
169
240
  - spec/blather/stream/client_spec.rb
170
241
  - spec/blather/stream/component_spec.rb