orient_db_client 0.0.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 (45) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +4 -0
  3. data/README.md +71 -0
  4. data/Rakefile +23 -0
  5. data/lib/orient_db_client/connection.rb +141 -0
  6. data/lib/orient_db_client/database_session.rb +107 -0
  7. data/lib/orient_db_client/deserializers/deserializer7.rb +166 -0
  8. data/lib/orient_db_client/exceptions.rb +25 -0
  9. data/lib/orient_db_client/network_message.rb +42 -0
  10. data/lib/orient_db_client/protocol_factory.rb +16 -0
  11. data/lib/orient_db_client/protocols/.DS_Store +0 -0
  12. data/lib/orient_db_client/protocols/protocol7.rb +571 -0
  13. data/lib/orient_db_client/protocols/protocol9.rb +79 -0
  14. data/lib/orient_db_client/rid.rb +39 -0
  15. data/lib/orient_db_client/serializers/serializer7.rb +208 -0
  16. data/lib/orient_db_client/server_session.rb +17 -0
  17. data/lib/orient_db_client/session.rb +10 -0
  18. data/lib/orient_db_client/types.rb +22 -0
  19. data/lib/orient_db_client/version.rb +3 -0
  20. data/lib/orient_db_client.rb +18 -0
  21. data/orient_db_client.gemspec +24 -0
  22. data/test/integration/connection_test.rb +18 -0
  23. data/test/integration/database_session_9_test.rb +82 -0
  24. data/test/integration/database_session_test.rb +222 -0
  25. data/test/integration/open_database_test.rb +28 -0
  26. data/test/integration/open_server_test.rb +24 -0
  27. data/test/integration/server_session_test.rb +37 -0
  28. data/test/support/connection_helper.rb +25 -0
  29. data/test/support/create_database.sql +33 -0
  30. data/test/support/databases.yml +8 -0
  31. data/test/support/expectation_helper.rb +13 -0
  32. data/test/support/protocol_helper.rb +25 -0
  33. data/test/support/server_config.rb +6 -0
  34. data/test/support/servers-template.yml +5 -0
  35. data/test/test_helper.rb +9 -0
  36. data/test/unit/connection_test.rb +84 -0
  37. data/test/unit/deserializers/deserializer7_test.rb +141 -0
  38. data/test/unit/network_message_test.rb +24 -0
  39. data/test/unit/orient_db_client_test.rb +16 -0
  40. data/test/unit/protocol_factory_test.rb +14 -0
  41. data/test/unit/protocols/protocol7_test.rb +658 -0
  42. data/test/unit/protocols/protocol9_test.rb +149 -0
  43. data/test/unit/rid_test.rb +32 -0
  44. data/test/unit/serializers/serializer7_test.rb +72 -0
  45. metadata +167 -0
@@ -0,0 +1,571 @@
1
+ require 'orient_db_client/network_message'
2
+ require 'orient_db_client/version'
3
+ require 'orient_db_client/deserializers/deserializer7'
4
+ require 'orient_db_client/serializers/serializer7'
5
+ require 'orient_db_client/exceptions'
6
+
7
+ module OrientDbClient
8
+ module Protocols
9
+ class Protocol7
10
+
11
+ module SyncModes
12
+ SYNC = 0
13
+ ASYNC = 1
14
+ end
15
+
16
+ module Operations
17
+ CONNECT = 2
18
+ COUNT = 40
19
+ DATACLUSTER_ADD = 10
20
+ DATACLUSTER_DATARANGE = 13
21
+ DATACLUSTER_REMOVE = 11
22
+ DB_CLOSE = 5
23
+ DB_COUNTRECORDS = 9
24
+ DB_CREATE = 4
25
+ DB_DELETE = 7
26
+ DB_EXIST = 6
27
+ DB_OPEN = 3
28
+ DB_RELOAD = 73
29
+ DB_SIZE = 8
30
+ COMMAND = 41
31
+ RECORD_CREATE = 31
32
+ RECORD_DELETE = 33
33
+ RECORD_LOAD = 30
34
+ RECORD_UPDATE = 32
35
+ end
36
+
37
+ module RecordTypes
38
+ RAW = 'b'.ord
39
+ FLAT = 'f'.ord
40
+ DOCUMENT = 'd'.ord
41
+ end
42
+
43
+ module Statuses
44
+ OK = 0
45
+ ERROR = 1
46
+ end
47
+
48
+ module PayloadStatuses
49
+ NO_RECORDS = 0
50
+ RESULTSET = 1
51
+ PREFETCHED = 2
52
+ NULL = 'n'.ord
53
+ RECORD = 'r'.ord
54
+ SERIALIZED = 'a'.ord
55
+ COLLECTION = 'l'.ord
56
+ end
57
+
58
+ module VersionControl
59
+ INCREMENTAL = -1
60
+ NONE = -2
61
+ ROLLBACK = -3
62
+ end
63
+
64
+ VERSION = 7
65
+
66
+ DRIVER_NAME = 'OrientDB Ruby Client'.freeze
67
+ DRIVER_VERSION = OrientDbClient::VERSION
68
+
69
+ NEW_SESSION = -1
70
+
71
+ def self.command(socket, session, command, options = {})
72
+ options = {
73
+ :async => false, # Async mode is not supported yet
74
+ :query_class_name => 'com.orientechnologies.orient.core.sql.query.OSQLSynchQuery',
75
+ :limit => -1
76
+ }.merge(options);
77
+
78
+ serialized_command = NetworkMessage.new { |m|
79
+ m.add :string, options[:query_class_name]
80
+ m.add :string, command
81
+ m.add :integer, options[:non_text_limit] || options[:limit]
82
+ m.add :integer, 0
83
+ }.pack
84
+
85
+ socket.write NetworkMessage.new { |m|
86
+ m.add :byte, Operations::COMMAND
87
+ m.add :integer, session
88
+ m.add :byte, options[:async] ? 'a' : 's'
89
+ m.add :string, serialized_command
90
+ }.pack
91
+
92
+ read_response(socket)
93
+
94
+ { :session => read_integer(socket),
95
+ :message_content => read_command(socket) }
96
+ end
97
+
98
+ def self.connect(socket, options = {})
99
+ socket.write NetworkMessage.new { |m|
100
+ m.add :byte, Operations::CONNECT
101
+ m.add :integer, NEW_SESSION
102
+ m.add :string, DRIVER_NAME
103
+ m.add :string, DRIVER_VERSION
104
+ m.add :short, self.version
105
+ m.add :integer, 0
106
+ m.add :string, options[:user]
107
+ m.add :string, options[:password]
108
+ }.pack
109
+
110
+ read_response(socket)
111
+
112
+ { :session => read_integer(socket),
113
+ :message_content => read_connect(socket) }
114
+ end
115
+
116
+ def self.count(socket, session, cluster_name)
117
+ socket.write NetworkMessage.new { |m|
118
+ m.add :byte, Operations::COUNT
119
+ m.add :integer, session
120
+ m.add :string, cluster_name
121
+ }.pack
122
+
123
+ read_response(socket)
124
+
125
+ { :session => read_integer(socket),
126
+ :message_content => read_count(socket) }
127
+ end
128
+
129
+ def self.datacluster_add(socket, session, type, options)
130
+ socket.write NetworkMessage.new { |m|
131
+ type = type.downcase.to_sym if type.is_a?(String)
132
+ type_string = type.to_s.upcase
133
+
134
+ m.add :byte, Operations::DATACLUSTER_ADD
135
+ m.add :integer, session
136
+ m.add :string, type_string
137
+
138
+ case type
139
+ when :physical
140
+ m.add :string, options[:name]
141
+ m.add :string, options[:file_name]
142
+ m.add :integer, options[:initial_size] || -1
143
+ when :logical
144
+ m.add :integer, options[:physical_cluster_container_id]
145
+ when :memory
146
+ m.add :string, options[:name]
147
+ end
148
+ }.pack
149
+
150
+ read_response(socket)
151
+
152
+ { :session => read_integer(socket),
153
+ :message_content => read_datacluster_add(socket) }
154
+ end
155
+
156
+ def self.datacluster_datarange(socket, session, cluster_id)
157
+ socket.write NetworkMessage.new { |m|
158
+ m.add :byte, Operations::DATACLUSTER_DATARANGE
159
+ m.add :integer, session
160
+ m.add :short, cluster_id
161
+ }.pack
162
+
163
+ read_response(socket)
164
+
165
+ { :session => read_integer(socket),
166
+ :message_content => read_datacluster_datarange(socket) }
167
+ end
168
+
169
+ def self.datacluster_remove(socket, session, cluster_id)
170
+ socket.write NetworkMessage.new { |m|
171
+ m.add :byte, Operations::DATACLUSTER_REMOVE
172
+ m.add :integer, session
173
+ m.add :short, cluster_id
174
+ }.pack
175
+
176
+ read_response(socket)
177
+
178
+ { :session => read_integer(socket),
179
+ :message_content => read_datacluster_remove(socket) }
180
+ end
181
+
182
+ def self.db_close(socket, session = NEW_SESSION)
183
+ socket.write NetworkMessage.new { |m|
184
+ m.add :byte, Operations::DB_CLOSE
185
+ m.add :integer, session
186
+ }.pack
187
+
188
+ return socket.closed?
189
+ end
190
+
191
+ def self.db_countrecords(socket, session)
192
+ socket.write NetworkMessage.new { |m|
193
+ m.add :byte, Operations::DB_COUNTRECORDS
194
+ m.add :integer, session
195
+ }.pack
196
+
197
+ read_response(socket)
198
+
199
+ { :session => read_integer(socket),
200
+ :message_content => read_db_countrecords(socket) }
201
+ end
202
+
203
+ def self.db_create(socket, session, database, options = {})
204
+ if options.is_a?(String)
205
+ options = { :storage_type => options }
206
+ end
207
+
208
+ options = { :storage_type => 'local' }.merge(options)
209
+
210
+ socket.write NetworkMessage.new { |m|
211
+ m.add :byte, Operations::DB_CREATE
212
+ m.add :integer, session
213
+ m.add :string, database
214
+ m.add :string, options[:storage_type]
215
+ }.pack
216
+
217
+ read_response(socket)
218
+
219
+ { :session => read_integer(socket) }
220
+ end
221
+
222
+ def self.db_delete(socket, session, database)
223
+ socket.write NetworkMessage.new { |m|
224
+ m.add :byte, Operations::DB_DELETE
225
+ m.add :integer, session
226
+ m.add :string, database
227
+ }.pack
228
+
229
+ read_response(socket)
230
+
231
+ { :session => read_integer(socket) }
232
+ end
233
+
234
+ def self.db_exist(socket, session, database)
235
+ socket.write NetworkMessage.new { |m|
236
+ m.add :byte, Operations::DB_EXIST
237
+ m.add :integer, session
238
+ m.add :string, database
239
+ }.pack
240
+
241
+ read_response(socket)
242
+
243
+ { :session => read_integer(socket),
244
+ :message_content => read_db_exist(socket) }
245
+ end
246
+
247
+ def self.db_open(socket, database, options = {})
248
+ socket.write NetworkMessage.new { |m|
249
+ m.add :byte, Operations::DB_OPEN
250
+ m.add :integer, NEW_SESSION
251
+ m.add :string, DRIVER_NAME
252
+ m.add :string, DRIVER_VERSION
253
+ m.add :short, self.version
254
+ m.add :integer, 0
255
+ m.add :string, database
256
+ m.add :string, options[:user]
257
+ m.add :string, options[:password]
258
+ }.pack
259
+
260
+ read_response(socket)
261
+
262
+ { :session => read_integer(socket),
263
+ :message_content => read_db_open(socket) }
264
+ end
265
+
266
+ def self.db_reload(socket, session)
267
+ socket.write NetworkMessage.new { |m|
268
+ m.add :byte, Operations::DB_RELOAD
269
+ m.add :integer, session
270
+ }.pack
271
+
272
+ read_response(socket)
273
+
274
+ { :session => read_integer(socket),
275
+ :message_content => read_db_reload(socket) }
276
+ end
277
+
278
+ def self.db_size(socket, session)
279
+ socket.write NetworkMessage.new { |m|
280
+ m.add :byte, Operations::DB_SIZE
281
+ m.add :integer, session
282
+ }.pack
283
+
284
+ read_response(socket)
285
+
286
+ { :session => read_integer(socket),
287
+ :message_content => read_db_size(socket) }
288
+ end
289
+
290
+ def self.record_create(socket, session, cluster_id, record)
291
+ socket.write NetworkMessage.new { |m|
292
+ m.add :byte, Operations::RECORD_CREATE
293
+ m.add :integer, session
294
+ m.add :short, cluster_id
295
+ m.add :string, serializer.serialize(record)
296
+ m.add :byte, RecordTypes::DOCUMENT
297
+ m.add :byte, SyncModes::SYNC
298
+ }.pack
299
+
300
+ read_response(socket)
301
+
302
+ { :session => read_integer(socket),
303
+ :message_content => read_record_create(socket).merge({ :cluster_id => cluster_id }) }
304
+ end
305
+
306
+ def self.record_delete(socket, session, cluster_id, cluster_position, version)
307
+ socket.write NetworkMessage.new { |m|
308
+ m.add :byte, Operations::RECORD_DELETE
309
+ m.add :integer, session
310
+ m.add :short, cluster_id
311
+ m.add :long, cluster_position
312
+ m.add :integer, version
313
+ m.add :byte, SyncModes::SYNC
314
+ }.pack
315
+
316
+ read_response(socket)
317
+
318
+ { :session => read_integer(socket),
319
+ :message_content => read_record_delete(socket) }
320
+ end
321
+
322
+ def self.record_load(socket, session, rid)
323
+ socket.write NetworkMessage.new { |m|
324
+ m.add :byte, Operations::RECORD_LOAD
325
+ m.add :integer, session
326
+ m.add :short, rid.cluster_id
327
+ m.add :long, rid.cluster_position
328
+ m.add :string, ""
329
+ }.pack
330
+
331
+ read_response(socket)
332
+
333
+ { :session => read_integer(socket),
334
+ :message_content => read_record_load(socket) }
335
+ end
336
+
337
+ def self.record_update(socket, session, cluster_id, cluster_position, record, version = VersionControl::NONE)
338
+ if version.is_a?(Symbol)
339
+ version = case version
340
+ when :none then VersionControl::NONE
341
+ when :incremental then VersionControl::INCREMENTAL
342
+ else VersionControl::NONE
343
+ end
344
+ end
345
+
346
+ socket.write NetworkMessage.new { |m|
347
+ m.add :byte, Operations::RECORD_UPDATE
348
+ m.add :integer, session
349
+ m.add :short, cluster_id
350
+ m.add :long, cluster_position
351
+ m.add :string, serializer.serialize(record)
352
+ m.add :integer, version
353
+ m.add :byte, RecordTypes::DOCUMENT
354
+ m.add :byte, SyncModes::SYNC
355
+ }.pack
356
+
357
+ read_response(socket)
358
+
359
+ { :session => read_integer(socket),
360
+ :message_content => read_record_update(socket) }
361
+ end
362
+
363
+ def self.deserializer
364
+ return OrientDbClient::Deserializers::Deserializer7.new
365
+ end
366
+
367
+ def self.serializer
368
+ return OrientDbClient::Serializers::Serializer7.new
369
+ end
370
+
371
+ def self.version
372
+ self::VERSION
373
+ end
374
+
375
+ private
376
+
377
+ def self.read_byte(socket)
378
+ socket.read(1).unpack('C').first
379
+ end
380
+
381
+ def self.read_count(socket)
382
+ { :record_count => read_long(socket) }
383
+ end
384
+
385
+ def self.read_clusters(socket)
386
+ clusters = []
387
+
388
+ read_short(socket).times do
389
+ clusters << {
390
+ :name => read_string(socket),
391
+ :id => read_short(socket),
392
+ :type => read_string(socket)
393
+ }
394
+ end
395
+
396
+ clusters
397
+ end
398
+
399
+ def self.read_collection_record(socket)
400
+ record = { :format => read_short(socket) }
401
+
402
+ case record[:format]
403
+ when 0
404
+ record.merge!({
405
+ :record_type => read_byte(socket),
406
+ :cluster_id => read_short(socket),
407
+ :cluster_position => read_long(socket),
408
+ :record_version => read_integer(socket),
409
+ :bytes => read_string(socket) })
410
+ else
411
+ throw "Unsupported record format: #{record[:format]}"
412
+ end
413
+ end
414
+
415
+ def self.read_command(socket)
416
+ result = []
417
+
418
+ while (status = read_byte(socket)) != PayloadStatuses::NO_RECORDS
419
+ case status
420
+ when PayloadStatuses::NULL
421
+ result.push(nil)
422
+ when PayloadStatuses::COLLECTION
423
+ collection = read_record_collection(socket)
424
+ result.concat collection
425
+ break
426
+ else
427
+ throw "Unsupported payload status: #{status}"
428
+ end
429
+ end
430
+
431
+ result
432
+ end
433
+
434
+ def self.read_connect(socket)
435
+ { :session => read_integer(socket) }
436
+ end
437
+
438
+ def self.read_datacluster_add(socket)
439
+ { :new_cluster_number => read_short(socket) }
440
+ end
441
+
442
+ def self.read_datacluster_datarange(socket)
443
+ { :begin => read_long(socket),
444
+ :end => read_long(socket) }
445
+ end
446
+
447
+ def self.read_datacluster_remove(socket)
448
+ { :result => read_byte(socket) }
449
+ end
450
+
451
+ def self.read_db_countrecords(socket)
452
+ { :count => read_long(socket) }
453
+ end
454
+
455
+ def self.read_db_exist(socket)
456
+ { :result => read_byte(socket) }
457
+ end
458
+
459
+ def self.read_db_open(socket)
460
+ { :session => read_integer(socket),
461
+ :clusters => read_clusters(socket),
462
+ :cluster_config => read_string(socket) }
463
+ end
464
+
465
+ def self.read_db_reload(socket)
466
+ { :clusters => read_clusters(socket) }
467
+ end
468
+
469
+ def self.read_db_size(socket)
470
+ { :size => read_long(socket) }
471
+ end
472
+
473
+ def self.read_integer(socket)
474
+ socket.read(4).unpack('l>').first
475
+ end
476
+
477
+ def self.read_long(socket)
478
+ socket.read(8).unpack('q>').first
479
+ end
480
+
481
+ def self.read_record(socket)
482
+ { :bytes => read_string(socket),
483
+ :record_version => read_integer(socket),
484
+ :record_type => read_byte(socket) }
485
+ end
486
+
487
+ def self.read_record_collection(socket)
488
+ count = read_integer(socket)
489
+ records = []
490
+
491
+ count.times do
492
+ record = read_collection_record(socket)
493
+ record[:document] = deserializer.deserialize(record[:bytes])[:document]
494
+ record.delete(:bytes)
495
+ records << record
496
+ end
497
+
498
+ records
499
+ end
500
+
501
+ def self.read_record_create(socket)
502
+ { :cluster_position => read_long(socket) }
503
+ end
504
+
505
+ def self.read_record_delete(socket)
506
+ { :result => read_byte(socket) }
507
+ end
508
+
509
+ def self.read_record_load(socket)
510
+ result = nil
511
+
512
+ while (status = read_byte(socket)) != PayloadStatuses::NO_RECORDS
513
+ case status
514
+ when PayloadStatuses::RESULTSET
515
+ record = record || read_record(socket)
516
+
517
+ case record[:record_type]
518
+ when 'd'.ord
519
+ result = result || record
520
+ result[:document] = deserializer.deserialize(record[:bytes])[:document]
521
+ else
522
+ throw "Unsupported record type: #{record[:record_type]}"
523
+ end
524
+ else
525
+ throw "Unsupported payload status: #{status}"
526
+ end
527
+ end
528
+
529
+ result
530
+ end
531
+
532
+ def self.read_record_update(socket)
533
+ { :record_version => read_integer(socket) }
534
+ end
535
+
536
+ def self.read_response(socket)
537
+ result = read_byte(socket)
538
+
539
+ raise_response_error(socket) unless result == Statuses::OK
540
+ end
541
+
542
+ def self.raise_response_error(socket)
543
+ session = read_integer(socket)
544
+ exceptions = []
545
+
546
+ while (result = read_byte(socket)) == Statuses::ERROR
547
+ exceptions << {
548
+ :exception_class => read_string(socket),
549
+ :exception_message => read_string(socket)
550
+ }
551
+ end
552
+
553
+ if exceptions[0] && exceptions[0][:exception_class] == "com.orientechnologies.orient.core.exception.ORecordNotFoundException"
554
+ raise RecordNotFound.new(session)
555
+ else
556
+ raise ProtocolError.new(session, *exceptions)
557
+ end
558
+ end
559
+
560
+ def self.read_short(socket)
561
+ socket.read(2).unpack('s>').first
562
+ end
563
+
564
+ def self.read_string(socket)
565
+ length = read_integer(socket)
566
+
567
+ length > 0 ? socket.read(length) : nil
568
+ end
569
+ end
570
+ end
571
+ end
@@ -0,0 +1,79 @@
1
+ require 'orient_db_client/network_message'
2
+ require 'orient_db_client/version'
3
+
4
+ module OrientDbClient
5
+ module Protocols
6
+ class Protocol9 < Protocol7
7
+ VERSION = 9
8
+
9
+ def self.command(socket, session, command, options = {})
10
+ options[:query_class_name].tap do |qcn|
11
+ if qcn.nil? || qcn == 'com.orientechnologies.orient.core.sql.query.OSQLSynchQuery'
12
+ options[:query_class_name] = 'q'
13
+ end
14
+ end
15
+
16
+ super socket, session, command, options
17
+ end
18
+
19
+ def self.db_create(socket, session, database, options = {})
20
+ if options.is_a?(String)
21
+ options = { :storage_type => options }
22
+ end
23
+
24
+ options = {
25
+ :database_type => 'document',
26
+ :storage_type => 'local'
27
+ }.merge(options)
28
+
29
+ socket.write NetworkMessage.new { |m|
30
+ m.add :byte, Operations::DB_CREATE
31
+ m.add :integer, session
32
+ m.add :string, database
33
+ m.add :string, options[:database_type]
34
+ m.add :string, options[:storage_type]
35
+ }.pack
36
+
37
+ read_response(socket)
38
+
39
+ { :session => read_integer(socket) }
40
+ end
41
+
42
+ def self.db_open(socket, database, options = {})
43
+ socket.write NetworkMessage.new { |m|
44
+ m.add :byte, Operations::DB_OPEN
45
+ m.add :integer, NEW_SESSION
46
+ m.add :string, DRIVER_NAME
47
+ m.add :string, DRIVER_VERSION
48
+ m.add :short, self.version
49
+ m.add :integer, 0
50
+ m.add :string, database
51
+ m.add :string, options[:database_type] || "document"
52
+ m.add :string, options[:user]
53
+ m.add :string, options[:password]
54
+ }.pack
55
+
56
+ read_response(socket)
57
+
58
+ { :session => read_integer(socket),
59
+ :message_content => read_db_open(socket) }
60
+ end
61
+
62
+ def self.record_load(socket, session, rid, options = {})
63
+ socket.write NetworkMessage.new { |m|
64
+ m.add :byte, Operations::RECORD_LOAD
65
+ m.add :integer, session
66
+ m.add :short, rid.cluster_id
67
+ m.add :long, rid.cluster_position
68
+ m.add :string, ""
69
+ m.add :byte, options[:ignore_cache] === true ? 1 : 0
70
+ }.pack
71
+
72
+ read_response(socket)
73
+
74
+ { :session => read_integer(socket),
75
+ :message_content => read_record_load(socket) }
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,39 @@
1
+ module OrientDbClient
2
+ class Rid
3
+ attr_reader :cluster_id
4
+ attr_reader :cluster_position
5
+
6
+ def initialize(cluster_id_or_rid = nil, cluster_position = nil)
7
+ if cluster_id_or_rid.is_a?(String) && cluster_position.nil?
8
+ rid = cluster_id_or_rid
9
+
10
+ rid = rid[1..rid.length] if rid[0] == '#'
11
+
12
+ @cluster_id, @cluster_position = rid.split(":")
13
+ elsif cluster_id_or_rid.is_a?(OrientDbClient::Rid)
14
+ rid = cluster_id_or_rid
15
+
16
+ @cluster_id = rid.cluster_id
17
+ @cluster_position = rid.cluster_position
18
+ else
19
+ @cluster_id = cluster_id_or_rid.nil? ? nil : cluster_id_or_rid
20
+ @cluster_position = cluster_position.nil? ? nil : cluster_position
21
+ end
22
+
23
+ @cluster_id = @cluster_id.to_i unless @cluster_id.nil?
24
+ @cluster_position = @cluster_position.to_i unless @cluster_position.nil?
25
+ end
26
+
27
+ def nil?
28
+ @cluster_id.nil? || @cluster_position.nil?
29
+ end
30
+
31
+ def to_s
32
+ if self.nil?
33
+ '#'
34
+ else
35
+ "##{@cluster_id}:#{@cluster_position}"
36
+ end
37
+ end
38
+ end
39
+ end