strokedb 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/CONTRIBUTORS +7 -0
  2. data/CREDITS +13 -0
  3. data/README +44 -0
  4. data/bin/sdbc +2 -0
  5. data/lib/config/config.rb +161 -0
  6. data/lib/data_structures/inverted_list.rb +297 -0
  7. data/lib/data_structures/point_query.rb +24 -0
  8. data/lib/data_structures/skiplist.rb +302 -0
  9. data/lib/document/associations.rb +107 -0
  10. data/lib/document/callback.rb +11 -0
  11. data/lib/document/coercions.rb +57 -0
  12. data/lib/document/delete.rb +28 -0
  13. data/lib/document/document.rb +684 -0
  14. data/lib/document/meta.rb +261 -0
  15. data/lib/document/slot.rb +199 -0
  16. data/lib/document/util.rb +27 -0
  17. data/lib/document/validations.rb +704 -0
  18. data/lib/document/versions.rb +106 -0
  19. data/lib/document/virtualize.rb +82 -0
  20. data/lib/init.rb +57 -0
  21. data/lib/stores/chainable_storage.rb +57 -0
  22. data/lib/stores/inverted_list_index/inverted_list_file_storage.rb +56 -0
  23. data/lib/stores/inverted_list_index/inverted_list_index.rb +49 -0
  24. data/lib/stores/remote_store.rb +172 -0
  25. data/lib/stores/skiplist_store/chunk.rb +119 -0
  26. data/lib/stores/skiplist_store/chunk_storage.rb +21 -0
  27. data/lib/stores/skiplist_store/file_chunk_storage.rb +44 -0
  28. data/lib/stores/skiplist_store/memory_chunk_storage.rb +37 -0
  29. data/lib/stores/skiplist_store/skiplist_store.rb +217 -0
  30. data/lib/stores/store.rb +5 -0
  31. data/lib/sync/chain_sync.rb +38 -0
  32. data/lib/sync/diff.rb +126 -0
  33. data/lib/sync/lamport_timestamp.rb +81 -0
  34. data/lib/sync/store_sync.rb +79 -0
  35. data/lib/sync/stroke_diff/array.rb +102 -0
  36. data/lib/sync/stroke_diff/default.rb +21 -0
  37. data/lib/sync/stroke_diff/hash.rb +186 -0
  38. data/lib/sync/stroke_diff/string.rb +116 -0
  39. data/lib/sync/stroke_diff/stroke_diff.rb +9 -0
  40. data/lib/util/blankslate.rb +42 -0
  41. data/lib/util/ext/blank.rb +50 -0
  42. data/lib/util/ext/enumerable.rb +36 -0
  43. data/lib/util/ext/fixnum.rb +16 -0
  44. data/lib/util/ext/hash.rb +22 -0
  45. data/lib/util/ext/object.rb +8 -0
  46. data/lib/util/ext/string.rb +35 -0
  47. data/lib/util/inflect.rb +217 -0
  48. data/lib/util/java_util.rb +9 -0
  49. data/lib/util/lazy_array.rb +54 -0
  50. data/lib/util/lazy_mapping_array.rb +64 -0
  51. data/lib/util/lazy_mapping_hash.rb +46 -0
  52. data/lib/util/serialization.rb +29 -0
  53. data/lib/util/trigger_partition.rb +136 -0
  54. data/lib/util/util.rb +38 -0
  55. data/lib/util/xml.rb +6 -0
  56. data/lib/view/view.rb +55 -0
  57. data/script/console +70 -0
  58. data/strokedb.rb +75 -0
  59. metadata +148 -0
@@ -0,0 +1,684 @@
1
+ module StrokeDB
2
+ # Slots which contain references to another documents are matched
3
+ # with these regexps.
4
+ DOCREF = /^@##{UUID_RE}$/
5
+ VERSIONREF = /^@##{UUID_RE}\.#{VERSION_RE}$/
6
+
7
+ #
8
+ # Raised on unexisting document access.
9
+ #
10
+ # Example:
11
+ #
12
+ # document.slot_that_does_not_exist_ever
13
+ #
14
+ class SlotNotFoundError < StandardError
15
+ attr_reader :slotname
16
+
17
+ def initialize(slotname)
18
+ @slotname = slotname
19
+ end
20
+
21
+ def message
22
+ "Can't find slot #{@slotname}"
23
+ end
24
+
25
+ def inspect
26
+ "#<#{self.class.name}: #{message}>"
27
+ end
28
+ end
29
+
30
+ #
31
+ # Raised when Document#save! is called on an invalid document
32
+ # (for which doc.valid? returns false)
33
+ #
34
+ class InvalidDocumentError < StandardError #:nodoc:
35
+ attr_reader :document
36
+
37
+ def initialize(document)
38
+ @document = document
39
+ end
40
+
41
+ def message
42
+ "Validation failed: #{@document.errors.messages.join(", ")}"
43
+ end
44
+
45
+ def inspect
46
+ "#<#{self.class.name}: #{message}>"
47
+ end
48
+ end
49
+
50
+ # Document is one of the core classes. It is being used to represent database document.
51
+ #
52
+ # Database document is an entity that:
53
+ #
54
+ # * is uniquely identified with UUID
55
+ # * has a number of slots, where each slot is a key-value pair (whereas pair could be a JSON object)
56
+ #
57
+ # Here is a simplistic example of document:
58
+ #
59
+ # <tt>1e3d02cc-0769-4bd8-9113-e033b246b013:</tt>
60
+ # name: "My Document"
61
+ # language: "English"
62
+ # authors: ["Yurii Rashkovskii","Oleg Andreev"]
63
+ #
64
+ class Document
65
+ include Validations::InstanceMethods
66
+
67
+ attr_reader :store, :callbacks #:nodoc:
68
+
69
+ def marshal_dump #:nodoc:
70
+ (@new ? '1' : '0') + (@saved ? '1' : '0') + to_raw.to_json
71
+ end
72
+
73
+ def marshal_load(content) #:nodoc:
74
+ @callbacks = {}
75
+ initialize_raw_slots(JSON.parse(content[2,content.length]))
76
+ @saved = content[1,1] == '1'
77
+ @new = content[0,1] == '1'
78
+ end
79
+
80
+ # Collection of meta documents
81
+ class Metas < Array #:nodoc:
82
+ def initialize(document)
83
+ @document = document
84
+ _meta = document[:meta]
85
+ concat [_meta].flatten.compact.map{|v| v.is_a?(DocumentReferenceValue) ? v.load : v}
86
+ end
87
+
88
+ def <<(meta)
89
+ add_meta(meta, :call_initialization_callbacks => true)
90
+ end
91
+
92
+ def add_meta(meta, opts = {})
93
+ opts = opts.stringify_keys
94
+ _module = nil
95
+
96
+ # meta can be specified both as a meta document and as a module
97
+ case meta
98
+ when Document
99
+ push meta
100
+ _module = StrokeDB::Document.collect_meta_modules(@document.store, meta).first
101
+ when Meta
102
+ push meta.document(@document.store)
103
+ _module = meta
104
+ else
105
+ raise ArgumentError, "Meta should be either document or meta module"
106
+ end
107
+
108
+ # register meta in the document
109
+ @document[:meta] = self
110
+
111
+ if _module
112
+ @document.extend(_module)
113
+
114
+ _module.send!(:setup_callbacks, @document) rescue nil
115
+
116
+ if opts['call_initialization_callbacks']
117
+ @document.send!(:execute_callbacks_for, _module, :on_initialization)
118
+ @document.send!(:execute_callbacks_for, _module, :on_new_document) if @document.new?
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ #
125
+ # Instantiates new document with given arguments (which are the same as in Document#new),
126
+ # and saves it right away
127
+ #
128
+ def self.create!(*args, &block)
129
+ new(*args, &block).save!
130
+ end
131
+
132
+ #
133
+ # Instantiates new document
134
+ #
135
+ # Here are few ways to call it:
136
+ #
137
+ # Document.new(:slot_1 => slot_1_value, :slot_2 => slot_2_value)
138
+ #
139
+ # This way new document with slots <tt>slot_1</tt> and <tt>slot_2</tt> will be initialized in the
140
+ # default store.
141
+ #
142
+ # Document.new(store,:slot_1 => slot_1_value, :slot_2 => slot_2_value)
143
+ #
144
+ # This way new document with slots <tt>slot_1</tt> and <tt>slot_2</tt> will be initialized in the
145
+ # given <tt>store</tt>.
146
+ #
147
+ # Document.new({:slot_1 => slot_1_value, :slot_2 => slot_2_value},uuid)
148
+ #
149
+ # where <tt>uuid</tt> is a string with UUID. *WARNING*: this way of initializing Document should not
150
+ # be used unless you know what are you doing!
151
+ #
152
+ def initialize(*args, &block)
153
+ @initialization_block = block
154
+
155
+ if args.first.is_a?(Hash) || args.empty?
156
+ raise NoDefaultStoreError unless StrokeDB.default_store
157
+ do_initialize(StrokeDB.default_store, *args)
158
+ else
159
+ do_initialize(*args)
160
+ end
161
+ end
162
+
163
+ #
164
+ # Get slot value by its name:
165
+ #
166
+ # document[:slot_1]
167
+ #
168
+ # If slot was not found, it will return <tt>nil</tt>
169
+ #
170
+ def [](slotname)
171
+ @slots[slotname.to_s].value rescue nil
172
+ end
173
+
174
+ #
175
+ # Set slot value by its name:
176
+ #
177
+ # document[:slot_1] = "some value"
178
+ #
179
+ def []=(slotname, value)
180
+ slotname = slotname.to_s
181
+
182
+ (@slots[slotname] ||= Slot.new(self, slotname)).value = value
183
+ update_version!(slotname)
184
+
185
+ value
186
+ end
187
+
188
+ #
189
+ # Checks slot presence. Unlike Document#slotnames it allows you to find even 'virtual slots' that could be
190
+ # computed runtime by associations or <tt>when_slot_found</tt> callbacks
191
+ #
192
+ # document.has_slot?(:slotname)
193
+ #
194
+ def has_slot?(slotname)
195
+ v = send(slotname)
196
+
197
+ (v.nil? && slotnames.include?(slotname.to_s)) ? true : !!v
198
+ rescue SlotNotFoundError
199
+ false
200
+ end
201
+
202
+ #
203
+ # Removes slot
204
+ #
205
+ # document.remove_slot!(:slotname)
206
+ #
207
+ def remove_slot!(slotname)
208
+ slotname = slotname.to_s
209
+
210
+ @slots.delete slotname
211
+ update_version! slotname
212
+
213
+ nil
214
+ end
215
+
216
+ #
217
+ # Returns an <tt>Array</tt> of explicitely defined slots
218
+ #
219
+ # document.slotnames #=> ["version","name","language","authors"]
220
+ #
221
+ def slotnames
222
+ @slots.keys
223
+ end
224
+
225
+ #
226
+ # Creates Diff document from <tt>from</tt> document to this document
227
+ #
228
+ # document.diff(original_document) #=> #<StrokeDB::Diff added_slots: {"b"=>2}, from: #<Doc a: 1>, removed_slots: {"a"=>1}, to: #<Doc b: 2>, updated_slots: {}>
229
+ #
230
+ def diff(from)
231
+ Diff.new(store, :from => from, :to => self)
232
+ end
233
+
234
+ def pretty_print #:nodoc:
235
+ slots = to_raw.except('meta')
236
+
237
+ s = is_a?(ImmutableDocument) ? "#<(imm)" : "#<"
238
+
239
+ Util.catch_circular_reference(self) do
240
+ if self[:meta] && name = meta[:name]
241
+ s << "#{name} "
242
+ else
243
+ s << "Doc "
244
+ end
245
+
246
+ slots.keys.sort.each do |k|
247
+ if %w(version previous_version).member?(k) && v = self[k]
248
+ s << "#{k}: #{v.gsub(/^(0)+/,'')[0,4]}..., "
249
+ else
250
+ s << "#{k}: #{self[k].inspect}, "
251
+ end
252
+ end
253
+
254
+ s.chomp!(', ')
255
+ s.chomp!(' ')
256
+ s << ">"
257
+ end
258
+
259
+ s
260
+ rescue Util::CircularReferenceCondition
261
+ "#(#{(self[:meta] ? "#{meta}" : "Doc")} #{('@#'+uuid)[0,5]}...)"
262
+ end
263
+
264
+ alias :to_s :pretty_print
265
+ alias :inspect :pretty_print
266
+
267
+ #
268
+ # Returns string with Document's JSON representation
269
+ #
270
+ def to_json
271
+ to_raw.to_json
272
+ end
273
+
274
+ #
275
+ # Returns string with Document's XML representation
276
+ #
277
+ def to_xml(opts = {})
278
+ to_raw.to_xml({ :root => 'document', :dasherize => true }.merge(opts))
279
+ end
280
+
281
+ #
282
+ # Primary serialization
283
+ #
284
+ def to_raw #:nodoc:
285
+ raw_slots = {}
286
+
287
+ @slots.each_pair do |k,v|
288
+ raw_slots[k.to_s] = v.to_raw
289
+ end
290
+
291
+ raw_slots
292
+ end
293
+
294
+ def to_optimized_raw #:nodoc:
295
+ __reference__
296
+ end
297
+
298
+ #
299
+ # Creates a document from a serialized representation
300
+ #
301
+ def self.from_raw(store, raw_slots, opts = {}) #:nodoc:
302
+ doc = new(store, raw_slots, true)
303
+
304
+ collect_meta_modules(store, raw_slots['meta']).each do |meta_module|
305
+ unless doc.is_a? meta_module
306
+ doc.extend(meta_module)
307
+
308
+ meta_module.send!(:setup_callbacks, doc) rescue nil
309
+ end
310
+ end
311
+
312
+ unless opts[:skip_callbacks]
313
+ doc.send! :execute_callbacks, :on_initialization
314
+ doc.send! :execute_callbacks, :on_load
315
+ end
316
+ doc
317
+ end
318
+
319
+ #
320
+ # Find document(s) by:
321
+ #
322
+ # a) UUID
323
+ #
324
+ # Document.find(uuid)
325
+ #
326
+ # b) search query
327
+ #
328
+ # Document.find(:slot => "value")
329
+ #
330
+ # If first argument is Store, that particular store will be used; otherwise default store will be assumed.
331
+ def self.find(*args)
332
+ store = nil
333
+ if args.empty? || args.first.is_a?(String) || args.first.is_a?(Hash)
334
+ store = StrokeDB.default_store
335
+ else
336
+ store = args.shift
337
+ end
338
+ raise NoDefaultStoreError.new unless store
339
+ query = args.first
340
+ case query
341
+ when UUID_RE
342
+ store.find(query)
343
+ when Hash
344
+ store.search(query)
345
+ else
346
+ raise TypeError
347
+ end
348
+ end
349
+
350
+ #
351
+ # Reloads head of the same document from store. All unsaved changes will be lost!
352
+ #
353
+ def reload
354
+ new? ? self : store.find(uuid)
355
+ end
356
+
357
+ #
358
+ # Returns <tt>true</tt> if this is a document that has never been saved.
359
+ #
360
+ def new?
361
+ !!@new
362
+ end
363
+
364
+ #
365
+ # Returns <tt>true</tt> if this document is a latest version of document being saved to a respective
366
+ # store
367
+ #
368
+ def head?
369
+ return false if new? || is_a?(VersionedDocument)
370
+ store.head_version(uuid) == version
371
+ end
372
+
373
+ #
374
+ # Saves the document. If validations do not pass, InvalidDocumentError
375
+ # exception is raised.
376
+ #
377
+ def save!(perform_validation = true)
378
+ execute_callbacks :before_save
379
+
380
+ if perform_validation
381
+ raise InvalidDocumentError.new(self) unless valid?
382
+ end
383
+
384
+ execute_callbacks :after_validation
385
+
386
+ store.save!(self)
387
+ @new = false
388
+ @saved = true
389
+
390
+ execute_callbacks :after_save
391
+
392
+ self
393
+ end
394
+
395
+ #
396
+ # Updates slots with specified <tt>hash</tt> and returns itself.
397
+ #
398
+ def update_slots(hash)
399
+ hash.each do |k, v|
400
+ self[k] = v
401
+ end
402
+
403
+ self
404
+ end
405
+
406
+ #
407
+ # Same as update_slots, but also saves the document.
408
+ #
409
+ def update_slots!(hash)
410
+ update_slots(hash).save!
411
+ end
412
+
413
+ #
414
+ # Returns document's metadocument (if any). In case if document has more than one metadocument,
415
+ # it will combine all metadocuments into one 'virtual' metadocument
416
+ #
417
+ def meta
418
+ unless (m = self[:meta]).kind_of? Array
419
+ # simple case
420
+ return m || Document.new(@store)
421
+ end
422
+
423
+ return m.first if m.size == 1
424
+
425
+ mm = m.clone
426
+ collected_meta = mm.shift.clone
427
+
428
+ names = collected_meta[:name].split(',') rescue []
429
+
430
+ mm.each do |next_meta|
431
+ next_meta = next_meta.clone
432
+ collected_meta += next_meta
433
+ names << next_meta.name if next_meta[:name]
434
+ end
435
+
436
+ collected_meta.name = names.uniq.join(',')
437
+ collected_meta.make_immutable!
438
+ end
439
+
440
+ #
441
+ # Instantiate a composite document
442
+ #
443
+ def +(document)
444
+ original, target = [to_raw, document.to_raw].map{ |raw| raw.except(*%w(uuid version previous_version)) }
445
+
446
+ Document.new(@store, original.merge(target).merge(:uuid => Util.random_uuid), true)
447
+ end
448
+
449
+ #
450
+ # Should be used to add metadocuments on the fly:
451
+ #
452
+ # document.metas << Buyer
453
+ # document.metas << Buyer.document
454
+ #
455
+ # Please not that it accept both meta modules and their documents, there is no difference
456
+ #
457
+ def metas
458
+ Metas.new(self)
459
+ end
460
+
461
+ #
462
+ # Returns document's version (which is stored in <tt>version</tt> slot)
463
+ #
464
+ def version
465
+ self[:version]
466
+ end
467
+
468
+ #
469
+ # Return document's uuid
470
+ #
471
+ def uuid
472
+ @uuid ||= self[:uuid]
473
+ end
474
+
475
+ #
476
+ # Returns document's previous version (which is stored in <tt>previous_version</tt> slot)
477
+ #
478
+ def previous_version
479
+ self[:previous_version]
480
+ end
481
+
482
+ def version=(v) #:nodoc:
483
+ self[:version] = v
484
+ end
485
+
486
+ #
487
+ # Returns an instance of Document::Versions
488
+ #
489
+ def versions
490
+ @versions ||= Versions.new(self)
491
+ end
492
+
493
+ def __reference__ #:nodoc:
494
+ "@##{uuid}.#{version}"
495
+ end
496
+
497
+ def ==(doc) #:nodoc:
498
+ case doc
499
+ when Document, DocumentReferenceValue
500
+ doc = doc.load if doc.kind_of? DocumentReferenceValue
501
+
502
+ # we make a quick UUID check here to skip two heavy to_raw calls
503
+ doc.uuid == uuid && doc.to_raw == to_raw
504
+ else
505
+ false
506
+ end
507
+ end
508
+
509
+ def eql?(doc) #:nodoc:
510
+ self == doc
511
+ end
512
+
513
+ # documents are hashed by their UUID
514
+ def hash #:nodoc:
515
+ uuid.hash
516
+ end
517
+
518
+ def make_immutable!
519
+ extend ImmutableDocument
520
+ self
521
+ end
522
+
523
+ def mutable?
524
+ true
525
+ end
526
+
527
+ def method_missing(sym, *args) #:nodoc:
528
+ sym = sym.to_s
529
+
530
+ return send(:[]=, sym.chomp('='), *args) if sym.ends_with? '='
531
+ return self[sym] if slotnames.include? sym
532
+ return !!send(sym.chomp('?'), *args) if sym.ends_with? '?'
533
+
534
+ raise SlotNotFoundError.new(sym) if (callbacks['when_slot_not_found'] || []).empty?
535
+
536
+ r = execute_callbacks(:when_slot_not_found, sym)
537
+ raise r if r.is_a? SlotNotFoundError # TODO: spec this behavior
538
+
539
+ r
540
+ end
541
+
542
+ def add_callback(cbk) #:nodoc:
543
+ name, uid = cbk.name, cbk.uid
544
+
545
+ callbacks[name] ||= []
546
+
547
+ # if uid is specified, previous callback with the same uid is deleted
548
+ if uid && old_cb = callbacks[name].find{ |cb| cb.uid == uid }
549
+ callbacks[name].delete old_cb
550
+ end
551
+
552
+ callbacks[name] << cbk
553
+ end
554
+
555
+ protected
556
+
557
+ # value of the last called callback is returned when executing callbacks
558
+ # (or nil if none found)
559
+ def execute_callbacks(name, *args) #:nodoc:
560
+ (callbacks[name.to_s] || []).inject(nil) do |prev_value, callback|
561
+ callback.call(self, *args)
562
+ end
563
+ end
564
+
565
+ def execute_callbacks_for(origin, name, *args) #:nodoc:
566
+ (callbacks[name.to_s] || []).inject(nil) do |prev_value, callback|
567
+ callback.origin == origin ? callback.call(self, *args) : prev_value
568
+ end
569
+ end
570
+
571
+ # initialize the document. initialize_raw is true when
572
+ # document is initialized from a raw serialized form
573
+ def do_initialize(store, slots={}, initialize_raw = false) #:nodoc:
574
+ @callbacks = {}
575
+ @store = store
576
+
577
+ if initialize_raw
578
+ initialize_raw_slots slots
579
+ @saved = true
580
+ else
581
+ @new = true
582
+ initialize_slots slots
583
+
584
+ self[:uuid] = Util.random_uuid unless self[:uuid]
585
+ generate_new_version! unless self[:version]
586
+ end
587
+ end
588
+
589
+ # initialize slots for a new, just created document
590
+ def initialize_slots(slots) #:nodoc:
591
+ @slots = {}
592
+ slots = slots.stringify_keys
593
+
594
+ # there is a reason for meta slot is initialized separately —
595
+ # we need to setup coercions before initializing actual slots
596
+ if meta = slots['meta']
597
+ meta = [meta] unless meta.is_a?(Array)
598
+ meta.each {|m| metas.add_meta(m) }
599
+ end
600
+
601
+ slots.except('meta').each {|name,value| self[name] = value }
602
+
603
+ # now, when we have all slots initialized, we can run initialization callbacks
604
+ execute_callbacks :on_initialization
605
+ execute_callbacks :on_new_document
606
+ end
607
+
608
+ # initialize slots from a raw representation
609
+ def initialize_raw_slots(slots) #:nodoc:
610
+ @slots = {}
611
+
612
+ slots.each do |name,value|
613
+ s = Slot.new(self, name)
614
+ s.raw_value = value
615
+
616
+ @slots[name.to_s] = s
617
+ end
618
+ end
619
+
620
+ # returns an array of meta modules (as constants) for a given something
621
+ # (a document reference, a document itself, or an array of the former)
622
+ def self.collect_meta_modules(store, meta) #:nodoc:
623
+ meta_names = []
624
+
625
+ case meta
626
+ when VERSIONREF
627
+ if m = store.find($1, $2)
628
+ meta_names << m[:name]
629
+ end
630
+ when DOCREF
631
+ if m = store.find($1)
632
+ meta_names << m[:name]
633
+ end
634
+ when Array
635
+ meta_names = meta.map { |m| collect_meta_modules(store, m) }.flatten
636
+ when Document
637
+ meta_names << meta[:name]
638
+ end
639
+
640
+ meta_names.map { |m| m.is_a?(String) ? (m.constantize rescue nil) : m }.compact
641
+ end
642
+
643
+ def generate_new_version!
644
+ self.version = Util.random_uuid
645
+ end
646
+
647
+ def update_version!(slotname)
648
+ if @saved && slotname != 'version' && slotname != 'previous_version'
649
+ self[:previous_version] = version unless version.nil?
650
+
651
+ generate_new_version!
652
+
653
+ @saved = nil
654
+ end
655
+ end
656
+ end
657
+
658
+ #
659
+ # VersionedDocument is a module that is being added to all document's of specific version.
660
+ # It should not be accessed directly
661
+ #
662
+ module VersionedDocument
663
+ #
664
+ # Reloads the same version of the same document from store. All unsaved changes will be lost!
665
+ #
666
+ def reload
667
+ store.find(uuid, version)
668
+ end
669
+ end
670
+
671
+ #
672
+ # ImmutableDocument can't be saved
673
+ # It should not be used directly, use Document#make_immutable! instead
674
+ #
675
+ module ImmutableDocument
676
+ def mutable?
677
+ false
678
+ end
679
+
680
+ def save!
681
+ self
682
+ end
683
+ end
684
+ end