jmoses-couchbase-model 0.5.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.travis.yml +11 -0
- data/.yardopts +5 -0
- data/CONTRIBUTING.markdown +75 -0
- data/Gemfile +4 -0
- data/HISTORY.markdown +112 -0
- data/README.markdown +160 -0
- data/Rakefile +22 -0
- data/couchbase-model.gemspec +24 -0
- data/lib/couchbase-model.rb +23 -0
- data/lib/couchbase/model.rb +865 -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_model.rb +302 -0
- data/test/test_model_rails_integration.rb +76 -0
- data/test/test_uuid.rb +32 -0
- metadata +148 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
// The map function is the most critical part of any view as it provides the
|
2
|
+
// logical mapping between the input fields of the individual objects stored
|
3
|
+
// within Couchbase to the information output when the view is accessed.
|
4
|
+
//
|
5
|
+
// Read more about how to write map functions at:
|
6
|
+
// http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views-writing-map.html
|
7
|
+
|
8
|
+
function(doc, meta) {
|
9
|
+
emit(meta.id, null);
|
10
|
+
}
|
11
|
+
|
12
|
+
// You can also check out following examples
|
13
|
+
//
|
14
|
+
// The simplest example of a map function:
|
15
|
+
//
|
16
|
+
// function(doc, meta) {
|
17
|
+
// emit(meta.id, doc);
|
18
|
+
// }
|
19
|
+
//
|
20
|
+
// Slightly more complex example of a function that defines a view on values
|
21
|
+
// computed from customer documents:
|
22
|
+
//
|
23
|
+
// function(doc, meta) {
|
24
|
+
// if (doc.type == "customer") {
|
25
|
+
// emit(meta.id, {last_name: doc.last_name, first_name: doc.first_name});
|
26
|
+
// }
|
27
|
+
// }
|
28
|
+
//
|
29
|
+
// To be able to filter or sort the view by some document property, you
|
30
|
+
// would use that property for the key. For example, the following view
|
31
|
+
// would allow you to lookup customer documents by the last_name or
|
32
|
+
// first_name fields (your keys could be compound, e.g. arrays):
|
33
|
+
//
|
34
|
+
// function(doc, meta) {
|
35
|
+
// if (doc.type == "customer") {
|
36
|
+
// emit(doc.last_name, {first_name: doc.first_name});
|
37
|
+
// emit(doc.first_name, {last_name: doc.last_name});
|
38
|
+
// }
|
39
|
+
// }
|
40
|
+
//
|
@@ -0,0 +1,61 @@
|
|
1
|
+
// If a view has a reduce function, it is used to produce aggregate results
|
2
|
+
// for that view. A reduce function is passed a set of intermediate values
|
3
|
+
// and combines them to a single value. Reduce functions must accept, as
|
4
|
+
// input, results emitted by its corresponding map function as well as
|
5
|
+
// results returned by the reduce function itself. The latter case is
|
6
|
+
// referred to as a rereduce.
|
7
|
+
//
|
8
|
+
// function (key, values, rereduce) {
|
9
|
+
// return sum(values);
|
10
|
+
// }
|
11
|
+
//
|
12
|
+
// Reduce functions must handle two cases:
|
13
|
+
//
|
14
|
+
// 1. When rereduce is false:
|
15
|
+
//
|
16
|
+
// reduce([ [key1,id1], [key2,id2], [key3,id3] ], [value1,value2,value3], false)
|
17
|
+
//
|
18
|
+
// * key will be an array whose elements are arrays of the form [key,id],
|
19
|
+
// where key is a key emitted by the map function and id is that of the
|
20
|
+
// document from which the key was generated.
|
21
|
+
// * values will be an array of the values emitted for the respective
|
22
|
+
// elements in keys
|
23
|
+
//
|
24
|
+
// 2. When rereduce is true:
|
25
|
+
//
|
26
|
+
// reduce(null, [intermediate1,intermediate2,intermediate3], true)
|
27
|
+
//
|
28
|
+
// * key will be null
|
29
|
+
// * values will be an array of values returned by previous calls to the
|
30
|
+
// reduce function
|
31
|
+
//
|
32
|
+
// Reduce functions should return a single value, suitable for both the
|
33
|
+
// value field of the final view and as a member of the values array passed
|
34
|
+
// to the reduce function.
|
35
|
+
//
|
36
|
+
// NOTE: If this file is empty, reduce part will be skipped in design document
|
37
|
+
//
|
38
|
+
// There is number of built-in functions, which could be used instead of
|
39
|
+
// javascript implementation of reduce function.
|
40
|
+
//
|
41
|
+
// The _count function provides a simple count of the input rows from the
|
42
|
+
// map function, using the keys and group level to provide to provide a
|
43
|
+
// count of the correlated items. The values generated during the map()
|
44
|
+
// stage are ignored.
|
45
|
+
//
|
46
|
+
// _count
|
47
|
+
//
|
48
|
+
// The built-in _sum function collates the output from the map function
|
49
|
+
// call. The information can either be a single number or an array of numbers.
|
50
|
+
//
|
51
|
+
// _sum
|
52
|
+
//
|
53
|
+
// The _stats built-in produces statistical calculations for the input data.
|
54
|
+
// Like the _sum call the source information should be a number. The
|
55
|
+
// generated statistics include the sum, count, minimum (min), maximum (max)
|
56
|
+
// and sum squared (sumsqr) of the input rows.
|
57
|
+
//
|
58
|
+
// _stats
|
59
|
+
//
|
60
|
+
// Read more about how to write reduce functions at:
|
61
|
+
// http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views-writing-reduce.html
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Couchbase <info@couchbase.com>
|
4
|
+
# Copyright:: 2012 Couchbase, Inc.
|
5
|
+
# License:: Apache License, Version 2.0
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
|
20
|
+
require 'rails/generators/couchbase_generator'
|
21
|
+
|
22
|
+
module Couchbase
|
23
|
+
module Generators
|
24
|
+
class ViewGenerator < Rails::Generators::Base
|
25
|
+
desc 'Creates a Couchbase views skeletons for map/reduce functions'
|
26
|
+
|
27
|
+
argument :model_name, :type => :string
|
28
|
+
argument :view_name, :type => :string
|
29
|
+
|
30
|
+
source_root File.expand_path('../templates', __FILE__)
|
31
|
+
|
32
|
+
def app_name
|
33
|
+
Rails::Application.subclasses.first.parent.to_s.underscore
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_map_reduce_files
|
37
|
+
template 'map.js', File.join('app', 'models', model_name, view_name, 'map.js')
|
38
|
+
template 'reduce.js', File.join('app', 'models', model_name, view_name, 'reduce.js')
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Couchbase <info@couchbase.com>
|
4
|
+
# Copyright:: 2012 Couchbase, Inc.
|
5
|
+
# License:: Apache License, Version 2.0
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
|
20
|
+
require 'rails/generators/named_base'
|
21
|
+
require 'rails/generators/active_model'
|
22
|
+
|
23
|
+
module Couchbase #:nodoc:
|
24
|
+
module Generators #:nodoc:
|
25
|
+
|
26
|
+
class Base < ::Rails::Generators::NamedBase #:nodoc:
|
27
|
+
|
28
|
+
def self.source_root
|
29
|
+
@_couchbase_source_root ||=
|
30
|
+
File.expand_path("../#{base_name}/#{generator_name}/templates", __FILE__)
|
31
|
+
end
|
32
|
+
|
33
|
+
unless methods.include?(:module_namespacing)
|
34
|
+
def module_namespacing(&block)
|
35
|
+
yield if block
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
data/tasks/package.rake
ADDED
@@ -0,0 +1,27 @@
|
|
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 'rubygems/package_task'
|
19
|
+
|
20
|
+
def gemspec
|
21
|
+
@clean_gemspec ||= eval(File.read(File.expand_path('../../couchbase-model.gemspec', __FILE__)))
|
22
|
+
end
|
23
|
+
|
24
|
+
Gem::PackageTask.new(gemspec) do |pkg|
|
25
|
+
pkg.need_tar = true
|
26
|
+
end
|
27
|
+
|
data/tasks/test.rake
ADDED
@@ -0,0 +1,34 @@
|
|
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-20120726.220757-19.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.pattern = 'test/test_*.rb'
|
31
|
+
test.options = '--verbose'
|
32
|
+
end
|
33
|
+
|
34
|
+
Rake::Task['test'].prerequisites.unshift('test/CouchbaseMock.jar')
|
data/tasks/util.rake
ADDED
@@ -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 -rcouchbase-model"
|
21
|
+
end
|
data/test/setup.rb
ADDED
@@ -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
|
data/test/test_model.rb
ADDED
@@ -0,0 +1,302 @@
|
|
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
|
+
attribute :author, :default => 'Anonymous'
|
24
|
+
attribute :created_at, :default => lambda { Time.utc('2010-01-01') }
|
25
|
+
end
|
26
|
+
|
27
|
+
class ValidPost < Couchbase::Model
|
28
|
+
attribute :title
|
29
|
+
|
30
|
+
def valid?
|
31
|
+
title && !title.empty?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Brewery < Couchbase::Model
|
36
|
+
attribute :name
|
37
|
+
end
|
38
|
+
|
39
|
+
class Beer < Couchbase::Model
|
40
|
+
attribute :name
|
41
|
+
belongs_to :brewery
|
42
|
+
end
|
43
|
+
|
44
|
+
class Attachment < Couchbase::Model
|
45
|
+
defaults :format => :plain
|
46
|
+
end
|
47
|
+
|
48
|
+
class Comments < Couchbase::Model
|
49
|
+
include Enumerable
|
50
|
+
attribute :comments, :default => []
|
51
|
+
end
|
52
|
+
|
53
|
+
class User < Couchbase::Model
|
54
|
+
design_document :people
|
55
|
+
end
|
56
|
+
|
57
|
+
class TestModel < MiniTest::Unit::TestCase
|
58
|
+
|
59
|
+
def setup
|
60
|
+
@mock = start_mock
|
61
|
+
bucket = Couchbase.connect(:hostname => @mock.host, :port => @mock.port)
|
62
|
+
[Post, ValidPost, Brewery, Beer, Attachment].each do |model|
|
63
|
+
model.bucket = bucket
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def teardown
|
68
|
+
stop_mock(@mock)
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_design_document
|
72
|
+
assert_equal 'people', User.design_document
|
73
|
+
assert_equal 'new_people', User.design_document('new_people')
|
74
|
+
assert_equal 'post', Post.design_document
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_it_supports_value_property
|
78
|
+
doc = {
|
79
|
+
'id' => 'x',
|
80
|
+
'key' => 'x',
|
81
|
+
'value' => 'x',
|
82
|
+
'doc' => {
|
83
|
+
'value' => {'title' => 'foo'}
|
84
|
+
}
|
85
|
+
}
|
86
|
+
post = Post.wrap(Post.bucket, doc)
|
87
|
+
assert_equal 'foo', post.title
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_it_supports_json_property
|
91
|
+
doc = {
|
92
|
+
'id' => 'x',
|
93
|
+
'key' => 'x',
|
94
|
+
'value' => 'x',
|
95
|
+
'doc' => {
|
96
|
+
'json' => {'title' => 'foo'}
|
97
|
+
}
|
98
|
+
}
|
99
|
+
post = Post.wrap(Post.bucket, doc)
|
100
|
+
assert_equal 'foo', post.title
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_assigns_attributes_from_the_hash
|
104
|
+
post = Post.new(:title => 'Hello, world')
|
105
|
+
assert_equal 'Hello, world', post.title
|
106
|
+
refute post.body
|
107
|
+
refute post.id
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_uses_default_value_or_nil
|
111
|
+
post = Post.new(:title => 'Hello, world')
|
112
|
+
refute post.body
|
113
|
+
assert_equal 'Anonymous', post.author
|
114
|
+
assert_equal 'Anonymous', post.attributes[:author]
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_allows_lambda_as_default_value
|
118
|
+
post = Post.new(:title => 'Hello, world')
|
119
|
+
expected = Time.utc('2010-01-01')
|
120
|
+
assert_equal expected, post.created_at
|
121
|
+
assert_equal expected, post.attributes[:created_at]
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_assings_id_and_saves_the_object
|
125
|
+
post = Post.create(:title => 'Hello, world')
|
126
|
+
assert post.id
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_updates_attributes
|
130
|
+
post = Post.create(:title => 'Hello, world')
|
131
|
+
post.update(:body => 'This is my first example')
|
132
|
+
assert_equal 'This is my first example', post.body
|
133
|
+
end
|
134
|
+
|
135
|
+
def test_refreshes_the_attributes_with_reload_method
|
136
|
+
orig = Post.create(:title => 'Hello, world')
|
137
|
+
double = Post.find(orig.id)
|
138
|
+
double.update(:title => 'Good bye, world')
|
139
|
+
orig.reload
|
140
|
+
assert_equal 'Good bye, world', orig.title
|
141
|
+
end
|
142
|
+
|
143
|
+
def test_it_raises_not_found_exception
|
144
|
+
assert_raises Couchbase::Error::NotFound do
|
145
|
+
Post.find('missing_key')
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def test_it_returns_nil_when_key_not_found
|
150
|
+
refute Post.find_by_id('missing_key')
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_doesnt_raise_if_the_attribute_redefined
|
154
|
+
eval <<-EOC
|
155
|
+
class RefinedPost < Couchbase::Model
|
156
|
+
attribute :title
|
157
|
+
attribute :title
|
158
|
+
end
|
159
|
+
EOC
|
160
|
+
end
|
161
|
+
|
162
|
+
def test_allows_to_define_several_attributes_at_once
|
163
|
+
eval <<-EOC
|
164
|
+
class Comment < Couchbase::Model
|
165
|
+
attribute :name, :email, :body
|
166
|
+
end
|
167
|
+
EOC
|
168
|
+
|
169
|
+
comment = Comment.new
|
170
|
+
assert_respond_to comment, :name
|
171
|
+
assert_respond_to comment, :email
|
172
|
+
assert_respond_to comment, :body
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_allows_arbitrary_ids
|
176
|
+
Post.create(:id => uniq_id, :title => 'Foo')
|
177
|
+
assert_equal 'Foo', Post.find(uniq_id).title
|
178
|
+
end
|
179
|
+
|
180
|
+
def test_returns_an_instance_of_post
|
181
|
+
Post.bucket.set(uniq_id, {:title => 'foo'})
|
182
|
+
assert Post.find(uniq_id).kind_of?(Post)
|
183
|
+
assert_equal uniq_id, Post.find(uniq_id).id
|
184
|
+
assert_equal 'foo', Post.find(uniq_id).title
|
185
|
+
end
|
186
|
+
|
187
|
+
def test_changes_its_attributes
|
188
|
+
post = Post.create(:title => 'Hello, world')
|
189
|
+
post.title = 'Good bye, world'
|
190
|
+
post.save.reload
|
191
|
+
assert_equal 'Good bye, world', post.title
|
192
|
+
end
|
193
|
+
|
194
|
+
def test_assings_a_new_id_to_each_record
|
195
|
+
post1 = Post.create
|
196
|
+
post2 = Post.create
|
197
|
+
|
198
|
+
refute post1.new?
|
199
|
+
refute post2.new?
|
200
|
+
refute_equal post1.id, post2.id
|
201
|
+
end
|
202
|
+
|
203
|
+
def test_deletes_an_existent_model
|
204
|
+
post = Post.create(:id => uniq_id)
|
205
|
+
assert post.delete
|
206
|
+
assert_raises Couchbase::Error::NotFound do
|
207
|
+
Post.bucket.get(uniq_id)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def test_fails_to_delete_model_without_id
|
212
|
+
post = Post.new(:title => 'Hello')
|
213
|
+
refute post.id
|
214
|
+
assert_raises Couchbase::Error::MissingId do
|
215
|
+
post.delete
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def test_belongs_to_assoc
|
220
|
+
brewery = Brewery.create(:name => 'Anheuser-Busch')
|
221
|
+
assert_includes Beer.attributes.keys, :brewery_id
|
222
|
+
beer = Beer.create(:name => 'Budweiser', :brewery_id => brewery.id)
|
223
|
+
assert_respond_to beer, :brewery
|
224
|
+
assoc = beer.brewery
|
225
|
+
assert_instance_of Brewery, assoc
|
226
|
+
assert_equal 'Anheuser-Busch', assoc.name
|
227
|
+
end
|
228
|
+
|
229
|
+
def test_to_key
|
230
|
+
assert_equal ['the-id'], Post.new(:id => 'the-id').to_key
|
231
|
+
assert_equal ['the-key'], Post.new(:key => 'the-key').to_key
|
232
|
+
end
|
233
|
+
|
234
|
+
def test_to_param
|
235
|
+
assert_equal 'the-id', Post.new(:id => 'the-id').to_param
|
236
|
+
assert_equal 'the-key', Post.new(:key => ['the', 'key']).to_param
|
237
|
+
end
|
238
|
+
|
239
|
+
def test_as_json
|
240
|
+
require 'active_support/json/encoding'
|
241
|
+
|
242
|
+
response = {'id' => 'the-id'}
|
243
|
+
assert_equal response, Post.new(:id => 'the-id').as_json
|
244
|
+
|
245
|
+
response = {}
|
246
|
+
assert_equal response, Post.new(:id => 'the-id').as_json(:except => :id)
|
247
|
+
end
|
248
|
+
|
249
|
+
def test_validation
|
250
|
+
post = ValidPost.create(:title => 'Hello, World!')
|
251
|
+
assert post.valid?, 'post with title should be valid'
|
252
|
+
post.title = nil
|
253
|
+
refute post.save
|
254
|
+
assert_raises(Couchbase::Error::RecordInvalid) do
|
255
|
+
post.save!
|
256
|
+
end
|
257
|
+
refute ValidPost.create(:title => nil)
|
258
|
+
assert_raises(Couchbase::Error::RecordInvalid) do
|
259
|
+
ValidPost.create!(:title => nil)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def test_blob_documents
|
264
|
+
contents = File.read(__FILE__)
|
265
|
+
id = Attachment.create(:raw => contents).id
|
266
|
+
blob = Attachment.find(id)
|
267
|
+
assert_equal contents, blob.raw
|
268
|
+
end
|
269
|
+
|
270
|
+
def test_couchbase_ancestor
|
271
|
+
assert_equal Couchbase::Model, Comments.couchbase_ancestor
|
272
|
+
end
|
273
|
+
|
274
|
+
def test_returns_multiple_instances_of_post
|
275
|
+
Post.create(:id => uniq_id('first'), :title => 'foo')
|
276
|
+
Post.create(:id => uniq_id('second'), :title => 'bar')
|
277
|
+
|
278
|
+
results = Post.find([uniq_id('first'), uniq_id('second')])
|
279
|
+
assert results.kind_of?(Array)
|
280
|
+
assert results.size == 2
|
281
|
+
assert results.detect { |post| post.id == uniq_id('first') }.title == 'foo'
|
282
|
+
assert results.detect { |post| post.id == uniq_id('second') }.title == 'bar'
|
283
|
+
end
|
284
|
+
|
285
|
+
def test_returns_array_for_array_of_ids
|
286
|
+
Post.create(:id => uniq_id('first'), :title => 'foo')
|
287
|
+
|
288
|
+
results = Post.find([uniq_id('first')])
|
289
|
+
assert results.kind_of?(Array)
|
290
|
+
assert results.size == 1
|
291
|
+
assert results[0].title == 'foo'
|
292
|
+
end
|
293
|
+
|
294
|
+
def test_returns_array_for_array_of_ids_using_find_by_id
|
295
|
+
Post.create(:id => uniq_id('first'), :title => 'foo')
|
296
|
+
|
297
|
+
results = Post.find_by_id([uniq_id('first')])
|
298
|
+
assert results.kind_of?(Array)
|
299
|
+
assert results.size == 1
|
300
|
+
assert results[0].title == 'foo'
|
301
|
+
end
|
302
|
+
end
|