couchbase-model 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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