jmoses-couchbase-model 0.5.3

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.
@@ -0,0 +1,865 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2012 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'digest/md5'
19
+
20
+ require 'couchbase'
21
+ require 'couchbase/model/version'
22
+ require 'couchbase/model/uuid'
23
+ require 'couchbase/model/configuration'
24
+
25
+ unless Object.respond_to?(:singleton_class)
26
+ require 'couchbase/model/ext/singleton_class'
27
+ end
28
+ unless ''.respond_to?(:constantize)
29
+ require 'couchbase/model/ext/constantize'
30
+ end
31
+ unless ''.respond_to?(:camelize)
32
+ require 'couchbase/model/ext/camelize'
33
+ end
34
+
35
+ module Couchbase
36
+
37
+ # @since 0.0.1
38
+ class Error::MissingId < Error::Base; end
39
+
40
+ # @since 0.4.0
41
+ class Error::RecordInvalid < Error::Base
42
+ attr_reader :record
43
+ def initialize(record)
44
+ @record = record
45
+ if @record.errors
46
+ super(@record.errors.full_messages.join(', '))
47
+ else
48
+ super('Record invalid')
49
+ end
50
+ end
51
+ end
52
+
53
+ # Declarative layer for Couchbase gem
54
+ #
55
+ # @since 0.0.1
56
+ #
57
+ # require 'couchbase/model'
58
+ #
59
+ # class Post < Couchbase::Model
60
+ # attribute :title
61
+ # attribute :body
62
+ # attribute :draft
63
+ # end
64
+ #
65
+ # p = Post.new(:id => 'hello-world',
66
+ # :title => 'Hello world',
67
+ # :draft => true)
68
+ # p.save
69
+ # p = Post.find('hello-world')
70
+ # p.body = "Once upon the times...."
71
+ # p.save
72
+ # p.update(:draft => false)
73
+ # Post.bucket.get('hello-world') #=> {"title"=>"Hello world", "draft"=>false,
74
+ # # "body"=>"Once upon the times...."}
75
+ #
76
+ # You can also let the library generate the unique identifier for you:
77
+ #
78
+ # p = Post.create(:title => 'How to generate ID',
79
+ # :body => 'Open up the editor...')
80
+ # p.id #=> "74f43c3116e788d09853226603000809"
81
+ #
82
+ # There are several algorithms available. By default it use `:sequential`
83
+ # algorithm, but you can change it to more suitable one for you:
84
+ #
85
+ # class Post < Couchbase::Model
86
+ # attribute :title
87
+ # attribute :body
88
+ # attribute :draft
89
+ #
90
+ # uuid_algorithm :random
91
+ # end
92
+ #
93
+ # You can define connection options on per model basis:
94
+ #
95
+ # class Post < Couchbase::Model
96
+ # attribute :title
97
+ # attribute :body
98
+ # attribute :draft
99
+ #
100
+ # connect :port => 80, :bucket => 'blog'
101
+ # end
102
+ class Model
103
+ # Each model must have identifier
104
+ #
105
+ # @since 0.0.1
106
+ attr_accessor :id
107
+
108
+ # @since 0.2.0
109
+ attr_reader :key
110
+
111
+ # @since 0.2.0
112
+ attr_reader :value
113
+
114
+ # @since 0.2.0
115
+ attr_reader :doc
116
+
117
+ # @since 0.2.0
118
+ attr_reader :meta
119
+
120
+ # @since 0.4.5
121
+ attr_reader :errors
122
+
123
+ # @since 0.4.5
124
+ attr_reader :raw
125
+
126
+ # @private Container for all attributes with defaults of all subclasses
127
+ @@attributes = {}
128
+
129
+ # @private Container for all view names of all subclasses
130
+ @@views = {}
131
+
132
+ # Use custom connection options
133
+ #
134
+ # @since 0.0.1
135
+ #
136
+ # @param [String, Hash, Array] options options for establishing
137
+ # connection.
138
+ # @return [Couchbase::Bucket]
139
+ #
140
+ # @see Couchbase::Bucket#initialize
141
+ #
142
+ # @example Choose specific bucket
143
+ # class Post < Couchbase::Model
144
+ # connect :bucket => 'posts'
145
+ # ...
146
+ # end
147
+ def self.connect(*options)
148
+ self.bucket = Couchbase.connect(*options)
149
+ end
150
+
151
+ # Associate custom design document with the model
152
+ #
153
+ # Design document is the special document which contains views, the
154
+ # chunks of code for building map/reduce indexes. When this method
155
+ # called without argument, it just returns the effective design document
156
+ # name.
157
+ #
158
+ # @since 0.1.0
159
+ #
160
+ # @see http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views.html
161
+ #
162
+ # @param [String, Symbol] name the name for the design document. By
163
+ # default underscored model name is used.
164
+ # @return [String] the effective design document
165
+ #
166
+ # @example Choose specific design document name
167
+ # class Post < Couchbase::Model
168
+ # design_document :my_posts
169
+ # ...
170
+ # end
171
+ def self.design_document(name = nil)
172
+ if name
173
+ @_design_doc = name.to_s
174
+ else
175
+ @_design_doc ||= begin
176
+ name = self.name.dup
177
+ name.gsub!(/::/, '_')
178
+ name.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
179
+ name.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
180
+ name.downcase!
181
+ end
182
+ end
183
+ end
184
+
185
+ def self.defaults(options = nil)
186
+ if options
187
+ @_defaults = options
188
+ else
189
+ @_defaults || {}
190
+ end
191
+ end
192
+
193
+ # Ensure that design document is up to date.
194
+ #
195
+ # @since 0.1.0
196
+ #
197
+ # This method also cares about organizing view in separate javascript
198
+ # files. The general structure is the following (+[root]+ is the
199
+ # directory, one of the {Model::Configuration.design_documents_paths}):
200
+ #
201
+ # [root]
202
+ # |
203
+ # `- link
204
+ # | |
205
+ # | `- by_created_at
206
+ # | | |
207
+ # | | `- map.js
208
+ # | |
209
+ # | `- by_session_id
210
+ # | | |
211
+ # | | `- map.js
212
+ # | |
213
+ # | `- total_views
214
+ # | | |
215
+ # | | `- map.js
216
+ # | | |
217
+ # | | `- reduce.js
218
+ #
219
+ # The directory structure above demonstrate layout for design document
220
+ # with id +_design/link+ and three views: +by_create_at+,
221
+ # +by_session_id` and `total_views`.
222
+ def self.ensure_design_document!
223
+ unless Configuration.design_documents_paths
224
+ raise 'Configuration.design_documents_path must be directory'
225
+ end
226
+
227
+ doc = {'_id' => "_design/#{design_document}", 'views' => {}}
228
+ digest = Digest::MD5.new
229
+ mtime = 0
230
+ views.each do |name, _|
231
+ doc['views'][name] = {}
232
+ doc['spatial'] = {}
233
+ ['map', 'reduce', 'spatial'].each do |type|
234
+ Configuration.design_documents_paths.each do |path|
235
+ ff = File.join(path, design_document.to_s, name.to_s, "#{type}.js")
236
+ if File.file?(ff)
237
+ contents = File.read(ff).gsub(/^\s*\/\/.*$\n\r?/, '').strip
238
+ next if contents.empty?
239
+ mtime = [mtime, File.mtime(ff).to_i].max
240
+ digest << contents
241
+ case type
242
+ when 'map', 'reduce'
243
+ doc['views'][name][type] = contents
244
+ when 'spatial'
245
+ doc['spatial'][name] = contents
246
+ end
247
+ break # pick first matching file
248
+ end
249
+ end
250
+ end
251
+ end
252
+
253
+ doc['views'].delete_if {|_, v| v.empty? }
254
+ doc.delete('spatial') if doc['spatial'] && doc['spatial'].empty?
255
+ doc['signature'] = digest.to_s
256
+ doc['timestamp'] = mtime
257
+ if doc['signature'] != thread_storage[:signature] && doc['timestamp'] > thread_storage[:timestamp].to_i
258
+ current_doc = bucket.design_docs[design_document.to_s]
259
+ if current_doc.nil? || (current_doc['signature'] != doc['signature'] && doc['timestamp'] > current_doc[:timestamp].to_i)
260
+ bucket.save_design_doc(doc)
261
+ current_doc = doc
262
+ end
263
+ thread_storage[:signature] = current_doc['signature']
264
+ thread_storage[:timestamp] = current_doc['timestamp'].to_i
265
+ end
266
+ end
267
+
268
+ # Choose the UUID generation algorithms
269
+ #
270
+ # @since 0.0.1
271
+ #
272
+ # @param [Symbol] algorithm (:sequential) one of the available
273
+ # algorithms.
274
+ #
275
+ # @see Couchbase::UUID#next
276
+ #
277
+ # @example Select :random UUID generation algorithm
278
+ # class Post < Couchbase::Model
279
+ # uuid_algorithm :random
280
+ # ...
281
+ # end
282
+ #
283
+ # @return [Symbol]
284
+ def self.uuid_algorithm(algorithm)
285
+ self.thread_storage[:uuid_algorithm] = algorithm
286
+ end
287
+
288
+ def read_attribute(attr_name)
289
+ @_attributes[attr_name]
290
+ end
291
+
292
+ def write_attribute(attr_name, value)
293
+ @_attributes[attr_name] = value
294
+ end
295
+
296
+ # Defines an attribute for the model
297
+ #
298
+ # @since 0.0.1
299
+ #
300
+ # @param [Symbol, String] name name of the attribute
301
+ #
302
+ # @example Define some attributes for a model
303
+ # class Post < Couchbase::Model
304
+ # attribute :title
305
+ # attribute :body
306
+ # attribute :published_at
307
+ # end
308
+ #
309
+ # post = Post.new(:title => 'Hello world',
310
+ # :body => 'This is the first example...',
311
+ # :published_at => Time.now)
312
+ def self.attribute(*names)
313
+ options = {}
314
+ if names.last.is_a?(Hash)
315
+ options = names.pop
316
+ end
317
+ names.each do |name|
318
+ name = name.to_sym
319
+ attributes[name] = options[:default]
320
+ next if self.instance_methods.include?(name)
321
+ define_method(name) do
322
+ read_attribute(name)
323
+ end
324
+ define_method(:"#{name}=") do |value|
325
+ write_attribute(name, value)
326
+ end
327
+ end
328
+ end
329
+
330
+ # Defines a view for the model
331
+ #
332
+ # @since 0.0.1
333
+ #
334
+ # @param [Symbol, String, Array] names names of the views
335
+ # @param [Hash] options options passed to the {Couchbase::View}
336
+ #
337
+ # @example Define some views for a model
338
+ # class Post < Couchbase::Model
339
+ # view :all, :published
340
+ # view :by_rating, :include_docs => false
341
+ # end
342
+ #
343
+ # post = Post.find("hello")
344
+ # post.by_rating.each do |r|
345
+ # # ...
346
+ # end
347
+ def self.view(*names)
348
+ options = {:wrapper_class => self, :include_docs => true}
349
+ if names.last.is_a?(Hash)
350
+ options.update(names.pop)
351
+ end
352
+ is_spatial = options.delete(:spatial)
353
+ names.each do |name|
354
+ path = '_design/%s/_%s/%s' % [design_document, is_spatial ? 'spatial' : 'view', name]
355
+ views[name] = lambda do |*params|
356
+ params = options.merge(params.first || {})
357
+ View.new(bucket, path, params)
358
+ end
359
+ singleton_class.send(:define_method, name, &views[name])
360
+ end
361
+ end
362
+
363
+ # Defines a belongs_to association for the model
364
+ #
365
+ # @since 0.3.0
366
+ #
367
+ # @param [Symbol, String] name name of the associated model
368
+ # @param [Hash] options association options
369
+ # @option options [String, Symbol] :class_name the name of the
370
+ # association class
371
+ #
372
+ # @example Define some association for a model
373
+ # class Brewery < Couchbase::Model
374
+ # attribute :name
375
+ # end
376
+ #
377
+ # class Beer < Couchbase::Model
378
+ # attribute :name, :brewery_id
379
+ # belongs_to :brewery
380
+ # end
381
+ #
382
+ # Beer.find("heineken").brewery.name
383
+ def self.belongs_to(name, options = {})
384
+ ref = "#{name}_id"
385
+ attribute(ref)
386
+ assoc = name.to_s.camelize.constantize
387
+ define_method(name) do
388
+ assoc.find(self.send(ref))
389
+ end
390
+ end
391
+
392
+ class << self
393
+ def _find(quiet, *ids)
394
+ wants_array = ids.first.kind_of?(Array)
395
+ ids = ids.flatten.compact.uniq
396
+ unless ids.empty?
397
+ res = bucket.get(ids, :quiet => quiet, :extended => true).map do |id, (obj, flags, cas)|
398
+ obj = {:raw => obj} unless obj.is_a?(Hash)
399
+ new({:id => id, :meta => {'flags' => flags, 'cas' => cas}}.merge(obj))
400
+ end
401
+ wants_array ? res : res.first
402
+ end
403
+ end
404
+
405
+ private :_find
406
+ end
407
+
408
+ # Find the model using +id+ attribute
409
+ #
410
+ # @since 0.0.1
411
+ #
412
+ # @param [String, Symbol, Array] id model identificator(s)
413
+ # @return [Couchbase::Model, Array] an instance of the model, or an array of instances
414
+ # @raise [Couchbase::Error::NotFound] when given key isn't exist
415
+ #
416
+ # @example Find model using +id+
417
+ # post = Post.find('the-id')
418
+ #
419
+ # @example Find multiple models using +id+
420
+ # post = Post.find('one', 'two')
421
+ def self.find(*id)
422
+ _find(false, *id)
423
+ end
424
+
425
+ # Find the model using +id+ attribute
426
+ #
427
+ # Unlike {Couchbase::Model.find}, this method won't raise
428
+ # {Couchbase::Error::NotFound} error when key doesn't exist in the
429
+ # bucket
430
+ #
431
+ # @since 0.1.0
432
+ #
433
+ # @param [String, Symbol] id model identificator(s)
434
+ # @return [Couchbase::Model, Array, nil] an instance of the model, an array
435
+ # of found instances of the model, or +nil+ if
436
+ # given key isn't exist
437
+ #
438
+ # @example Find model using +id+
439
+ # post = Post.find_by_id('the-id')
440
+ # @example Find multiple models using +id+
441
+ # posts = Post.find_by_id(['the-id', 'the-id2'])
442
+ def self.find_by_id(*id)
443
+ _find(true, *id)
444
+ end
445
+
446
+ # Create the model with given attributes
447
+ #
448
+ # @since 0.0.1
449
+ #
450
+ # @param [Hash] args attribute-value pairs for the object
451
+ # @return [Couchbase::Model, false] an instance of the model
452
+ def self.create(*args)
453
+ new(*args).create
454
+ end
455
+
456
+ # Creates an object just like {{Model.create} but raises an exception if
457
+ # the record is invalid.
458
+ #
459
+ # @since 0.5.1
460
+ # @raise [Couchbase::Error::RecordInvalid] if the instance is invalid
461
+ def self.create!(*args)
462
+ new(*args).create!
463
+ end
464
+
465
+ # Constructor for all subclasses of Couchbase::Model
466
+ #
467
+ # @since 0.0.1
468
+ #
469
+ # Optionally takes a Hash of attribute value pairs.
470
+ #
471
+ # @param [Hash] attrs attribute-value pairs
472
+ def initialize(attrs = {})
473
+ @errors = ::ActiveModel::Errors.new(self) if defined?(::ActiveModel)
474
+ @_attributes = ::Hash.new do |h, k|
475
+ default = self.class.attributes[k]
476
+ h[k] = if default.respond_to?(:call)
477
+ default.call
478
+ else
479
+ default
480
+ end
481
+ end
482
+ case attrs
483
+ when Hash
484
+ if defined?(HashWithIndifferentAccess) && !attrs.is_a?(HashWithIndifferentAccess)
485
+ attrs = attrs.with_indifferent_access
486
+ end
487
+ @id = attrs.delete(:id)
488
+ @key = attrs.delete(:key)
489
+ @value = attrs.delete(:value)
490
+ @doc = attrs.delete(:doc)
491
+ @meta = attrs.delete(:meta)
492
+ @raw = attrs.delete(:raw)
493
+ update_attributes(@doc || attrs)
494
+ else
495
+ @raw = attrs
496
+ end
497
+ end
498
+
499
+ # Create this model and assign new id if necessary
500
+ #
501
+ # @since 0.0.1
502
+ #
503
+ # @return [Couchbase::Model, false] newly created object
504
+ #
505
+ # @raise [Couchbase::Error::KeyExists] if model with the same +id+
506
+ # exists in the bucket
507
+ #
508
+ # @example Create the instance of the Post model
509
+ # p = Post.new(:title => 'Hello world', :draft => true)
510
+ # p.create
511
+ def create(options = {})
512
+ @id ||= Couchbase::Model::UUID.generator.next(1, model.thread_storage[:uuid_algorithm])
513
+ if respond_to?(:valid?) && !valid?
514
+ return false
515
+ end
516
+ options = model.defaults.merge(options)
517
+ value = (options[:format] == :plain) ? @raw : attributes_with_values
518
+ unless @meta
519
+ @meta = {}
520
+ if @meta.respond_to?(:with_indifferent_access)
521
+ @meta = @meta.with_indifferent_access
522
+ end
523
+ end
524
+ @meta['cas'] = model.bucket.add(@id, value, options)
525
+ self
526
+ end
527
+
528
+ # Creates an object just like {{Model#create} but raises an exception if
529
+ # the record is invalid.
530
+ #
531
+ # @since 0.5.1
532
+ #
533
+ # @raise [Couchbase::Error::RecordInvalid] if the instance is invalid
534
+ def create!(options = {})
535
+ create(options) || raise(Couchbase::Error::RecordInvalid.new(self))
536
+ end
537
+
538
+ # Create or update this object based on the state of #new?.
539
+ #
540
+ # @since 0.0.1
541
+ #
542
+ # @param [Hash] options options for operation, see
543
+ # {{Couchbase::Bucket#set}}
544
+ #
545
+ # @return [Couchbase::Model, false] saved object or false if there
546
+ # are validation errors
547
+ #
548
+ # @example Update the Post model
549
+ # p = Post.find('hello-world')
550
+ # p.draft = false
551
+ # p.save
552
+ #
553
+ # @example Use CAS value for optimistic lock
554
+ # p = Post.find('hello-world')
555
+ # p.draft = false
556
+ # p.save('cas' => p.meta['cas'])
557
+ #
558
+ def save(options = {})
559
+ return create(options) unless @meta
560
+ if respond_to?(:valid?) && !valid?
561
+ return false
562
+ end
563
+ options = model.defaults.merge(options)
564
+ value = (options[:format] == :plain) ? @raw : attributes_with_values
565
+ @meta['cas'] = model.bucket.replace(@id, value, options)
566
+ self
567
+ end
568
+
569
+ # Creates an object just like {{Model#save} but raises an exception if
570
+ # the record is invalid.
571
+ #
572
+ # @since 0.5.1
573
+ #
574
+ # @raise [Couchbase::Error::RecordInvalid] if the instance is invalid
575
+ def save!(options = {})
576
+ save(options) || raise(Couchbase::Error::RecordInvalid.new(self))
577
+ end
578
+
579
+ # Update this object, optionally accepting new attributes.
580
+ #
581
+ # @since 0.0.1
582
+ #
583
+ # @param [Hash] attrs Attribute value pairs to use for the updated
584
+ # version
585
+ # @param [Hash] options options for operation, see
586
+ # {{Couchbase::Bucket#set}}
587
+ # @return [Couchbase::Model] The updated object
588
+ def update(attrs, options = {})
589
+ update_attributes(attrs)
590
+ save(options)
591
+ end
592
+
593
+ # Delete this object from the bucket
594
+ #
595
+ # @since 0.0.1
596
+ #
597
+ # @note This method will reset +id+ attribute
598
+ #
599
+ # @param [Hash] options options for operation, see
600
+ # {{Couchbase::Bucket#delete}}
601
+ # @return [Couchbase::Model] Returns a reference of itself.
602
+ #
603
+ # @example Delete the Post model
604
+ # p = Post.find('hello-world')
605
+ # p.delete
606
+ def delete(options = {})
607
+ raise Couchbase::Error::MissingId, 'missing id attribute' unless @id
608
+ model.bucket.delete(@id, options)
609
+ @id = nil
610
+ @meta = nil
611
+ self
612
+ end
613
+
614
+ # Check if the record have +id+ attribute
615
+ #
616
+ # @since 0.0.1
617
+ #
618
+ # @return [true, false] Whether or not this object has an id.
619
+ #
620
+ # @note +true+ doesn't mean that record exists in the database
621
+ #
622
+ # @see Couchbase::Model#exists?
623
+ def new?
624
+ !@id
625
+ end
626
+
627
+ # @return [true, false] Where on on this object persisted in the storage
628
+ def persisted?
629
+ !!@id
630
+ end
631
+
632
+ # Check if the key exists in the bucket
633
+ #
634
+ # @since 0.0.1
635
+ #
636
+ # @param [String, Symbol] id the record identifier
637
+ # @return [true, false] Whether or not the object with given +id+
638
+ # presented in the bucket.
639
+ def self.exists?(id)
640
+ !!bucket.get(id, :quiet => true)
641
+ end
642
+
643
+ # Check if this model exists in the bucket.
644
+ #
645
+ # @since 0.0.1
646
+ #
647
+ # @return [true, false] Whether or not this object presented in the
648
+ # bucket.
649
+ def exists?
650
+ model.exists?(@id)
651
+ end
652
+
653
+ # All defined attributes within a class.
654
+ #
655
+ # @since 0.0.1
656
+ #
657
+ # @see Model.attribute
658
+ #
659
+ # @return [Hash]
660
+ def self.attributes
661
+ @attributes ||= if self == Model
662
+ @@attributes.dup
663
+ else
664
+ couchbase_ancestor.attributes.dup
665
+ end
666
+ end
667
+
668
+ # All defined views within a class.
669
+ #
670
+ # @since 0.1.0
671
+ #
672
+ # @see Model.view
673
+ #
674
+ # @return [Array]
675
+ def self.views
676
+ @views ||= if self == Model
677
+ @@views.dup
678
+ else
679
+ couchbase_ancestor.views.dup
680
+ end
681
+ end
682
+
683
+ # Returns the first ancestor that is also a Couchbase::Model ancestor.
684
+ #
685
+ # @return Class
686
+ def self.couchbase_ancestor
687
+ ancestors[1..-1].each do |ancestor|
688
+ return ancestor if ancestor.ancestors.include?(Couchbase::Model)
689
+ end
690
+ end
691
+
692
+ # All the attributes of the current instance
693
+ #
694
+ # @since 0.0.1
695
+ #
696
+ # @return [Hash]
697
+ def attributes
698
+ @_attributes
699
+ end
700
+
701
+ # Update all attributes without persisting the changes.
702
+ #
703
+ # @since 0.0.1
704
+ #
705
+ # @param [Hash] attrs attribute-value pairs.
706
+ def update_attributes(attrs)
707
+ if id = attrs.delete(:id)
708
+ @id = id
709
+ end
710
+ attrs.each do |key, value|
711
+ setter = :"#{key}="
712
+ send(setter, value) if respond_to?(setter)
713
+ end
714
+ end
715
+
716
+ # Reload all the model attributes from the bucket
717
+ #
718
+ # @since 0.0.1
719
+ #
720
+ # @return [Model] the latest model state
721
+ #
722
+ # @raise [Error::MissingId] for records without +id+
723
+ # attribute
724
+ def reload
725
+ raise Couchbase::Error::MissingId, 'missing id attribute' unless @id
726
+ attrs = model.find(@id).attributes
727
+ update_attributes(attrs)
728
+ self
729
+ end
730
+
731
+ # Format the model for use in a JSON response
732
+ #
733
+ # @since 0.5.2
734
+ #
735
+ # @return [Hash] a JSON representation of the model for REST APIs
736
+ #
737
+ def as_json(options = {})
738
+ attributes.merge({:id => @id}).as_json(options)
739
+ end
740
+
741
+ # @private The thread local storage for model specific stuff
742
+ #
743
+ # @since 0.0.1
744
+ def self.thread_storage
745
+ Couchbase.thread_storage[self] ||= {:uuid_algorithm => :sequential}
746
+ end
747
+
748
+ # @private Fetch the current connection
749
+ #
750
+ # @since 0.0.1
751
+ def self.bucket
752
+ self.thread_storage[:bucket] ||= Couchbase.bucket
753
+ end
754
+
755
+ # @private Set the current connection
756
+ #
757
+ # @since 0.0.1
758
+ #
759
+ # @param [Bucket] connection the connection instance
760
+ def self.bucket=(connection)
761
+ self.thread_storage[:bucket] = connection
762
+ end
763
+
764
+ # @private Get model class
765
+ #
766
+ # @since 0.0.1
767
+ def model
768
+ self.class
769
+ end
770
+
771
+ # @private Wrap the hash to the model class.
772
+ #
773
+ # @since 0.0.1
774
+ #
775
+ # @param [Bucket] bucket the reference to Bucket instance
776
+ # @param [Hash] data the Hash fetched by View, it should have at least
777
+ # +"id"+, +"key"+ and +"value"+ keys, also it could have optional
778
+ # +"doc"+ key.
779
+ #
780
+ # @return [Model]
781
+ def self.wrap(bucket, data)
782
+ doc = {
783
+ :id => data['id'],
784
+ :key => data['key'],
785
+ :value => data['value']
786
+ }
787
+ if data['doc']
788
+ doc[:meta] = data['doc']['meta']
789
+ doc[:doc] = data['doc']['value'] || data['doc']['json']
790
+ end
791
+ new(doc)
792
+ end
793
+
794
+ # @private Returns a string containing a human-readable representation
795
+ # of the record.
796
+ #
797
+ # @since 0.0.1
798
+ def inspect
799
+ attrs = []
800
+ attrs << ['key', @key.inspect] unless @key.nil?
801
+ attrs << ['value', @value.inspect] unless @value.nil?
802
+ model.attributes.map do |attr, default|
803
+ val = read_attribute(attr)
804
+ attrs << [attr.to_s, val.inspect] unless val.nil?
805
+ end
806
+ attrs.sort!
807
+ attrs.unshift([:id, id]) unless new?
808
+ sprintf('#<%s %s>', model, attrs.map { |a| a.join(': ') }.join(', '))
809
+ end
810
+
811
+ def self.inspect
812
+ buf = "#{name}"
813
+ if self != Couchbase::Model
814
+ buf << "(#{['id', attributes.map(&:first)].flatten.join(', ')})"
815
+ end
816
+ buf
817
+ end
818
+
819
+ # @private Returns a hash with model attributes
820
+ #
821
+ # @since 0.1.0
822
+ def attributes_with_values
823
+ ret = {:type => model.design_document}
824
+ model.attributes.keys.each do |attr|
825
+ ret[attr] = read_attribute(attr)
826
+ end
827
+ ret
828
+ end
829
+
830
+ protected :attributes_with_values
831
+
832
+ if defined?(::ActiveModel)
833
+ extend ActiveModel::Callbacks
834
+ extend ActiveModel::Naming
835
+ include ActiveModel::Conversion
836
+ include ActiveModel::Validations
837
+
838
+ define_model_callbacks :create, :update, :delete, :save
839
+ [:save, :create, :update, :delete].each do |meth|
840
+ class_eval <<-EOC
841
+ alias #{meth}_without_callbacks #{meth}
842
+ def #{meth}(*args, &block)
843
+ run_callbacks(:#{meth}) do
844
+ #{meth}_without_callbacks(*args, &block)
845
+ end
846
+ end
847
+ EOC
848
+ end
849
+ end
850
+
851
+ # Redefine (if exists) #to_key to use #key if #id is missing
852
+ def to_key
853
+ keys = [id || key]
854
+ keys.empty? ? nil : keys
855
+ end
856
+
857
+ def to_param
858
+ keys = to_key
859
+ if keys && !keys.empty?
860
+ keys.join('-')
861
+ end
862
+ end
863
+ end
864
+
865
+ end