jmoses-couchbase-model 0.5.3
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.
- 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
|