ruby-dbus 0.16.0 → 0.18.1

Sign up to get free protection for your applications and to get access to all the features.
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