ruby-dbus 0.17.0 → 0.18.0.beta3

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