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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +203 -0
- data/README.md +155 -0
- data/Rakefile +22 -0
- data/couchbase-jruby-model.gemspec +27 -0
- data/lib/couchbase-jruby-model.rb +23 -0
- data/lib/couchbase/active_model.rb +69 -0
- data/lib/couchbase/model.rb +859 -0
- data/lib/couchbase/model/configuration.rb +30 -0
- data/lib/couchbase/model/ext/camelize.rb +23 -0
- data/lib/couchbase/model/ext/constantize.rb +29 -0
- data/lib/couchbase/model/ext/singleton_class.rb +24 -0
- data/lib/couchbase/model/uuid.rb +113 -0
- data/lib/couchbase/model/version.rb +26 -0
- data/lib/couchbase/railtie.rb +142 -0
- data/lib/rails/generators/couchbase/config/config_generator.rb +43 -0
- data/lib/rails/generators/couchbase/config/templates/couchbase.yml +23 -0
- data/lib/rails/generators/couchbase/view/templates/map.js +40 -0
- data/lib/rails/generators/couchbase/view/templates/reduce.js +61 -0
- data/lib/rails/generators/couchbase/view/view_generator.rb +43 -0
- data/lib/rails/generators/couchbase_generator.rb +42 -0
- data/tasks/package.rake +27 -0
- data/tasks/test.rake +34 -0
- data/tasks/util.rake +21 -0
- data/test/setup.rb +168 -0
- data/test/test_active_model_integration.rb +124 -0
- data/test/test_model.rb +311 -0
- data/test/test_model_rails_integration.rb +76 -0
- data/test/test_uuid.rb +32 -0
- metadata +151 -0
@@ -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
|