orient_db_client 0.0.1

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