ruby-dbus 0.16.0 → 0.18.0.beta2

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