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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/NEWS.md +160 -0
  3. data/README.md +3 -5
  4. data/Rakefile +18 -8
  5. data/VERSION +1 -1
  6. data/doc/Reference.md +106 -7
  7. data/examples/doc/_extract_examples +7 -0
  8. data/examples/gdbus/gdbus +31 -24
  9. data/examples/no-introspect/nm-test.rb +2 -0
  10. data/examples/no-introspect/tracker-test.rb +3 -1
  11. data/examples/rhythmbox/playpause.rb +2 -1
  12. data/examples/service/call_service.rb +2 -1
  13. data/examples/service/complex-property.rb +21 -0
  14. data/examples/service/service_newapi.rb +1 -1
  15. data/examples/simple/call_introspect.rb +1 -0
  16. data/examples/simple/get_id.rb +2 -1
  17. data/examples/simple/properties.rb +2 -0
  18. data/examples/utils/listnames.rb +1 -0
  19. data/examples/utils/notify.rb +1 -0
  20. data/lib/dbus/api_options.rb +9 -0
  21. data/lib/dbus/auth.rb +20 -15
  22. data/lib/dbus/bus.rb +123 -75
  23. data/lib/dbus/bus_name.rb +12 -8
  24. data/lib/dbus/core_ext/class/attribute.rb +1 -1
  25. data/lib/dbus/data.rb +821 -0
  26. data/lib/dbus/emits_changed_signal.rb +83 -0
  27. data/lib/dbus/error.rb +4 -2
  28. data/lib/dbus/introspect.rb +132 -31
  29. data/lib/dbus/logger.rb +3 -1
  30. data/lib/dbus/marshall.rb +247 -296
  31. data/lib/dbus/matchrule.rb +16 -16
  32. data/lib/dbus/message.rb +44 -37
  33. data/lib/dbus/message_queue.rb +16 -10
  34. data/lib/dbus/object.rb +358 -24
  35. data/lib/dbus/object_path.rb +11 -6
  36. data/lib/dbus/proxy_object.rb +22 -1
  37. data/lib/dbus/proxy_object_factory.rb +13 -7
  38. data/lib/dbus/proxy_object_interface.rb +63 -30
  39. data/lib/dbus/raw_message.rb +91 -0
  40. data/lib/dbus/type.rb +318 -86
  41. data/lib/dbus/xml.rb +32 -17
  42. data/lib/dbus.rb +14 -7
  43. data/ruby-dbus.gemspec +7 -3
  44. data/spec/async_spec.rb +2 -0
  45. data/spec/binding_spec.rb +2 -0
  46. data/spec/bus_and_xml_backend_spec.rb +2 -0
  47. data/spec/bus_driver_spec.rb +2 -0
  48. data/spec/bus_name_spec.rb +3 -1
  49. data/spec/bus_spec.rb +2 -0
  50. data/spec/byte_array_spec.rb +2 -0
  51. data/spec/client_robustness_spec.rb +4 -2
  52. data/spec/data/marshall.yaml +1667 -0
  53. data/spec/data_spec.rb +673 -0
  54. data/spec/emits_changed_signal_spec.rb +58 -0
  55. data/spec/err_msg_spec.rb +2 -0
  56. data/spec/introspect_xml_parser_spec.rb +2 -0
  57. data/spec/introspection_spec.rb +2 -0
  58. data/spec/main_loop_spec.rb +3 -1
  59. data/spec/node_spec.rb +23 -0
  60. data/spec/object_path_spec.rb +3 -0
  61. data/spec/object_spec.rb +138 -0
  62. data/spec/packet_marshaller_spec.rb +41 -0
  63. data/spec/packet_unmarshaller_spec.rb +248 -0
  64. data/spec/property_spec.rb +192 -5
  65. data/spec/proxy_object_spec.rb +2 -0
  66. data/spec/server_robustness_spec.rb +2 -0
  67. data/spec/server_spec.rb +2 -0
  68. data/spec/service_newapi.rb +70 -70
  69. data/spec/session_bus_spec.rb +3 -1
  70. data/spec/session_bus_spec_manual.rb +2 -0
  71. data/spec/signal_spec.rb +5 -3
  72. data/spec/spec_helper.rb +37 -9
  73. data/spec/thread_safety_spec.rb +2 -0
  74. data/spec/tools/dbus-limited-session.conf +4 -0
  75. data/spec/type_spec.rb +214 -6
  76. data/spec/value_spec.rb +16 -1
  77. data/spec/variant_spec.rb +4 -2
  78. data/spec/zzz_quit_spec.rb +16 -0
  79. 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