ruby-dbus 0.16.0 → 0.25.0

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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/NEWS.md +288 -3
  3. data/README.md +3 -5
  4. data/Rakefile +27 -12
  5. data/VERSION +1 -1
  6. data/doc/Reference.md +109 -10
  7. data/examples/doc/_extract_examples +7 -0
  8. data/examples/no-bus/pulseaudio.rb +50 -0
  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 +3 -3
  15. data/examples/simple/call_introspect.rb +1 -0
  16. data/examples/simple/get_id.rb +6 -3
  17. data/examples/simple/properties.rb +2 -0
  18. data/examples/utils/listnames.rb +10 -1
  19. data/examples/utils/notify.rb +1 -0
  20. data/lib/dbus/api_options.rb +9 -0
  21. data/lib/dbus/auth.rb +307 -217
  22. data/lib/dbus/bus.rb +155 -588
  23. data/lib/dbus/bus_name.rb +12 -8
  24. data/lib/dbus/connection.rb +363 -0
  25. data/lib/dbus/core_ext/class/attribute.rb +1 -1
  26. data/lib/dbus/data.rb +821 -0
  27. data/lib/dbus/emits_changed_signal.rb +83 -0
  28. data/lib/dbus/error.rb +4 -2
  29. data/lib/dbus/introspect.rb +133 -31
  30. data/lib/dbus/logger.rb +6 -3
  31. data/lib/dbus/main.rb +66 -0
  32. data/lib/dbus/marshall.rb +247 -296
  33. data/lib/dbus/matchrule.rb +16 -16
  34. data/lib/dbus/message.rb +66 -57
  35. data/lib/dbus/message_queue.rb +40 -25
  36. data/lib/dbus/node_tree.rb +105 -0
  37. data/lib/dbus/object.rb +442 -33
  38. data/lib/dbus/object_manager.rb +61 -0
  39. data/lib/dbus/object_path.rb +11 -6
  40. data/lib/dbus/object_server.rb +149 -0
  41. data/lib/dbus/org.freedesktop.DBus.xml +97 -0
  42. data/lib/dbus/platform.rb +26 -0
  43. data/lib/dbus/proxy_object.rb +30 -11
  44. data/lib/dbus/proxy_object_factory.rb +13 -7
  45. data/lib/dbus/proxy_object_interface.rb +63 -30
  46. data/lib/dbus/proxy_service.rb +107 -0
  47. data/lib/dbus/raw_message.rb +91 -0
  48. data/lib/dbus/type.rb +319 -86
  49. data/lib/dbus/xml.rb +40 -19
  50. data/lib/dbus.rb +28 -17
  51. data/ruby-dbus.gemspec +13 -6
  52. data/spec/async_spec.rb +2 -0
  53. data/spec/auth_spec.rb +225 -0
  54. data/spec/binding_spec.rb +2 -0
  55. data/spec/bus_and_xml_backend_spec.rb +5 -1
  56. data/spec/bus_connection_spec.rb +93 -0
  57. data/spec/bus_name_spec.rb +3 -1
  58. data/spec/bus_spec.rb +2 -0
  59. data/spec/byte_array_spec.rb +2 -0
  60. data/spec/client_robustness_spec.rb +4 -2
  61. data/spec/connection_spec.rb +37 -0
  62. data/spec/coverage_helper.rb +39 -0
  63. data/spec/data/marshall.yaml +1667 -0
  64. data/spec/data_spec.rb +673 -0
  65. data/spec/dbus_spec.rb +22 -0
  66. data/spec/emits_changed_signal_spec.rb +58 -0
  67. data/spec/err_msg_spec.rb +2 -0
  68. data/spec/introspect_xml_parser_spec.rb +2 -0
  69. data/spec/introspection_spec.rb +2 -0
  70. data/spec/main_loop_spec.rb +17 -1
  71. data/spec/message_spec.rb +21 -0
  72. data/spec/mock-service/cockpit-dbustests.rb +29 -0
  73. data/spec/mock-service/com.redhat.Cockpit.DBusTests.xml +180 -0
  74. data/spec/mock-service/org.ruby.service.service +4 -0
  75. data/spec/mock-service/org.rubygems.ruby_dbus.DBusTests.service +4 -0
  76. data/spec/{service_newapi.rb → mock-service/spaghetti-monster.rb} +122 -75
  77. data/spec/node_spec.rb +65 -0
  78. data/spec/object_manager_spec.rb +33 -0
  79. data/spec/object_path_spec.rb +3 -0
  80. data/spec/object_server_spec.rb +138 -0
  81. data/spec/object_spec.rb +194 -0
  82. data/spec/packet_marshaller_spec.rb +41 -0
  83. data/spec/packet_unmarshaller_spec.rb +248 -0
  84. data/spec/platform_spec.rb +14 -0
  85. data/spec/property_spec.rb +214 -12
  86. data/spec/proxy_object_interface_spec.rb +35 -0
  87. data/spec/proxy_object_spec.rb +2 -0
  88. data/spec/{bus_driver_spec.rb → proxy_service_spec.rb} +15 -8
  89. data/spec/raw_message_spec.rb +32 -0
  90. data/spec/server_robustness_spec.rb +20 -6
  91. data/spec/session_bus_spec.rb +36 -1
  92. data/spec/session_bus_spec_manual.rb +2 -0
  93. data/spec/signal_spec.rb +16 -3
  94. data/spec/spec_helper.rb +25 -33
  95. data/spec/thread_safety_spec.rb +55 -12
  96. data/spec/tools/dbus-launch-simple +9 -6
  97. data/spec/tools/dbus-limited-session.conf +32 -0
  98. data/spec/tools/test_env +26 -6
  99. data/spec/type_spec.rb +214 -6
  100. data/spec/value_spec.rb +16 -1
  101. data/spec/variant_spec.rb +4 -2
  102. data/spec/zzz_quit_spec.rb +16 -0
  103. metadata +92 -20
  104. data/examples/gdbus/gdbus +0 -257
  105. data/examples/gdbus/gdbus.glade +0 -98
  106. data/examples/gdbus/launch.sh +0 -4
  107. data/spec/server_spec.rb +0 -53
  108. data/spec/tools/test_server +0 -39
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is part of the ruby-dbus project
4
+ # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
5
+ # Copyright (C) 2023 Martin Vidner
6
+ #
7
+ # This library is free software; you can redistribute it and/or
8
+ # modify it under the terms of the GNU Lesser General Public
9
+ # License, version 2.1 as published by the Free Software Foundation.
10
+ # See the file "COPYING" for the exact licensing terms.
11
+
12
+ require_relative "node_tree"
13
+
14
+ module DBus
15
+ # Used by clients to represent a named service on the other side of the bus.
16
+ #
17
+ # Formerly this class was intermixed with {ObjectServer} as Service.
18
+ #
19
+ # @example Usage
20
+ # svc = DBus.system_bus["org.freedesktop.machine1"]
21
+ # manager = svc["/org/freedesktop/machine1"]
22
+ # p manager.ListImages
23
+ class ProxyService < NodeTree
24
+ # @return [BusName,nil] The service name.
25
+ # Will be nil for a {PeerConnection}
26
+ attr_reader :name
27
+ # @return [Connection] The connection we're using.
28
+ attr_reader :connection
29
+
30
+ # @param connection [Connection] The connection we're using.
31
+ def initialize(name, connection)
32
+ @name = BusName.new(name)
33
+ @connection = connection
34
+ super()
35
+ end
36
+
37
+ # Determine whether the service name already exists.
38
+ def exists?
39
+ bus = connection # TODO: raise a better error if this is a peer connection
40
+ bus.proxy.ListNames[0].member?(@name)
41
+ end
42
+
43
+ # Perform an introspection on all the objects on the service
44
+ # (starting recursively from the root).
45
+ def introspect
46
+ raise NotImplementedError if block_given?
47
+
48
+ rec_introspect(@root, "/")
49
+ self
50
+ end
51
+
52
+ # Retrieves an object at the given _path_.
53
+ # @param path [ObjectPath]
54
+ # @return [ProxyObject]
55
+ def [](path)
56
+ object(path, api: ApiOptions::A1)
57
+ end
58
+
59
+ # Retrieves an object at the given _path_
60
+ # whose methods always return an array.
61
+ # @param path [ObjectPath]
62
+ # @param api [ApiOptions]
63
+ # @return [ProxyObject]
64
+ def object(path, api: ApiOptions::A0)
65
+ node = get_node(path, create: true)
66
+ if node.object.nil? || node.object.api != api
67
+ node.object = ProxyObject.new(
68
+ @connection, @name, path,
69
+ api: api
70
+ )
71
+ end
72
+ node.object
73
+ end
74
+
75
+ private
76
+
77
+ # Perform a recursive retrospection on the given current _node_
78
+ # on the given _path_.
79
+ def rec_introspect(node, path)
80
+ xml = connection.introspect_data(@name, path)
81
+ intfs, subnodes = IntrospectXMLParser.new(xml).parse
82
+ subnodes.each do |nodename|
83
+ subnode = node[nodename] = Node.new(nodename)
84
+ subpath = if path == "/"
85
+ "/#{nodename}"
86
+ else
87
+ "#{path}/#{nodename}"
88
+ end
89
+ rec_introspect(subnode, subpath)
90
+ end
91
+ return if intfs.empty?
92
+
93
+ node.object = ProxyObjectFactory.new(xml, @connection, @name, path).build
94
+ end
95
+ end
96
+
97
+ # A hack for pretending that a {PeerConnection} has a single unnamed {ProxyService}
98
+ # so that we can get {ProxyObject}s from it.
99
+ class ProxyPeerService < ProxyService
100
+ # @param connection [Connection] The peer connection we're using.
101
+ def initialize(connection)
102
+ # this way we disallow ProxyService taking a nil name by accident
103
+ super(":0.0", connection)
104
+ @name = nil
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,91 @@
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
+ # A message while it is being parsed: a binary string,
13
+ # with a position cursor (*pos*), and an *endianness* tag.
14
+ class RawMessage
15
+ # @return [String]
16
+ # attr_reader :bytes
17
+
18
+ # @return [Integer] position in the byte buffer
19
+ attr_reader :pos
20
+
21
+ # @return [:little,:big]
22
+ attr_reader :endianness
23
+
24
+ # @param bytes [String]
25
+ # @param endianness [:little,:big,nil]
26
+ # if not given, read the 1st byte of *bytes*
27
+ def initialize(bytes, endianness = nil)
28
+ @bytes = bytes
29
+ @pos = 0
30
+ @endianness = endianness || self.class.endianness(@bytes[0])
31
+ end
32
+
33
+ # Get the endiannes switch as a Symbol,
34
+ # which will make using it slightly more efficient
35
+ # @param tag_char [String]
36
+ # @return [:little,:big]
37
+ def self.endianness(tag_char)
38
+ case tag_char
39
+ when LIL_END
40
+ :little
41
+ when BIG_END
42
+ :big
43
+ else
44
+ raise InvalidPacketException, "Incorrect endianness #{tag_char.inspect}"
45
+ end
46
+ end
47
+
48
+ # @return [void]
49
+ # @raise IncompleteBufferException if there are not enough bytes remaining
50
+ def want!(size)
51
+ raise IncompleteBufferException if @pos + size > @bytes.bytesize
52
+ end
53
+
54
+ # @return [String]
55
+ # @raise IncompleteBufferException if there are not enough bytes remaining
56
+ # TODO: stress test this with encodings. always binary?
57
+ def read(size)
58
+ want!(size)
59
+ ret = @bytes.slice(@pos, size)
60
+ @pos += size
61
+ ret
62
+ end
63
+
64
+ # @return [String]
65
+ # @api private
66
+ def remaining_bytes
67
+ # This returns "" if pos is just past the end of the string,
68
+ # and nil if it is further.
69
+ @bytes[@pos..-1]
70
+ end
71
+
72
+ # Align the *pos* index on a multiple of *alignment*
73
+ # @param alignment [Integer] must be 1, 2, 4 or 8
74
+ # @return [void]
75
+ def align(alignment)
76
+ case alignment
77
+ when 1
78
+ nil
79
+ when 2, 4, 8
80
+ bits = alignment - 1
81
+ pad_size = ((@pos + bits) & ~bits) - @pos
82
+ pad = read(pad_size)
83
+ unless pad.bytes.all?(&:zero?)
84
+ raise InvalidPacketException, "Alignment bytes are not NUL"
85
+ end
86
+ else
87
+ raise ArgumentError, "Unsupported alignment #{alignment}"
88
+ end
89
+ end
90
+ end
91
+ end
data/lib/dbus/type.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # dbus/type.rb - module containing low-level D-Bus data type information
2
4
  #
3
5
  # This file is part of the ruby-dbus project
@@ -9,13 +11,33 @@
9
11
  # See the file "COPYING" for the exact licensing terms.
10
12
 
11
13
  module DBus
12
- # = D-Bus type module
14
+ # Like a {Signature} but containing only a single complete type.
15
+ #
16
+ # For documentation purposes only.
17
+ class SingleCompleteType < String; end
18
+
19
+ # Zero or more {SingleCompleteType}s; its own type code is "g".
20
+ # For example "ssv" for a method taking two Strings and a Variant/
21
+ #
22
+ # For documentation purposes only.
23
+ class Signature < String; end
24
+
25
+ # Similar to {Signature} but for {DBus::Object.define_method},
26
+ # contains names and direction of the parameters.
27
+ # For example "in query:s, in case_sensitive:b, out results:ao".
13
28
  #
14
- # This module containts the constants of the types specified in the D-Bus
15
- # protocol.
16
- module Type
29
+ # For documentation purposes only.
30
+ class Prototype < String; end
31
+
32
+ # Represents the D-Bus types.
33
+ #
34
+ # Corresponds to {SingleCompleteType}.
35
+ # Instances are immutable/frozen once fully constructed.
36
+ #
37
+ # See also {DBus::Data::Signature} which is "type on the wire".
38
+ class Type
17
39
  # Mapping from type number to name and alignment.
18
- TypeMapping = {
40
+ TYPE_MAPPING = {
19
41
  0 => ["INVALID", nil],
20
42
  "y" => ["BYTE", 1],
21
43
  "b" => ["BOOLEAN", 4],
@@ -36,7 +58,7 @@ module DBus
36
58
  "h" => ["UNIX_FD", 4]
37
59
  }.freeze
38
60
  # Defines the set of constants
39
- TypeMapping.each_pair do |key, value|
61
+ TYPE_MAPPING.each_pair do |key, value|
40
62
  Type.const_set(value.first, key)
41
63
  end
42
64
 
@@ -44,87 +66,141 @@ module DBus
44
66
  class SignatureException < Exception
45
67
  end
46
68
 
47
- # = D-Bus type conversion class
69
+ # Formerly this was a Module and there was a DBus::Type::Type class
70
+ # but the class got too prominent to keep its double double name.
71
+ # This is for backward compatibility.
72
+ Type = self # rubocop:disable Naming/ConstantName
73
+
74
+ # @return [String] the signature type character, eg "s" or "e".
75
+ attr_reader :sigtype
76
+ # @return [Array<Type>] contained member types.
77
+ attr_reader :members
78
+
79
+ # Use {DBus.type} instead, because this allows constructing
80
+ # incomplete or invalid types, for backward compatibility.
48
81
  #
49
- # Helper class for representing a D-Bus type.
50
- class Type
51
- # Returns the signature type number.
52
- attr_reader :sigtype
53
- # Return contained member types.
54
- attr_reader :members
55
-
56
- # Create a new type instance for type number _sigtype_.
57
- def initialize(sigtype)
58
- if !TypeMapping.keys.member?(sigtype)
59
- raise SignatureException, "Unknown key in signature: #{sigtype.chr}"
82
+ # @param abstract [Boolean] allow abstract types "r" and "e"
83
+ # (Enabled for internal usage by {Parser}.)
84
+ def initialize(sigtype, abstract: false)
85
+ if !TYPE_MAPPING.keys.member?(sigtype)
86
+ case sigtype
87
+ when ")"
88
+ raise SignatureException, "STRUCT unexpectedly closed: )"
89
+ when "}"
90
+ raise SignatureException, "DICT_ENTRY unexpectedly closed: }"
91
+ else
92
+ raise SignatureException, "Unknown type code #{sigtype.inspect}"
60
93
  end
61
- @sigtype = sigtype
62
- @members = []
63
- end
64
-
65
- # Return the required alignment for the type.
66
- def alignment
67
- TypeMapping[@sigtype].last
68
94
  end
69
95
 
70
- # Return a string representation of the type according to the
71
- # D-Bus specification.
72
- def to_s
73
- case @sigtype
96
+ unless abstract
97
+ case sigtype
74
98
  when STRUCT
75
- "(" + @members.collect(&:to_s).join + ")"
76
- when ARRAY
77
- "a" + child.to_s
99
+ raise SignatureException, "Abstract STRUCT, use \"(...)\" instead of \"#{STRUCT}\""
78
100
  when DICT_ENTRY
79
- "{" + @members.collect(&:to_s).join + "}"
80
- else
81
- if !TypeMapping.keys.member?(@sigtype)
82
- raise NotImplementedError
83
- end
84
- @sigtype.chr
101
+ raise SignatureException, "Abstract DICT_ENTRY, use \"{..}\" instead of \"#{DICT_ENTRY}\""
85
102
  end
86
103
  end
87
104
 
88
- # Add a new member type _a_.
89
- def <<(a)
90
- if ![STRUCT, ARRAY, DICT_ENTRY].member?(@sigtype)
91
- raise SignatureException
92
- end
93
- raise SignatureException if @sigtype == ARRAY && !@members.empty?
94
- if @sigtype == DICT_ENTRY
95
- if @members.size == 2
96
- raise SignatureException, "Dict entries have exactly two members"
97
- end
98
- if @members.empty?
99
- if [STRUCT, ARRAY, DICT_ENTRY].member?(a.sigtype)
100
- raise SignatureException, "Dict entry keys must be basic types"
101
- end
102
- end
103
- end
104
- @members << a
105
+ @sigtype = sigtype.freeze
106
+ @members = [] # not frozen yet, Parser#parse_one or Factory will do it
107
+ freeze
108
+ end
109
+
110
+ # A Type is equal to
111
+ # - another Type with the same string representation
112
+ # - a String ({SingleCompleteType}) describing the type
113
+ def ==(other)
114
+ case other
115
+ when ::String
116
+ to_s == other
117
+ else
118
+ eql?(other)
105
119
  end
120
+ end
121
+
122
+ # A Type is eql? to
123
+ # - another Type with the same string representation
124
+ #
125
+ # Hash key equality
126
+ # See https://ruby-doc.org/core-3.0.0/Object.html#method-i-eql-3F
127
+ def eql?(other)
128
+ return false unless other.is_a?(Type)
129
+
130
+ @sigtype == other.sigtype && @members == other.members
131
+ end
132
+
133
+ # Return the required alignment for the type.
134
+ def alignment
135
+ TYPE_MAPPING[@sigtype].last
136
+ end
106
137
 
107
- # Return the first contained member type.
108
- def child
109
- @members[0]
138
+ # Return a string representation of the type according to the
139
+ # D-Bus specification.
140
+ def to_s
141
+ case @sigtype
142
+ when STRUCT
143
+ "(#{@members.collect(&:to_s).join})"
144
+ when ARRAY
145
+ "a#{child}"
146
+ when DICT_ENTRY
147
+ "{#{@members.collect(&:to_s).join}}"
148
+ else
149
+ @sigtype.chr
110
150
  end
151
+ end
152
+
153
+ # Add a new member type _item_.
154
+ # @param item [Type]
155
+ def <<(item)
156
+ raise ArgumentError unless item.is_a?(Type)
111
157
 
112
- def inspect
113
- s = TypeMapping[@sigtype].first
114
- if [STRUCT, ARRAY].member?(@sigtype)
115
- s += ": " + @members.inspect
158
+ if ![STRUCT, ARRAY, DICT_ENTRY].member?(@sigtype)
159
+ raise SignatureException
160
+ end
161
+ raise SignatureException if @sigtype == ARRAY && !@members.empty?
162
+
163
+ if @sigtype == DICT_ENTRY
164
+ case @members.size
165
+ when 2
166
+ raise SignatureException,
167
+ "DICT_ENTRY must have 2 subtypes, found 3 or more: #{@members.inspect} << #{item.inspect}"
168
+ when 0
169
+ if [STRUCT, ARRAY, DICT_ENTRY, VARIANT].member?(item.sigtype)
170
+ raise SignatureException, "DICT_ENTRY key must be basic (non-container)"
171
+ end
116
172
  end
117
- s
118
173
  end
119
- end # class Type
174
+ @members << item
175
+ end
176
+
177
+ # Return the first contained member type.
178
+ def child
179
+ @members[0]
180
+ end
181
+
182
+ def inspect
183
+ s = TYPE_MAPPING[@sigtype].first
184
+ if [STRUCT, ARRAY, DICT_ENTRY].member?(@sigtype)
185
+ s += ": #{@members.inspect}"
186
+ end
187
+ s
188
+ end
120
189
 
121
190
  # = D-Bus type parser class
122
191
  #
123
192
  # Helper class to parse a type signature in the protocol.
193
+ # @api private
124
194
  class Parser
125
195
  # Create a new parser for the given _signature_.
196
+ # @param signature [Signature]
126
197
  def initialize(signature)
127
198
  @signature = signature
199
+ if signature.size > 255
200
+ msg = "Potential signature is longer than 255 characters (#{@signature.size}): #{@signature}"
201
+ raise SignatureException, msg
202
+ end
203
+
128
204
  @idx = 0
129
205
  end
130
206
 
@@ -135,57 +211,214 @@ module DBus
135
211
  c
136
212
  end
137
213
 
138
- # Parse one character _c_ of the signature.
139
- def parse_one(c)
214
+ # Parse one character _char_ of the signature.
215
+ # @param for_array [Boolean] are we parsing an immediate child of an ARRAY
216
+ # @return [Type]
217
+ def parse_one(char, for_array: false)
140
218
  res = nil
141
- case c
219
+ case char
142
220
  when "a"
143
221
  res = Type.new(ARRAY)
144
- c = nextchar
145
- raise SignatureException, "Parse error in #{@signature}" if c.nil?
146
- child = parse_one(c)
222
+ char = nextchar
223
+ raise SignatureException, "Empty ARRAY in #{@signature}" if char.nil?
224
+
225
+ child = parse_one(char, for_array: true)
147
226
  res << child
148
227
  when "("
149
- res = Type.new(STRUCT)
150
- while (c = nextchar) && c != ")"
151
- res << parse_one(c)
228
+ res = Type.new(STRUCT, abstract: true)
229
+ while (char = nextchar) && char != ")"
230
+ res << parse_one(char)
152
231
  end
153
- raise SignatureException, "Parse error in #{@signature}" if c.nil?
232
+ raise SignatureException, "STRUCT not closed in #{@signature}" if char.nil?
233
+ raise SignatureException, "Empty STRUCT in #{@signature}" if res.members.empty?
154
234
  when "{"
155
- res = Type.new(DICT_ENTRY)
156
- while (c = nextchar) && c != "}"
157
- res << parse_one(c)
235
+ raise SignatureException, "DICT_ENTRY not an immediate child of an ARRAY" unless for_array
236
+
237
+ res = Type.new(DICT_ENTRY, abstract: true)
238
+
239
+ # key type, value type
240
+ 2.times do |i|
241
+ char = nextchar
242
+ raise SignatureException, "DICT_ENTRY not closed in #{@signature}" if char.nil?
243
+
244
+ raise SignatureException, "DICT_ENTRY must have 2 subtypes, found #{i} in #{@signature}" if char == "}"
245
+
246
+ res << parse_one(char)
158
247
  end
159
- raise SignatureException, "Parse error in #{@signature}" if c.nil?
248
+
249
+ # closing "}"
250
+ char = nextchar
251
+ raise SignatureException, "DICT_ENTRY not closed in #{@signature}" if char.nil?
252
+
253
+ raise SignatureException, "DICT_ENTRY must have 2 subtypes, found 3 or more in #{@signature}" if char != "}"
160
254
  else
161
- res = Type.new(c)
255
+ res = Type.new(char)
162
256
  end
257
+ res.members.freeze
163
258
  res
164
259
  end
165
260
 
166
261
  # Parse the entire signature, return a DBus::Type object.
262
+ # @return [Array<Type>]
167
263
  def parse
168
264
  @idx = 0
169
265
  ret = []
170
266
  while (c = nextchar)
171
267
  ret << parse_one(c)
172
268
  end
173
- ret
269
+ ret.freeze
270
+ end
271
+
272
+ # Parse one {SingleCompleteType}
273
+ # @return [Type]
274
+ def parse1
275
+ c = nextchar
276
+ raise SignatureException, "Empty signature, expecting a Single Complete Type" if c.nil?
277
+
278
+ t = parse_one(c)
279
+ raise SignatureException, "Has more than a Single Complete Type: #{@signature}" unless nextchar.nil?
280
+
281
+ t.freeze
282
+ end
283
+ end
284
+
285
+ class Factory
286
+ # @param type [Type,SingleCompleteType,Class]
287
+ # @see from_plain_class
288
+ # @return [Type] (frozen)
289
+ def self.make_type(type)
290
+ case type
291
+ when Type
292
+ type
293
+ when String
294
+ DBus.type(type)
295
+ when Class
296
+ from_plain_class(type)
297
+ else
298
+ msg = "Expecting DBus::Type, DBus::SingleCompleteType(aka ::String), or Class, got #{type.inspect}"
299
+ raise ArgumentError, msg
300
+ end
174
301
  end
175
- end # class Parser
176
- end # module Type
302
+
303
+ # Make a {Type} corresponding to some plain classes:
304
+ # - String
305
+ # - Float
306
+ # - DBus::ObjectPath
307
+ # - DBus::Signature, DBus::SingleCompleteType
308
+ # @param klass [Class]
309
+ # @return [Type] (frozen)
310
+ def self.from_plain_class(klass)
311
+ @signature_type ||= DBus.type(SIGNATURE)
312
+ @class_to_type ||= {
313
+ DBus::ObjectPath => DBus.type(OBJECT_PATH),
314
+ DBus::Signature => @signature_type,
315
+ DBus::SingleCompleteType => @signature_type,
316
+ String => DBus.type(STRING),
317
+ Float => DBus.type(DOUBLE)
318
+ }
319
+ t = @class_to_type[klass]
320
+ raise ArgumentError, "Cannot convert plain class #{klass} to a D-Bus type" if t.nil?
321
+
322
+ t
323
+ end
324
+ end
325
+
326
+ # Syntactic helper for constructing an array Type.
327
+ # You may be looking for {Data::Array} instead.
328
+ # @example
329
+ # t = Type::Array[Type::INT16]
330
+ class ArrayFactory < Factory
331
+ # @param member_type [Type,SingleCompleteType]
332
+ # @return [Type] (frozen)
333
+ def self.[](member_type)
334
+ t = Type.new(ARRAY)
335
+ t << make_type(member_type)
336
+ t.members.freeze
337
+ t
338
+ end
339
+ end
340
+
341
+ # @example
342
+ # t = Type::Array[Type::INT16]
343
+ Array = ArrayFactory
344
+
345
+ # Syntactic helper for constructing a hash Type.
346
+ # You may be looking for {Data::Array} and {Data::DictEntry} instead.
347
+ # @example
348
+ # t = Type::Hash[Type::STRING, Type::VARIANT]
349
+ class HashFactory < Factory
350
+ # @param key_type [Type,SingleCompleteType]
351
+ # @param value_type [Type,SingleCompleteType]
352
+ # @return [Type] (frozen)
353
+ def self.[](key_type, value_type)
354
+ t = Type.new(ARRAY)
355
+ de = Type.new(DICT_ENTRY, abstract: true)
356
+ de << make_type(key_type)
357
+ de << make_type(value_type)
358
+ de.members.freeze
359
+ t << de
360
+ t.members.freeze
361
+ t
362
+ end
363
+ end
364
+
365
+ # @example
366
+ # t = Type::Hash[Type::INT16]
367
+ Hash = HashFactory
368
+
369
+ # Syntactic helper for constructing a struct Type.
370
+ # You may be looking for {Data::Struct} instead.
371
+ # @example
372
+ # t = Type::Struct[Type::INT16, Type::STRING]
373
+ class StructFactory < Factory
374
+ # @param member_types [::Array<Type,SingleCompleteType>]
375
+ # @return [Type] (frozen)
376
+ def self.[](*member_types)
377
+ raise ArgumentError if member_types.empty?
378
+
379
+ t = Type.new(STRUCT, abstract: true)
380
+ member_types.each do |mt|
381
+ t << make_type(mt)
382
+ end
383
+ t.members.freeze
384
+ t
385
+ end
386
+ end
387
+
388
+ # @example
389
+ # t = Type::Struct[Type::INT16, Type::STRING]
390
+ Struct = StructFactory
391
+ end
177
392
 
178
393
  # shortcuts
179
394
 
180
- # Parse a String to a DBus::Type::Type
395
+ # Parse a String to a valid {DBus::Type}.
396
+ # This is prefered to {Type#initialize} which allows
397
+ # incomplete or invalid types.
398
+ # @param string_type [SingleCompleteType]
399
+ # @return [DBus::Type] (frozen)
400
+ # @raise SignatureException
181
401
  def type(string_type)
182
- Type::Parser.new(string_type).parse[0]
402
+ Type::Parser.new(string_type).parse1
183
403
  end
184
404
  module_function :type
185
405
 
406
+ # Parse a String to zero or more {DBus::Type}s.
407
+ # @param string_type [Signature]
408
+ # @return [Array<DBus::Type>] (frozen)
409
+ # @raise SignatureException
410
+ def types(string_type)
411
+ Type::Parser.new(string_type).parse
412
+ end
413
+ module_function :types
414
+
186
415
  # Make an explicit [Type, value] pair
416
+ # @param string_type [SingleCompleteType]
417
+ # @param value [::Object]
418
+ # @return [Array(DBus::Type::Type,::Object)]
419
+ # @deprecated Use {Data::Variant#initialize} instead
187
420
  def variant(string_type, value)
188
- [type(string_type), value]
421
+ Data::Variant.new(value, member_type: string_type)
189
422
  end
190
423
  module_function :variant
191
- end # module DBus
424
+ end