blather 0.4.8 → 0.4.10

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