couchbase-jruby-model 0.1.0-java

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