couchbase-model 0.0.1

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,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ test/CouchbaseMock.jar
@@ -0,0 +1,11 @@
1
+ before_install:
2
+ - wget -O- http://packages.couchbase.com/ubuntu/couchbase.key | sudo apt-key add -
3
+ - echo deb http://packages.couchbase.com/ubuntu lucid lucid/main | sudo tee /etc/apt/sources.list.d/couchbase.list
4
+ - sudo apt-get update
5
+ - sudo apt-get -y install libevent-dev libvbucket-dev libcouchbase-dev
6
+
7
+ rvm:
8
+ - 1.8.7
9
+ - 1.9.2
10
+ - 1.9.3
11
+ - ree
@@ -0,0 +1,5 @@
1
+ --protected
2
+ --no-private
3
+ -
4
+ README.markdown
5
+ HISTORY.markdown
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in couchbase-model.gemspec
4
+ gemspec
@@ -0,0 +1,51 @@
1
+ # Couchbase Model [![Build Status](https://secure.travis-ci.org/avsej/ruby-couchbase-model.png?branch=master)](http://travis-ci.org/avsej/ruby-couchbase-model)
2
+
3
+ This library allows to declare models for [couchbase gem][1]. Here are example:
4
+
5
+ require 'couchbase/model'
6
+
7
+ class Post < Couchbase::Model
8
+ attribute :title
9
+ attribute :body
10
+ attribute :draft
11
+ end
12
+
13
+ p = Post.new(:id => 'hello-world',
14
+ :title => 'Hello world',
15
+ :draft => true)
16
+ p.save
17
+ p = Post.find('hello-world')
18
+ p.body = "Once upon the times...."
19
+ p.save
20
+ p.update(:draft => false)
21
+ Post.bucket.get('hello-world') #=> {"title"=>"Hello world", "draft"=>false,
22
+ # "body"=>"Once upon the times...."}
23
+
24
+ You can also let the library generate the unique identifier for you:
25
+
26
+ p = Post.create(:title => 'How to generate ID',
27
+ :body => 'Open up the editor...')
28
+ p.id #=> "74f43c3116e788d09853226603000809"
29
+
30
+ There are several algorithms available. By default it use `:sequential`
31
+ algorithm, but you can change it to more suitable one for you:
32
+
33
+ class Post < Couchbase::Model
34
+ attribute :title
35
+ attribute :body
36
+ attribute :draft
37
+
38
+ uuid_algorithm :random
39
+ end
40
+
41
+ You can define connection options on per model basis:
42
+
43
+ class Post < Couchbase::Model
44
+ attribute :title
45
+ attribute :body
46
+ attribute :draft
47
+
48
+ connect :port => 80, :bucket => 'blog'
49
+ end
50
+
51
+ [1]: https://github.com/couchbase/couchbase-ruby-client/
@@ -0,0 +1,22 @@
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 'bundler/gem_tasks'
19
+
20
+ Dir['tasks/*.rake'].sort.each { |f| load f }
21
+
22
+ task :default => :test
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "couchbase/model/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "couchbase-model"
7
+ s.version = Couchbase::Model::VERSION
8
+ s.authors = ["Sergey Avseyev"]
9
+ s.email = ["sergey.avseyev@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Declarative interface to Couchbase}
12
+ s.description = %q{ORM-like interface allows you to persist your models to Couchbase}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_runtime_dependency 'couchbase', '~> 1.1.0'
20
+
21
+ s.add_development_dependency 'rake', '~> 0.8.7'
22
+ s.add_development_dependency 'minitest'
23
+ s.add_development_dependency 'rdiscount'
24
+ s.add_development_dependency 'yard'
25
+ s.add_development_dependency RUBY_VERSION =~ /^1\.9/ ? 'ruby-debug19' : 'ruby-debug'
26
+ end
@@ -0,0 +1,18 @@
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'
@@ -0,0 +1,332 @@
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'
19
+ require 'couchbase/model/version'
20
+ require 'couchbase/model/uuid'
21
+
22
+ module Couchbase
23
+
24
+ class Error::MissingId < Error::Base; end
25
+
26
+ # Declarative layer for Couchbase gem
27
+ #
28
+ # require 'couchbase/model'
29
+ #
30
+ # class Post < Couchbase::Model
31
+ # attribute :title
32
+ # attribute :body
33
+ # attribute :draft
34
+ # end
35
+ #
36
+ # p = Post.new(:id => 'hello-world',
37
+ # :title => 'Hello world',
38
+ # :draft => true)
39
+ # p.save
40
+ # p = Post.find('hello-world')
41
+ # p.body = "Once upon the times...."
42
+ # p.save
43
+ # p.update(:draft => false)
44
+ # Post.bucket.get('hello-world') #=> {"title"=>"Hello world", "draft"=>false,
45
+ # # "body"=>"Once upon the times...."}
46
+ #
47
+ # You can also let the library generate the unique identifier for you:
48
+ #
49
+ # p = Post.create(:title => 'How to generate ID',
50
+ # :body => 'Open up the editor...')
51
+ # p.id #=> "74f43c3116e788d09853226603000809"
52
+ #
53
+ # There are several algorithms available. By default it use `:sequential`
54
+ # algorithm, but you can change it to more suitable one for you:
55
+ #
56
+ # class Post < Couchbase::Model
57
+ # attribute :title
58
+ # attribute :body
59
+ # attribute :draft
60
+ #
61
+ # uuid_algorithm :random
62
+ # end
63
+ #
64
+ # You can define connection options on per model basis:
65
+ #
66
+ # class Post < Couchbase::Model
67
+ # attribute :title
68
+ # attribute :body
69
+ # attribute :draft
70
+ #
71
+ # connect :port => 80, :bucket => 'blog'
72
+ # end
73
+ class Model
74
+ # Each model must have identifier
75
+ attr_accessor :id
76
+
77
+ # @private Container for all attributes of all subclasses
78
+ @@attributes = ::Hash.new {|hash, key| hash[key] = []}
79
+
80
+ # Use custom connection options
81
+ #
82
+ # @param [String, Hash, Array] options for establishing connection.
83
+ #
84
+ # @see Couchbase::Bucket#initialize
85
+ #
86
+ # @example Choose specific bucket
87
+ # class Post < Couchbase::Model
88
+ # connect :bucket => 'posts'
89
+ # ...
90
+ # end
91
+ def self.connect(*options)
92
+ self.bucket = Couchbase.connect(*options)
93
+ end
94
+
95
+ # Choose the UUID generation algorithms
96
+ #
97
+ # @param [Symbol] algorithm (:sequential) one of the available
98
+ # algorithms.
99
+ #
100
+ # @see Couchbase::UUID#next
101
+ #
102
+ # @example Select :random UUID generation algorithm
103
+ # class Post < Couchbase::Model
104
+ # uuid_algorithm :random
105
+ # ...
106
+ # end
107
+ def self.uuid_algorithm(algorithm)
108
+ self.thread_storage[:uuid_algorithm] = algorithm
109
+ end
110
+
111
+ # Defines an attribute for the model
112
+ #
113
+ # @param [Symbol, String] name name of the attribute
114
+ #
115
+ # @example Define some attributes for a model
116
+ # class Post < Couchbase::Model
117
+ # attribute :title
118
+ # attribute :body
119
+ # attribute :published_at
120
+ # end
121
+ #
122
+ # post = Post.new(:title => 'Hello world',
123
+ # :body => 'This is the first example...',
124
+ # :published_at => Time.now)
125
+ def self.attribute(name)
126
+ define_method(name) do
127
+ @_attributes[name]
128
+ end
129
+ define_method(:"#{name}=") do |value|
130
+ @_attributes[name] = value
131
+ end
132
+ attributes << name unless attributes.include?(name)
133
+ end
134
+
135
+ # Find the model using +id+ attribute
136
+ #
137
+ # @param [String, Symbol] id model identificator
138
+ # @return [Couchbase::Model] an instance of the model
139
+ #
140
+ # @example Find model using +id+
141
+ # post = Post.find('the-id')
142
+ def self.find(id)
143
+ if id && (obj = bucket.get(id))
144
+ new({:id => id}.merge(obj))
145
+ end
146
+ end
147
+
148
+ # Create the model with given attributes
149
+ #
150
+ # @param [Hash] args attribute-value pairs for the object
151
+ # @return [Couchbase::Model] an instance of the model
152
+ def self.create(*args)
153
+ new(*args).create
154
+ end
155
+
156
+ # Constructor for all subclasses of Couchbase::Model, which optionally
157
+ # takes a Hash of attribute value pairs.
158
+ #
159
+ # @param [Hash] attrs attribute-value pairs
160
+ def initialize(attrs = {})
161
+ @id = nil
162
+ @_attributes = ::Hash.new
163
+ update_attributes(attrs)
164
+ end
165
+
166
+ # Create this model and assign new id if necessary
167
+ #
168
+ # @return [Couchbase::Model] newly created object
169
+ #
170
+ # @raise [Couchbase::Error::KeyExists] if model with the same +id+
171
+ # exists in the bucket
172
+ #
173
+ # @example Create the instance of the Post model
174
+ # p = Post.new(:title => 'Hello world', :draft => true)
175
+ # p.create
176
+ def create
177
+ @id ||= Couchbase::Model::UUID.generator.next(1, model.thread_storage[:uuid_algorithm])
178
+ model.bucket.add(@id, attributes)
179
+ self
180
+ end
181
+
182
+ # Create or update this object based on the state of #new?.
183
+ #
184
+ # @return [Couchbase::Model] The saved object
185
+ #
186
+ # @example Update the Post model
187
+ # p = Post.find('hello-world')
188
+ # p.draft = false
189
+ # p.save
190
+ def save
191
+ return create if new?
192
+ model.bucket.set(@id, attributes)
193
+ self
194
+ end
195
+
196
+ # Update this object, optionally accepting new attributes.
197
+ #
198
+ # @param [Hash] attrs Attribute value pairs to use for the updated
199
+ # version
200
+ # @return [Couchbase::Model] The updated object
201
+ def update(attrs)
202
+ update_attributes(attrs)
203
+ save
204
+ end
205
+
206
+ # Delete this object from the bucket
207
+ #
208
+ # @note This method will reset +id+ attribute
209
+ #
210
+ # @return [Couchbase::Model] Returns a reference of itself.
211
+ #
212
+ # @example Delete the Post model
213
+ # p = Post.find('hello-world')
214
+ # p.delete
215
+ def delete
216
+ raise Couchbase::Error::MissingId, "missing id attribute" unless @id
217
+ model.bucket.delete(@id)
218
+ @id = nil
219
+ self
220
+ end
221
+
222
+ # Check if the record have +id+ attribute
223
+ #
224
+ # @return [true, false] Whether or not this object has an id.
225
+ #
226
+ # @note +true+ doesn't mean that record exists in the database
227
+ #
228
+ # @see Couchbase::Model#exists?
229
+ def new?
230
+ !@id
231
+ end
232
+
233
+ # Check if the key exists in the bucket
234
+ #
235
+ # @param [String, Symbol] id the record identifier
236
+ # @return [true, false] Whether or not the object with given +id+
237
+ # presented in the bucket.
238
+ def self.exists?(id)
239
+ !!bucket.get(id, :quiet => true)
240
+ end
241
+
242
+ # Check if this model exists in the bucket.
243
+ #
244
+ # @return [true, false] Whether or not this object presented in the
245
+ # bucket.
246
+ def exists?
247
+ model.exists?(@id)
248
+ end
249
+
250
+ # All the defined attributes within a class.
251
+ #
252
+ # @see Model.attribute
253
+ def self.attributes
254
+ @@attributes[self]
255
+ end
256
+
257
+ # All the attributes of the current instance
258
+ #
259
+ # @return [Hash]
260
+ def attributes
261
+ @_attributes
262
+ end
263
+
264
+ # Update all attributes without persisting the changes.
265
+ #
266
+ # @param [Hash] attrs attribute-value pairs.
267
+ def update_attributes(attrs)
268
+ if id = attrs.delete(:id)
269
+ @id = id
270
+ end
271
+ attrs.each do |key, value|
272
+ send(:"#{key}=", value)
273
+ end
274
+ end
275
+
276
+ # Reload all the model attributes from the bucket
277
+ #
278
+ # @return [Model] the latest model state
279
+ #
280
+ # @raise [Error::MissingId] for records without +id+
281
+ # attribute
282
+ def reload
283
+ raise Couchbase::Error::MissingId, "missing id attribute" unless @id
284
+ attrs = model.find(@id).attributes
285
+ update_attributes(attrs)
286
+ self
287
+ end
288
+
289
+ # @private The thread local storage for model specific stuff
290
+ def self.thread_storage
291
+ Couchbase.thread_storage[self] ||= {:uuid_algorithm => :sequential}
292
+ end
293
+
294
+ # @private Fetch the current connection
295
+ def self.bucket
296
+ self.thread_storage[:bucket] ||= Couchbase.bucket
297
+ end
298
+
299
+ # @private Set the current connection
300
+ #
301
+ # @param [Bucket] connection the connection instance
302
+ def self.bucket=(connection)
303
+ self.thread_storage[:bucket] = connection
304
+ end
305
+
306
+ # @private Get model class
307
+ def model
308
+ self.class
309
+ end
310
+
311
+ # @private Wrap the hash to the model class
312
+ #
313
+ # @param [Model, Hash] the Couchbase::Model subclass or the
314
+ # attribute-value pairs
315
+ def self.wrap(object)
316
+ object.class == self ? object : new(object)
317
+ end
318
+
319
+ # @private Returns a string containing a human-readable representation
320
+ # of the record.
321
+ def inspect
322
+ attrs = model.attributes.sort.map do |attr|
323
+ [attr, @_attributes[attr].inspect]
324
+ end
325
+ sprintf("#<%s:%s %s>",
326
+ model, new? ? "?" : id,
327
+ attrs.map{|a| a.join("=")}.join(" "))
328
+ end
329
+
330
+ end
331
+
332
+ end
@@ -0,0 +1,103 @@
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 'thread'
19
+
20
+ module Couchbase
21
+
22
+ class Model
23
+
24
+ # Generator of CouchDB specfic UUIDs. This is the ruby implementation of
25
+ # couch_uuids.erl from couchdb distribution. It is threadsafe.
26
+
27
+ class UUID
28
+ # Get default UUID generator. You can create your own if you like.
29
+ def self.generator
30
+ @generator ||= UUID.new
31
+ end
32
+
33
+ # Initialize generator.
34
+ #
35
+ # @param [Fixnum] seed seed for pseudorandom number generator.
36
+ def initialize(seed = nil)
37
+ seed ? srand(seed) : srand
38
+ @prefix, _ = rand_bytes(13).unpack("H26")
39
+ @inc = rand(0xfff) + 1
40
+ @lock = Mutex.new
41
+ end
42
+
43
+ # Generate list of UUIDs.
44
+ #
45
+ # @param [Fixnum] count number of UUIDs you need
46
+ #
47
+ # @param [Symbol] algorithm Algorithm to use. Known algorithms:
48
+ # [:random]
49
+ # 128 bits of random awesome. All awesome, all the time.
50
+ # [:sequential]
51
+ # Monotonically increasing ids with random increments. First 26 hex
52
+ # characters are random. Last 6 increment in random amounts until an
53
+ # overflow occurs. On overflow, the random prefix is regenerated and
54
+ # the process starts over.
55
+ # [:utc_random]
56
+ # Time since Jan 1, 1970 UTC with microseconds. First 14 characters
57
+ # are the time in hex. Last 18 are random.
58
+ #
59
+ # @return [String, Array] single string value or array of strings. Where
60
+ # each value represents 128-bit number written in hexadecimal format.
61
+ def next(count = 1, algorithm = :sequential)
62
+ raise ArgumentError, "count should be a positive number" unless count > 0
63
+ uuids = case algorithm
64
+ when :random
65
+ rand_bytes(16 * count).unpack("H32" * count)
66
+ when :utc_random
67
+ now = Time.now.utc
68
+ prefix = "%014x" % [now.to_i * 1_000_000 + now.usec]
69
+ rand_bytes(9 * count).unpack("H18" * count).map do |tail|
70
+ "#{prefix}#{tail}"
71
+ end
72
+ when :sequential
73
+ (1..count).map{ next_seq }
74
+ else
75
+ raise ArgumentError, "Unknown algorithm #{algo}. Should be one :sequential, :random or :utc_random"
76
+ end
77
+ uuids.size == 1 ? uuids[0] : uuids
78
+ end
79
+
80
+ private
81
+
82
+ def next_seq
83
+ @lock.synchronize do
84
+ if @inc >= 0xfff000
85
+ @prefix, _ = rand_bytes(13).unpack("H26")
86
+ @inc = rand(0xfff) + 1
87
+ end
88
+ @inc += rand(0xfff) + 1
89
+ "%s%06x" % [@prefix, @inc]
90
+ end
91
+ end
92
+
93
+ def rand_bytes(count)
94
+ bytes = ""
95
+ count.times { bytes << rand(256) }
96
+ bytes
97
+ end
98
+
99
+ end
100
+
101
+ end
102
+
103
+ end
@@ -0,0 +1,26 @@
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
+ module Couchbase
19
+
20
+ class Model
21
+
22
+ VERSION = "0.0.1"
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,25 @@
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 'rake/clean'
19
+ require 'yard'
20
+ require 'yard/rake/yardoc_task'
21
+
22
+ YARD::Rake::YardocTask.new do |t|
23
+ t.options = %w(--protected --no-private)
24
+ t.files.push('-', 'README.markdown', 'HISTORY.markdown')
25
+ end
@@ -0,0 +1,35 @@
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 'rake/testtask'
19
+ require 'rake/clean'
20
+
21
+ rule 'test/CouchbaseMock.jar' do |task|
22
+ jar_path = "0.5-SNAPSHOT/CouchbaseMock-0.5-20120222.060643-15.jar"
23
+ sh %{wget -q -O test/CouchbaseMock.jar http://files.couchbase.com/maven2/org/couchbase/mock/CouchbaseMock/#{jar_path}}
24
+ end
25
+
26
+ CLOBBER << 'test/CouchbaseMock.jar'
27
+
28
+ Rake::TestTask.new do |test|
29
+ test.libs << "test" << "."
30
+ test.ruby_opts << "-rruby-debug" if ENV['DEBUG']
31
+ test.pattern = 'test/test_*.rb'
32
+ test.options = '--verbose'
33
+ end
34
+
35
+ Rake::Task['test'].prerequisites.unshift('test/CouchbaseMock.jar')
@@ -0,0 +1,21 @@
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
+ desc 'Start an irb session and load the library.'
19
+ task :console do
20
+ exec "irb -I lib -rruby-debug -rcouchbase-model"
21
+ end
@@ -0,0 +1,168 @@
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 'minitest/autorun'
19
+ require 'couchbase'
20
+ require 'couchbase/model'
21
+
22
+ require 'socket'
23
+ require 'open-uri'
24
+
25
+ class CouchbaseServer
26
+ attr_accessor :host, :port, :num_nodes, :buckets_spec
27
+
28
+ def real?
29
+ true
30
+ end
31
+
32
+ def initialize(params = {})
33
+ @host, @port = ENV['COUCHBASE_SERVER'].split(':')
34
+ @port = @port.to_i
35
+
36
+ if @host.nil? || @host.empty? || @port == 0
37
+ raise ArgumentError, "Check COUCHBASE_SERVER variable. It should be hostname:port"
38
+ end
39
+
40
+ @config = Yajl::Parser.parse(open("http://#{@host}:#{@port}/pools/default"))
41
+ @num_nodes = @config["nodes"].size
42
+ @buckets_spec = params[:buckets_spec] || "default:" # "default:,protected:secret,cache::memcache"
43
+ end
44
+
45
+ def start
46
+ # flush all buckets
47
+ @buckets_spec.split(',') do |bucket|
48
+ name, password, _ = bucket.split(':')
49
+ connection = Couchbase.new(:hostname => @host,
50
+ :port => @port,
51
+ :username => name,
52
+ :bucket => name,
53
+ :password => password)
54
+ connection.flush
55
+ end
56
+ end
57
+ def stop; end
58
+ end
59
+
60
+ class CouchbaseMock
61
+ Monitor = Struct.new(:pid, :client, :socket, :port)
62
+
63
+ attr_accessor :host, :port, :buckets_spec, :num_nodes, :num_vbuckets
64
+
65
+ def real?
66
+ false
67
+ end
68
+
69
+ def initialize(params = {})
70
+ @host = "127.0.0.1"
71
+ @port = 0
72
+ @num_nodes = 10
73
+ @num_vbuckets = 4096
74
+ @buckets_spec = "default:" # "default:,protected:secret,cache::memcache"
75
+ params.each do |key, value|
76
+ send("#{key}=", value)
77
+ end
78
+ yield self if block_given?
79
+ if @num_vbuckets < 1 || (@num_vbuckets & (@num_vbuckets - 1) != 0)
80
+ raise ArgumentError, "Number of vbuckets should be a power of two and greater than zero"
81
+ end
82
+ end
83
+
84
+ def start
85
+ @monitor = Monitor.new
86
+ @monitor.socket = TCPServer.new(nil, 0)
87
+ @monitor.socket.listen(10)
88
+ _, @monitor.port, _, _ = @monitor.socket.addr
89
+ trap("CLD") do
90
+ puts "CouchbaseMock.jar died unexpectedly during startup"
91
+ exit(1)
92
+ end
93
+ @monitor.pid = fork
94
+ if @monitor.pid.nil?
95
+ rc = exec(command_line("--harakiri-monitor=:#{@monitor.port}"))
96
+ else
97
+ trap("CLD", "SIG_DFL")
98
+ @monitor.client, _ = @monitor.socket.accept
99
+ @port = @monitor.client.recv(100).to_i
100
+ end
101
+ end
102
+
103
+ def stop
104
+ @monitor.client.close
105
+ @monitor.socket.close
106
+ Process.kill("TERM", @monitor.pid)
107
+ Process.wait(@monitor.pid)
108
+ end
109
+
110
+ def failover_node(index, bucket = "default")
111
+ @monitor.client.send("failover,#{index},#{bucket}", 0)
112
+ end
113
+
114
+ def respawn_node(index, bucket = "default")
115
+ @monitor.client.send("respawn,#{index},#{bucket}", 0)
116
+ end
117
+
118
+ protected
119
+
120
+ def command_line(extra = nil)
121
+ cmd = "java -jar #{File.dirname(__FILE__)}/CouchbaseMock.jar"
122
+ cmd << " --host #{@host}" if @host
123
+ cmd << " --port #{@port}" if @port
124
+ cmd << " --nodes #{@num_nodes}" if @num_nodes
125
+ cmd << " --vbuckets #{@num_vbuckets}" if @num_vbuckets
126
+ cmd << " --buckets #{@buckets_spec}" if @buckets_spec
127
+ cmd << " #{extra}"
128
+ cmd
129
+ end
130
+ end
131
+
132
+ class MiniTest::Unit::TestCase
133
+
134
+ def start_mock(params = {})
135
+ mock = nil
136
+ if ENV['COUCHBASE_SERVER']
137
+ mock = CouchbaseServer.new(params)
138
+ if (params[:port] && mock.port != params[:port]) ||
139
+ (params[:host] && mock.host != params[:host]) ||
140
+ mock.buckets_spec != "default:"
141
+ skip("Unable to configure real cluster. Requested config is: #{params.inspect}")
142
+ end
143
+ else
144
+ mock = CouchbaseMock.new(params)
145
+ end
146
+ mock.start
147
+ mock
148
+ end
149
+
150
+ def stop_mock(mock)
151
+ assert(mock)
152
+ mock.stop
153
+ end
154
+
155
+ def with_mock(params = {})
156
+ mock = nil
157
+ if block_given?
158
+ mock = start_mock(params)
159
+ yield mock
160
+ end
161
+ ensure
162
+ stop_mock(mock) if mock
163
+ end
164
+
165
+ def uniq_id(*suffixes)
166
+ [caller.first[/.*[` ](.*)'/, 1], suffixes].join("_")
167
+ end
168
+ end
@@ -0,0 +1,114 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2011, 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 File.join(File.dirname(__FILE__), 'setup')
19
+
20
+ class Post < Couchbase::Model
21
+ attribute :title
22
+ attribute :body
23
+ end
24
+
25
+ class TestModel < MiniTest::Unit::TestCase
26
+
27
+ def setup
28
+ @mock = start_mock
29
+ Post.bucket = Couchbase.connect(:hostname => @mock.host,
30
+ :port => @mock.port)
31
+ end
32
+
33
+ def teardown
34
+ stop_mock(@mock)
35
+ end
36
+
37
+ def test_assigns_attributes_from_the_hash
38
+ post = Post.new(:title => "Hello, world")
39
+ assert_equal "Hello, world", post.title
40
+ refute post.body
41
+ refute post.id
42
+ end
43
+
44
+ def test_assings_id_and_saves_the_object
45
+ post = Post.create(:title => "Hello, world")
46
+ assert post.id
47
+ end
48
+
49
+ def test_updates_attributes
50
+ post = Post.create(:title => "Hello, world")
51
+ post.update(:body => "This is my first example")
52
+ assert_equal "This is my first example", post.body
53
+ end
54
+
55
+ def test_refreshes_the_attributes_with_reload_method
56
+ orig = Post.create(:title => "Hello, world")
57
+ double = Post.find(orig.id)
58
+ double.update(:title => "Good bye, world")
59
+ orig.reload
60
+ assert_equal "Good bye, world", orig.title
61
+ end
62
+
63
+ def test_doesnt_raise_if_the_attribute_redefined
64
+ eval <<-EOC
65
+ class RefinedPost < Couchbase::Model
66
+ attribute :title
67
+ attribute :title
68
+ end
69
+ EOC
70
+ end
71
+
72
+ def test_allows_arbitrary_ids
73
+ Post.create(:id => uniq_id, :title => "Foo")
74
+ assert_equal "Foo", Post.find(uniq_id).title
75
+ end
76
+
77
+ def test_returns_an_instance_of_post
78
+ Post.bucket.set(uniq_id, {:title => 'foo'})
79
+ assert Post.find(uniq_id).kind_of?(Post)
80
+ assert_equal uniq_id, Post.find(uniq_id).id
81
+ assert_equal "foo", Post.find(uniq_id).title
82
+ end
83
+
84
+ def test_changes_its_attributes
85
+ post = Post.create(:title => "Hello, world")
86
+ post.title = "Good bye, world"
87
+ post.save.reload
88
+ assert_equal "Good bye, world", post.title
89
+ end
90
+
91
+ def test_assings_a_new_id_to_each_record
92
+ post1 = Post.create
93
+ post2 = Post.create
94
+
95
+ refute post1.new?
96
+ refute post2.new?
97
+ refute_equal post1.id, post2.id
98
+ end
99
+
100
+ def test_deletes_an_existent_model
101
+ post = Post.create(:id => uniq_id)
102
+ assert post.delete
103
+ refute Post.bucket.get(uniq_id)
104
+ end
105
+
106
+ def test_fails_to_delete_model_without_id
107
+ post = Post.new(:title => "Hello")
108
+ refute post.id
109
+ assert_raises Couchbase::Error::MissingId do
110
+ post.delete
111
+ end
112
+ end
113
+
114
+ end
@@ -0,0 +1,32 @@
1
+ require File.join(File.dirname(__FILE__), 'setup')
2
+
3
+ class TestUUID < MiniTest::Unit::TestCase
4
+
5
+ def test_it_can_generate_10k_unique_ids
6
+ random = Couchbase::Model::UUID.new.next(10_000, :random)
7
+ assert_equal 10_000, random.uniq.size
8
+
9
+ utc_random = Couchbase::Model::UUID.new.next(10_000, :utc_random)
10
+ assert_equal 10_000, utc_random.uniq.size
11
+
12
+ sequential = Couchbase::Model::UUID.new.next(10_000, :sequential)
13
+ assert_equal 10_000, sequential.uniq.size
14
+ end
15
+
16
+ def test_it_produces_monotonically_increasing_ids
17
+ utc_random = Couchbase::Model::UUID.new
18
+ assert utc_random.next(1, :utc_random) < utc_random.next(1, :utc_random)
19
+
20
+ sequential = Couchbase::Model::UUID.new
21
+ assert sequential.next(1, :sequential) < sequential.next(1, :sequential)
22
+ end
23
+
24
+ def test_it_roll_over
25
+ generator = Couchbase::Model::UUID.new
26
+ prefix = generator.next[0, 26]
27
+ n = 0
28
+ n += 1 while prefix == generator.next[0, 26]
29
+ assert(n >= 5000 && n <= 11000)
30
+ end
31
+
32
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: couchbase-model
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Sergey Avseyev
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-07 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: couchbase
16
+ requirement: &8647480 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.1.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *8647480
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ requirement: &8646760 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 0.8.7
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *8646760
36
+ - !ruby/object:Gem::Dependency
37
+ name: minitest
38
+ requirement: &8575100 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *8575100
47
+ - !ruby/object:Gem::Dependency
48
+ name: rdiscount
49
+ requirement: &8574240 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *8574240
58
+ - !ruby/object:Gem::Dependency
59
+ name: yard
60
+ requirement: &8573480 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *8573480
69
+ - !ruby/object:Gem::Dependency
70
+ name: ruby-debug19
71
+ requirement: &8572660 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *8572660
80
+ description: ORM-like interface allows you to persist your models to Couchbase
81
+ email:
82
+ - sergey.avseyev@gmail.com
83
+ executables: []
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - .gitignore
88
+ - .travis.yml
89
+ - .yardopts
90
+ - Gemfile
91
+ - README.markdown
92
+ - Rakefile
93
+ - couchbase-model.gemspec
94
+ - lib/couchbase-model.rb
95
+ - lib/couchbase/model.rb
96
+ - lib/couchbase/model/uuid.rb
97
+ - lib/couchbase/model/version.rb
98
+ - tasks/doc.rake
99
+ - tasks/test.rake
100
+ - tasks/util.rake
101
+ - test/setup.rb
102
+ - test/test_model.rb
103
+ - test/test_uuid.rb
104
+ homepage: ''
105
+ licenses: []
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ none: false
112
+ requirements:
113
+ - - ! '>='
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubyforge_project:
124
+ rubygems_version: 1.8.10
125
+ signing_key:
126
+ specification_version: 3
127
+ summary: Declarative interface to Couchbase
128
+ test_files:
129
+ - test/setup.rb
130
+ - test/test_model.rb
131
+ - test/test_uuid.rb