ruby-dbus 0.16.0 → 0.18.1
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.
- checksums.yaml +4 -4
- data/NEWS.md +160 -0
- data/README.md +3 -5
- data/Rakefile +18 -8
- data/VERSION +1 -1
- data/doc/Reference.md +106 -7
- data/examples/doc/_extract_examples +7 -0
- data/examples/gdbus/gdbus +31 -24
- data/examples/no-introspect/nm-test.rb +2 -0
- data/examples/no-introspect/tracker-test.rb +3 -1
- data/examples/rhythmbox/playpause.rb +2 -1
- data/examples/service/call_service.rb +2 -1
- data/examples/service/complex-property.rb +21 -0
- data/examples/service/service_newapi.rb +1 -1
- data/examples/simple/call_introspect.rb +1 -0
- data/examples/simple/get_id.rb +2 -1
- data/examples/simple/properties.rb +2 -0
- data/examples/utils/listnames.rb +1 -0
- data/examples/utils/notify.rb +1 -0
- data/lib/dbus/api_options.rb +9 -0
- data/lib/dbus/auth.rb +20 -15
- data/lib/dbus/bus.rb +123 -75
- data/lib/dbus/bus_name.rb +12 -8
- data/lib/dbus/core_ext/class/attribute.rb +1 -1
- data/lib/dbus/data.rb +821 -0
- data/lib/dbus/emits_changed_signal.rb +83 -0
- data/lib/dbus/error.rb +4 -2
- data/lib/dbus/introspect.rb +132 -31
- data/lib/dbus/logger.rb +3 -1
- data/lib/dbus/marshall.rb +247 -296
- data/lib/dbus/matchrule.rb +16 -16
- data/lib/dbus/message.rb +44 -37
- data/lib/dbus/message_queue.rb +16 -10
- data/lib/dbus/object.rb +358 -24
- data/lib/dbus/object_path.rb +11 -6
- data/lib/dbus/proxy_object.rb +22 -1
- data/lib/dbus/proxy_object_factory.rb +13 -7
- data/lib/dbus/proxy_object_interface.rb +63 -30
- data/lib/dbus/raw_message.rb +91 -0
- data/lib/dbus/type.rb +318 -86
- data/lib/dbus/xml.rb +32 -17
- data/lib/dbus.rb +14 -7
- data/ruby-dbus.gemspec +7 -3
- data/spec/async_spec.rb +2 -0
- data/spec/binding_spec.rb +2 -0
- data/spec/bus_and_xml_backend_spec.rb +2 -0
- data/spec/bus_driver_spec.rb +2 -0
- data/spec/bus_name_spec.rb +3 -1
- data/spec/bus_spec.rb +2 -0
- data/spec/byte_array_spec.rb +2 -0
- data/spec/client_robustness_spec.rb +4 -2
- data/spec/data/marshall.yaml +1667 -0
- data/spec/data_spec.rb +673 -0
- data/spec/emits_changed_signal_spec.rb +58 -0
- data/spec/err_msg_spec.rb +2 -0
- data/spec/introspect_xml_parser_spec.rb +2 -0
- data/spec/introspection_spec.rb +2 -0
- data/spec/main_loop_spec.rb +3 -1
- data/spec/node_spec.rb +23 -0
- data/spec/object_path_spec.rb +3 -0
- data/spec/object_spec.rb +138 -0
- data/spec/packet_marshaller_spec.rb +41 -0
- data/spec/packet_unmarshaller_spec.rb +248 -0
- data/spec/property_spec.rb +192 -5
- data/spec/proxy_object_spec.rb +2 -0
- data/spec/server_robustness_spec.rb +2 -0
- data/spec/server_spec.rb +2 -0
- data/spec/service_newapi.rb +70 -70
- data/spec/session_bus_spec.rb +3 -1
- data/spec/session_bus_spec_manual.rb +2 -0
- data/spec/signal_spec.rb +5 -3
- data/spec/spec_helper.rb +37 -9
- data/spec/thread_safety_spec.rb +2 -0
- data/spec/tools/dbus-limited-session.conf +4 -0
- data/spec/type_spec.rb +214 -6
- data/spec/value_spec.rb +16 -1
- data/spec/variant_spec.rb +4 -2
- data/spec/zzz_quit_spec.rb +16 -0
- metadata +34 -8
data/lib/dbus/data.rb
ADDED
@@ -0,0 +1,821 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file is part of the ruby-dbus project
|
4
|
+
# Copyright (C) 2022 Martin Vidner
|
5
|
+
#
|
6
|
+
# This library is free software; you can redistribute it and/or
|
7
|
+
# modify it under the terms of the GNU Lesser General Public
|
8
|
+
# License, version 2.1 as published by the Free Software Foundation.
|
9
|
+
# See the file "COPYING" for the exact licensing terms.
|
10
|
+
|
11
|
+
module DBus
|
12
|
+
# FIXME: in general, when an API gives me, a user, a choice,
|
13
|
+
# remember to make it easy for the case of:
|
14
|
+
# "I don't CARE, I don't WANT to care, WHY should I care?"
|
15
|
+
|
16
|
+
# Exact/explicit representation of D-Bus data types:
|
17
|
+
#
|
18
|
+
# - {Boolean}
|
19
|
+
# - {Byte}, {Int16}, {Int32}, {Int64}, {UInt16}, {UInt32}, {UInt64}
|
20
|
+
# - {Double}
|
21
|
+
# - {String}, {ObjectPath}, {Signature}
|
22
|
+
# - {Array}, {DictEntry}, {Struct}
|
23
|
+
# - {UnixFD}
|
24
|
+
# - {Variant}
|
25
|
+
#
|
26
|
+
# The common base type is {Base}.
|
27
|
+
#
|
28
|
+
# There are other intermediate classes in the inheritance hierarchy, using
|
29
|
+
# the names the specification uses, but they are an implementation detail:
|
30
|
+
#
|
31
|
+
# - A value is either {Basic} or a {Container}.
|
32
|
+
# - Basic values are either {Fixed}-size or {StringLike}.
|
33
|
+
module Data
|
34
|
+
# Given a plain Ruby *value* and wanting a D-Bus *type*,
|
35
|
+
# construct an appropriate {Data::Base} instance.
|
36
|
+
#
|
37
|
+
# @param type [SingleCompleteType,Type]
|
38
|
+
# @param value [::Object,Data::Base] a plain value; exact values also allowed
|
39
|
+
# @return [Data::Base]
|
40
|
+
# @raise TypeError
|
41
|
+
def make_typed(type, value)
|
42
|
+
type = DBus.type(type) unless type.is_a?(Type)
|
43
|
+
data_class = Data::BY_TYPE_CODE[type.sigtype]
|
44
|
+
# not nil because DBus.type validates
|
45
|
+
|
46
|
+
data_class.from_typed(value, type: type)
|
47
|
+
end
|
48
|
+
module_function :make_typed
|
49
|
+
|
50
|
+
# The base class for explicitly typed values.
|
51
|
+
#
|
52
|
+
# A value is either {Basic} or a {Container}.
|
53
|
+
# {Basic} values are either {Fixed}-size or {StringLike}.
|
54
|
+
class Base
|
55
|
+
# @!method self.basic?
|
56
|
+
# @return [Boolean]
|
57
|
+
|
58
|
+
# @!method self.fixed?
|
59
|
+
# @return [Boolean]
|
60
|
+
|
61
|
+
# @return [::Object] a valid value, plain-Ruby typed.
|
62
|
+
# @see Data::Container#exact_value
|
63
|
+
attr_reader :value
|
64
|
+
|
65
|
+
# @!method self.type_code
|
66
|
+
# @return [String] a single-character string, for example "a" for arrays
|
67
|
+
|
68
|
+
# @!method type
|
69
|
+
# @abstract
|
70
|
+
# Note that for Variants type=="v",
|
71
|
+
# for the specific see {Variant#member_type}
|
72
|
+
# @return [Type] the exact type of this value
|
73
|
+
|
74
|
+
# @!method self.from_typed(value, type:)
|
75
|
+
# @param value [::Object]
|
76
|
+
# @param type [Type]
|
77
|
+
# @return [Base]
|
78
|
+
# @api private
|
79
|
+
# Use {Data.make_typed} instead.
|
80
|
+
# Construct an instance of the specific subclass, with a type further
|
81
|
+
# specified in the *type* argument.
|
82
|
+
|
83
|
+
# Child classes must validate *value*.
|
84
|
+
def initialize(value)
|
85
|
+
@value = value
|
86
|
+
end
|
87
|
+
|
88
|
+
def ==(other)
|
89
|
+
@value == if other.is_a?(Base)
|
90
|
+
other.value
|
91
|
+
else
|
92
|
+
other
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Hash key equality
|
97
|
+
# See https://ruby-doc.org/core-3.0.0/Object.html#method-i-eql-3F
|
98
|
+
# Stricter than #== (RSpec: eq), 1==1.0 but 1.eql(1.0)->false
|
99
|
+
def eql?(other)
|
100
|
+
return false unless other.class == self.class
|
101
|
+
|
102
|
+
other.value.eql?(@value)
|
103
|
+
# TODO: this should work, now check derived classes, exact_value
|
104
|
+
end
|
105
|
+
|
106
|
+
# @param type [Type]
|
107
|
+
def self.assert_type_matches_class(type)
|
108
|
+
raise ArgumentError, "Expecting #{type_code.inspect} for class #{self}, got #{type.sigtype.inspect}" \
|
109
|
+
unless type.sigtype == type_code
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# A value that is not a {Container}.
|
114
|
+
class Basic < Base
|
115
|
+
def self.basic?
|
116
|
+
true
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [Type]
|
120
|
+
def self.type
|
121
|
+
# memoize
|
122
|
+
@type ||= Type.new(type_code).freeze
|
123
|
+
end
|
124
|
+
|
125
|
+
def type
|
126
|
+
# The basic types can do this, unlike the containers
|
127
|
+
self.class.type
|
128
|
+
end
|
129
|
+
|
130
|
+
# @param value [::Object]
|
131
|
+
# @param type [Type]
|
132
|
+
# @return [Basic]
|
133
|
+
def self.from_typed(value, type:)
|
134
|
+
assert_type_matches_class(type)
|
135
|
+
new(value)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# A value that has a fixed size (unlike {StringLike}).
|
140
|
+
class Fixed < Basic
|
141
|
+
def self.fixed?
|
142
|
+
true
|
143
|
+
end
|
144
|
+
|
145
|
+
# most Fixed types are valid
|
146
|
+
# whatever bits from the wire are used to initialize them
|
147
|
+
# @param mode [:plain,:exact]
|
148
|
+
def self.from_raw(value, mode:)
|
149
|
+
return value if mode == :plain
|
150
|
+
|
151
|
+
new(value)
|
152
|
+
end
|
153
|
+
|
154
|
+
# @param endianness [:little,:big]
|
155
|
+
def marshall(endianness)
|
156
|
+
[value].pack(self.class.format[endianness])
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Format strings for String#unpack, both little- and big-endian.
|
161
|
+
Format = ::Struct.new(:little, :big)
|
162
|
+
|
163
|
+
# Represents integers
|
164
|
+
class Int < Fixed
|
165
|
+
# @!method self.range
|
166
|
+
# @return [Range] the full range of allowed values
|
167
|
+
|
168
|
+
# @param value [::Integer,DBus::Data::Int]
|
169
|
+
# @raise RangeError
|
170
|
+
def initialize(value)
|
171
|
+
value = value.value if value.is_a?(self.class)
|
172
|
+
r = self.class.range
|
173
|
+
raise RangeError, "#{value.inspect} is not a member of #{r}" unless r.member?(value)
|
174
|
+
|
175
|
+
super(value)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Byte.
|
180
|
+
#
|
181
|
+
# TODO: a specialized ByteArray for `ay` may be useful,
|
182
|
+
# to save memory and for natural handling
|
183
|
+
class Byte < Int
|
184
|
+
def self.type_code
|
185
|
+
"y"
|
186
|
+
end
|
187
|
+
|
188
|
+
def self.alignment
|
189
|
+
1
|
190
|
+
end
|
191
|
+
FORMAT = Format.new("C", "C")
|
192
|
+
def self.format
|
193
|
+
FORMAT
|
194
|
+
end
|
195
|
+
|
196
|
+
def self.range
|
197
|
+
(0..255)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Boolean: encoded as a {UInt32} but only 0 and 1 are valid.
|
202
|
+
class Boolean < Fixed
|
203
|
+
def self.type_code
|
204
|
+
"b"
|
205
|
+
end
|
206
|
+
|
207
|
+
def self.alignment
|
208
|
+
4
|
209
|
+
end
|
210
|
+
FORMAT = Format.new("L<", "L>")
|
211
|
+
def self.format
|
212
|
+
FORMAT
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.validate_raw!(value)
|
216
|
+
return if [0, 1].member?(value)
|
217
|
+
|
218
|
+
raise InvalidPacketException, "BOOLEAN must be 0 or 1, found #{value}"
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.from_raw(value, mode:)
|
222
|
+
validate_raw!(value)
|
223
|
+
|
224
|
+
value = value == 1
|
225
|
+
return value if mode == :plain
|
226
|
+
|
227
|
+
new(value)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Accept any *value*, store its Ruby truth value
|
231
|
+
# (excepting another instance of this class, where use its {#value}).
|
232
|
+
#
|
233
|
+
# So new(0).value is true.
|
234
|
+
# @param value [::Object,DBus::Data::Boolean]
|
235
|
+
def initialize(value)
|
236
|
+
value = value.value if value.is_a?(self.class)
|
237
|
+
super(value ? true : false)
|
238
|
+
end
|
239
|
+
|
240
|
+
# @param endianness [:little,:big]
|
241
|
+
def marshall(endianness)
|
242
|
+
int = value ? 1 : 0
|
243
|
+
[int].pack(UInt32.format[endianness])
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Signed 16 bit integer.
|
248
|
+
class Int16 < Int
|
249
|
+
def self.type_code
|
250
|
+
"n"
|
251
|
+
end
|
252
|
+
|
253
|
+
def self.alignment
|
254
|
+
2
|
255
|
+
end
|
256
|
+
|
257
|
+
FORMAT = Format.new("s<", "s>")
|
258
|
+
def self.format
|
259
|
+
FORMAT
|
260
|
+
end
|
261
|
+
|
262
|
+
def self.range
|
263
|
+
(-32_768..32_767)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# Unsigned 16 bit integer.
|
268
|
+
class UInt16 < Int
|
269
|
+
def self.type_code
|
270
|
+
"q"
|
271
|
+
end
|
272
|
+
|
273
|
+
def self.alignment
|
274
|
+
2
|
275
|
+
end
|
276
|
+
|
277
|
+
FORMAT = Format.new("S<", "S>")
|
278
|
+
def self.format
|
279
|
+
FORMAT
|
280
|
+
end
|
281
|
+
|
282
|
+
def self.range
|
283
|
+
(0..65_535)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# Signed 32 bit integer.
|
288
|
+
class Int32 < Int
|
289
|
+
def self.type_code
|
290
|
+
"i"
|
291
|
+
end
|
292
|
+
|
293
|
+
def self.alignment
|
294
|
+
4
|
295
|
+
end
|
296
|
+
|
297
|
+
FORMAT = Format.new("l<", "l>")
|
298
|
+
def self.format
|
299
|
+
FORMAT
|
300
|
+
end
|
301
|
+
|
302
|
+
def self.range
|
303
|
+
(-2_147_483_648..2_147_483_647)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Unsigned 32 bit integer.
|
308
|
+
class UInt32 < Int
|
309
|
+
def self.type_code
|
310
|
+
"u"
|
311
|
+
end
|
312
|
+
|
313
|
+
def self.alignment
|
314
|
+
4
|
315
|
+
end
|
316
|
+
|
317
|
+
FORMAT = Format.new("L<", "L>")
|
318
|
+
def self.format
|
319
|
+
FORMAT
|
320
|
+
end
|
321
|
+
|
322
|
+
def self.range
|
323
|
+
(0..4_294_967_295)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
# Unix file descriptor, not implemented yet.
|
328
|
+
class UnixFD < UInt32
|
329
|
+
def self.type_code
|
330
|
+
"h"
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
# Signed 64 bit integer.
|
335
|
+
class Int64 < Int
|
336
|
+
def self.type_code
|
337
|
+
"x"
|
338
|
+
end
|
339
|
+
|
340
|
+
def self.alignment
|
341
|
+
8
|
342
|
+
end
|
343
|
+
|
344
|
+
FORMAT = Format.new("q<", "q>")
|
345
|
+
def self.format
|
346
|
+
FORMAT
|
347
|
+
end
|
348
|
+
|
349
|
+
def self.range
|
350
|
+
(-9_223_372_036_854_775_808..9_223_372_036_854_775_807)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
# Unsigned 64 bit integer.
|
355
|
+
class UInt64 < Int
|
356
|
+
def self.type_code
|
357
|
+
"t"
|
358
|
+
end
|
359
|
+
|
360
|
+
def self.alignment
|
361
|
+
8
|
362
|
+
end
|
363
|
+
|
364
|
+
FORMAT = Format.new("Q<", "Q>")
|
365
|
+
def self.format
|
366
|
+
FORMAT
|
367
|
+
end
|
368
|
+
|
369
|
+
def self.range
|
370
|
+
(0..18_446_744_073_709_551_615)
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# Double-precision floating point number.
|
375
|
+
class Double < Fixed
|
376
|
+
def self.type_code
|
377
|
+
"d"
|
378
|
+
end
|
379
|
+
|
380
|
+
def self.alignment
|
381
|
+
8
|
382
|
+
end
|
383
|
+
|
384
|
+
FORMAT = Format.new("E", "G")
|
385
|
+
def self.format
|
386
|
+
FORMAT
|
387
|
+
end
|
388
|
+
|
389
|
+
# @param value [#to_f,DBus::Data::Double]
|
390
|
+
# @raise TypeError,ArgumentError
|
391
|
+
def initialize(value)
|
392
|
+
value = value.value if value.is_a?(self.class)
|
393
|
+
value = Kernel.Float(value)
|
394
|
+
super(value)
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# {DBus::Data::String}, {DBus::Data::ObjectPath}, or {DBus::Data::Signature}.
|
399
|
+
class StringLike < Basic
|
400
|
+
def self.fixed?
|
401
|
+
false
|
402
|
+
end
|
403
|
+
|
404
|
+
def initialize(value)
|
405
|
+
if value.is_a?(self.class)
|
406
|
+
value = value.value
|
407
|
+
else
|
408
|
+
self.class.validate_raw!(value)
|
409
|
+
end
|
410
|
+
|
411
|
+
super(value)
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
# UTF-8 encoded string.
|
416
|
+
class String < StringLike
|
417
|
+
def self.type_code
|
418
|
+
"s"
|
419
|
+
end
|
420
|
+
|
421
|
+
def self.alignment
|
422
|
+
4
|
423
|
+
end
|
424
|
+
|
425
|
+
def self.size_class
|
426
|
+
UInt32
|
427
|
+
end
|
428
|
+
|
429
|
+
def self.validate_raw!(value)
|
430
|
+
value.each_codepoint do |cp|
|
431
|
+
raise InvalidPacketException, "Invalid string, contains NUL" if cp.zero?
|
432
|
+
end
|
433
|
+
rescue ArgumentError
|
434
|
+
raise InvalidPacketException, "Invalid string, not in UTF-8"
|
435
|
+
end
|
436
|
+
|
437
|
+
def self.from_raw(value, mode:)
|
438
|
+
value.force_encoding(Encoding::UTF_8)
|
439
|
+
if mode == :plain
|
440
|
+
validate_raw!(value)
|
441
|
+
return value
|
442
|
+
end
|
443
|
+
|
444
|
+
new(value)
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
# See also {DBus::ObjectPath}
|
449
|
+
class ObjectPath < StringLike
|
450
|
+
def self.type_code
|
451
|
+
"o"
|
452
|
+
end
|
453
|
+
|
454
|
+
def self.alignment
|
455
|
+
4
|
456
|
+
end
|
457
|
+
|
458
|
+
def self.size_class
|
459
|
+
UInt32
|
460
|
+
end
|
461
|
+
|
462
|
+
# @raise InvalidPacketException
|
463
|
+
def self.validate_raw!(value)
|
464
|
+
DBus::ObjectPath.new(value)
|
465
|
+
rescue DBus::Error => e
|
466
|
+
raise InvalidPacketException, e.message
|
467
|
+
end
|
468
|
+
|
469
|
+
def self.from_raw(value, mode:)
|
470
|
+
if mode == :plain
|
471
|
+
validate_raw!(value)
|
472
|
+
return value
|
473
|
+
end
|
474
|
+
|
475
|
+
new(value)
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
# Signature string, zero or more single complete types.
|
480
|
+
# See also {DBus::Type}
|
481
|
+
class Signature < StringLike
|
482
|
+
def self.type_code
|
483
|
+
"g"
|
484
|
+
end
|
485
|
+
|
486
|
+
def self.alignment
|
487
|
+
1
|
488
|
+
end
|
489
|
+
|
490
|
+
def self.size_class
|
491
|
+
Byte
|
492
|
+
end
|
493
|
+
|
494
|
+
# @return [::Array<Type>]
|
495
|
+
def self.validate_raw!(value)
|
496
|
+
DBus.types(value)
|
497
|
+
rescue Type::SignatureException => e
|
498
|
+
raise InvalidPacketException, "Invalid signature: #{e.message}"
|
499
|
+
end
|
500
|
+
|
501
|
+
def self.from_raw(value, mode:)
|
502
|
+
if mode == :plain
|
503
|
+
_types = validate_raw!(value)
|
504
|
+
return value
|
505
|
+
end
|
506
|
+
|
507
|
+
new(value)
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
# Contains one or more other values.
|
512
|
+
class Container < Base
|
513
|
+
def self.basic?
|
514
|
+
false
|
515
|
+
end
|
516
|
+
|
517
|
+
def self.fixed?
|
518
|
+
false
|
519
|
+
end
|
520
|
+
|
521
|
+
# For containers, the type varies among instances
|
522
|
+
# @see Base#type
|
523
|
+
attr_reader :type
|
524
|
+
|
525
|
+
# @return something that is, or contains, {Data::Base}.
|
526
|
+
# Er, this docs kinda sucks.
|
527
|
+
def exact_value
|
528
|
+
@value
|
529
|
+
end
|
530
|
+
|
531
|
+
def value
|
532
|
+
@value.map(&:value)
|
533
|
+
end
|
534
|
+
|
535
|
+
# Hash key equality
|
536
|
+
# See https://ruby-doc.org/core-3.0.0/Object.html#method-i-eql-3F
|
537
|
+
# Stricter than #== (RSpec: eq), 1==1.0 but 1.eql(1.0)->false
|
538
|
+
def eql?(other)
|
539
|
+
return false unless other.class == self.class
|
540
|
+
|
541
|
+
other.exact_value.eql?(exact_value)
|
542
|
+
end
|
543
|
+
|
544
|
+
# def ==(other)
|
545
|
+
# eql?(other) || super
|
546
|
+
# end
|
547
|
+
end
|
548
|
+
|
549
|
+
# An Array, or a Dictionary (Hash).
|
550
|
+
class Array < Container
|
551
|
+
def self.type_code
|
552
|
+
"a"
|
553
|
+
end
|
554
|
+
|
555
|
+
def self.alignment
|
556
|
+
4
|
557
|
+
end
|
558
|
+
|
559
|
+
# TODO: check that Hash keys are basic types
|
560
|
+
# @param mode [:plain,:exact]
|
561
|
+
# @param type [Type]
|
562
|
+
# @param hash [Boolean] are we unmarshalling an ARRAY of DICT_ENTRY
|
563
|
+
# @return [Data::Array]
|
564
|
+
def self.from_items(value, mode:, type:, hash: false)
|
565
|
+
value = Hash[value] if hash
|
566
|
+
return value if mode == :plain
|
567
|
+
|
568
|
+
new(value, type: type)
|
569
|
+
end
|
570
|
+
|
571
|
+
# @param value [::Object]
|
572
|
+
# @param type [Type]
|
573
|
+
# @return [Data::Array]
|
574
|
+
def self.from_typed(value, type:)
|
575
|
+
new(value, type: type) # initialize(::Array<Data::Base>)
|
576
|
+
end
|
577
|
+
|
578
|
+
def value
|
579
|
+
v = super
|
580
|
+
if type.child.sigtype == Type::DICT_ENTRY
|
581
|
+
# BTW this makes a copy so mutating it is pointless
|
582
|
+
v.to_h
|
583
|
+
else
|
584
|
+
v
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
# FIXME: should Data::Array be mutable?
|
589
|
+
# if it is, is its type mutable too?
|
590
|
+
|
591
|
+
# TODO: specify type or guess type?
|
592
|
+
# Data is the exact type, so its constructor should be exact
|
593
|
+
# and guesswork should be clearly labeled
|
594
|
+
|
595
|
+
# @param value [Data::Array,Enumerable]
|
596
|
+
# @param type [SingleCompleteType,Type]
|
597
|
+
def initialize(value, type:)
|
598
|
+
type = Type::Factory.make_type(type)
|
599
|
+
self.class.assert_type_matches_class(type)
|
600
|
+
@type = type
|
601
|
+
|
602
|
+
typed_value = case value
|
603
|
+
when Data::Array
|
604
|
+
unless value.type == type
|
605
|
+
raise ArgumentError,
|
606
|
+
"Specified type is #{type.inspect} but value type is #{value.type.inspect}"
|
607
|
+
end
|
608
|
+
|
609
|
+
value.exact_value
|
610
|
+
else
|
611
|
+
# TODO: Dict??
|
612
|
+
value.map do |i|
|
613
|
+
Data.make_typed(type.child, i)
|
614
|
+
end
|
615
|
+
end
|
616
|
+
super(typed_value)
|
617
|
+
end
|
618
|
+
end
|
619
|
+
|
620
|
+
# A fixed size, heterogenerous tuple.
|
621
|
+
#
|
622
|
+
# (The item count is fixed, not the byte size.)
|
623
|
+
class Struct < Container
|
624
|
+
def self.type_code
|
625
|
+
"r"
|
626
|
+
end
|
627
|
+
|
628
|
+
def self.alignment
|
629
|
+
8
|
630
|
+
end
|
631
|
+
|
632
|
+
# @param value [::Array]
|
633
|
+
def self.from_items(value, mode:, type:)
|
634
|
+
value.freeze
|
635
|
+
return value if mode == :plain
|
636
|
+
|
637
|
+
new(value, type: type)
|
638
|
+
end
|
639
|
+
|
640
|
+
# @param value [::Object] (#size, #each)
|
641
|
+
# @param type [Type]
|
642
|
+
# @return [Struct]
|
643
|
+
def self.from_typed(value, type:)
|
644
|
+
new(value, type: type)
|
645
|
+
end
|
646
|
+
|
647
|
+
# @param value [Data::Struct,Enumerable]
|
648
|
+
# @param type [SingleCompleteType,Type]
|
649
|
+
def initialize(value, type:)
|
650
|
+
type = Type::Factory.make_type(type)
|
651
|
+
self.class.assert_type_matches_class(type)
|
652
|
+
@type = type
|
653
|
+
|
654
|
+
typed_value = case value
|
655
|
+
when self.class
|
656
|
+
unless value.type == type
|
657
|
+
raise ArgumentError,
|
658
|
+
"Specified type is #{type.inspect} but value type is #{value.type.inspect}"
|
659
|
+
end
|
660
|
+
|
661
|
+
value.exact_value
|
662
|
+
else
|
663
|
+
member_types = type.members
|
664
|
+
unless value.size == member_types.size
|
665
|
+
raise ArgumentError, "Specified type has #{member_types.size} members " \
|
666
|
+
"but value has #{value.size} members"
|
667
|
+
end
|
668
|
+
|
669
|
+
member_types.zip(value).map do |item_type, item|
|
670
|
+
Data.make_typed(item_type, item)
|
671
|
+
end
|
672
|
+
end
|
673
|
+
super(typed_value)
|
674
|
+
end
|
675
|
+
|
676
|
+
def ==(other)
|
677
|
+
case other
|
678
|
+
when ::Struct
|
679
|
+
@value.size == other.size &&
|
680
|
+
@value.zip(other.to_a).all? { |i, other_i| i == other_i }
|
681
|
+
else
|
682
|
+
super
|
683
|
+
end
|
684
|
+
end
|
685
|
+
end
|
686
|
+
|
687
|
+
# Dictionary/Hash entry.
|
688
|
+
# TODO: shouldn't instantiate?
|
689
|
+
class DictEntry < Struct
|
690
|
+
def self.type_code
|
691
|
+
"e"
|
692
|
+
end
|
693
|
+
|
694
|
+
# @param value [::Array]
|
695
|
+
def self.from_items(value, mode:, type:) # rubocop:disable Lint/UnusedMethodArgument
|
696
|
+
value.freeze
|
697
|
+
# DictEntry ignores the :exact mode
|
698
|
+
value
|
699
|
+
end
|
700
|
+
|
701
|
+
# @param value [::Object] (#size, #each)
|
702
|
+
# @param type [Type]
|
703
|
+
# @return [DictEntry]
|
704
|
+
def self.from_typed(value, type:)
|
705
|
+
new(value, type: type)
|
706
|
+
end
|
707
|
+
end
|
708
|
+
|
709
|
+
# A generic type.
|
710
|
+
#
|
711
|
+
# Implementation note: @value is a {Data::Base}.
|
712
|
+
class Variant < Container
|
713
|
+
def self.type_code
|
714
|
+
"v"
|
715
|
+
end
|
716
|
+
|
717
|
+
def self.alignment
|
718
|
+
1
|
719
|
+
end
|
720
|
+
|
721
|
+
def value
|
722
|
+
@value.value
|
723
|
+
end
|
724
|
+
|
725
|
+
# @param member_type [Type]
|
726
|
+
def self.from_items(value, mode:, member_type:)
|
727
|
+
return value if mode == :plain
|
728
|
+
|
729
|
+
new(value, member_type: member_type)
|
730
|
+
end
|
731
|
+
|
732
|
+
# @param value [::Object]
|
733
|
+
# @param type [Type]
|
734
|
+
# @return [Variant]
|
735
|
+
def self.from_typed(value, type:)
|
736
|
+
assert_type_matches_class(type)
|
737
|
+
|
738
|
+
# nil: decide on type of value
|
739
|
+
new(value, member_type: nil)
|
740
|
+
end
|
741
|
+
|
742
|
+
# @return [Type]
|
743
|
+
def self.type
|
744
|
+
# memoize
|
745
|
+
@type ||= Type.new(type_code).freeze
|
746
|
+
end
|
747
|
+
|
748
|
+
# Note that for Variants type.to_s=="v",
|
749
|
+
# for the specific see {Variant#member_type}
|
750
|
+
# @return [Type] the exact type of this value
|
751
|
+
def type
|
752
|
+
self.class.type
|
753
|
+
end
|
754
|
+
|
755
|
+
# @return [Type]
|
756
|
+
attr_reader :member_type
|
757
|
+
|
758
|
+
# Determine the type of *value*
|
759
|
+
# @param value [::Object]
|
760
|
+
# @return [Type]
|
761
|
+
# @api private
|
762
|
+
# See also {PacketMarshaller.make_variant}
|
763
|
+
def self.guess_type(value)
|
764
|
+
sct, = PacketMarshaller.make_variant(value)
|
765
|
+
DBus.type(sct)
|
766
|
+
end
|
767
|
+
|
768
|
+
# @param member_type [SingleCompleteType,Type,nil]
|
769
|
+
def initialize(value, member_type:)
|
770
|
+
member_type = Type::Factory.make_type(member_type) if member_type
|
771
|
+
# TODO: validate that the given *member_type* matches *value*
|
772
|
+
case value
|
773
|
+
when Data::Variant
|
774
|
+
# Copy the contained value instead of boxing it more
|
775
|
+
# TODO: except perhaps for round-tripping in exact mode?
|
776
|
+
@member_type = value.member_type
|
777
|
+
value = value.exact_value
|
778
|
+
when Data::Base
|
779
|
+
@member_type = member_type || value.type
|
780
|
+
raise ArgumentError, "Variant type #{@member_type} does not match value type #{value.type}" \
|
781
|
+
unless @member_type == value.type
|
782
|
+
else
|
783
|
+
@member_type = member_type || self.class.guess_type(value)
|
784
|
+
value = Data.make_typed(@member_type, value)
|
785
|
+
end
|
786
|
+
super(value)
|
787
|
+
end
|
788
|
+
|
789
|
+
# Internal helpers to keep the {DBus.variant} method working.
|
790
|
+
# Formerly it returned just a pair of [DBus.type(string_type), value]
|
791
|
+
# so let's provide [0], [1], .first, .last
|
792
|
+
def [](index)
|
793
|
+
case index
|
794
|
+
when 0
|
795
|
+
member_type
|
796
|
+
when 1
|
797
|
+
value
|
798
|
+
else
|
799
|
+
raise ArgumentError, "DBus.variant can only be indexed with 0 or 1, seen #{index.inspect}"
|
800
|
+
end
|
801
|
+
end
|
802
|
+
|
803
|
+
# @see #[]
|
804
|
+
def first
|
805
|
+
self[0]
|
806
|
+
end
|
807
|
+
|
808
|
+
# @see #[]
|
809
|
+
def last
|
810
|
+
self[1]
|
811
|
+
end
|
812
|
+
end
|
813
|
+
|
814
|
+
consts = constants.map { |c_sym| const_get(c_sym) }
|
815
|
+
classes = consts.find_all { |c| c.respond_to?(:type_code) }
|
816
|
+
by_type_code = classes.map { |cl| [cl.type_code, cl] }.to_h
|
817
|
+
|
818
|
+
# { "b" => Data::Boolean, "s" => Data::String, ...}
|
819
|
+
BY_TYPE_CODE = by_type_code
|
820
|
+
end
|
821
|
+
end
|