derfred-couchrest 0.12.6 → 0.12.6.3
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/couchrest.rb +1 -1
- data/lib/couchrest/core/database.rb +20 -3
- data/lib/couchrest/helper/file_manager.rb +309 -0
- data/lib/couchrest/mixins.rb +3 -0
- data/lib/couchrest/mixins/design_doc.rb +63 -0
- data/lib/couchrest/mixins/document_queries.rb +48 -0
- data/lib/couchrest/mixins/extended_document_mixins.rb +4 -0
- data/lib/couchrest/mixins/extended_views.rb +169 -0
- data/lib/couchrest/mixins/properties.rb +63 -0
- data/lib/couchrest/mixins/views.rb +59 -0
- data/lib/couchrest/more/extended_document.rb +114 -0
- data/lib/couchrest/more/property.rb +26 -0
- data/spec/couchrest/core/database_spec.rb +12 -0
- data/spec/couchrest/core/server_spec.rb +34 -0
- data/spec/couchrest/more/property_spec.rb +36 -0
- data/spec/fixtures/more/card.rb +7 -0
- metadata +21 -1
data/lib/couchrest.rb
CHANGED
@@ -61,14 +61,31 @@ module CouchRest
|
|
61
61
|
# paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi
|
62
62
|
def view(name, params = {}, &block)
|
63
63
|
keys = params.delete(:keys)
|
64
|
+
stripes = params.delete(:stripes)
|
65
|
+
raw = params.delete(:raw)
|
64
66
|
url = CouchRest.paramify_url "#{@uri}/_view/#{name}", params
|
65
|
-
|
66
|
-
|
67
|
+
case
|
68
|
+
when keys
|
69
|
+
if raw
|
70
|
+
CouchRest.post_raw(url, {:keys => keys})
|
71
|
+
else
|
72
|
+
CouchRest.post(url, {:keys => keys})
|
73
|
+
end
|
74
|
+
when stripes
|
75
|
+
if raw
|
76
|
+
CouchRest.post_raw(url, {:stripes => stripes})
|
77
|
+
else
|
78
|
+
CouchRest.post(url, {:stripes => stripes})
|
79
|
+
end
|
67
80
|
else
|
68
81
|
if block_given?
|
69
82
|
@streamer.view(name, params, &block)
|
70
83
|
else
|
71
|
-
|
84
|
+
if raw
|
85
|
+
CouchRest.get_raw url
|
86
|
+
else
|
87
|
+
CouchRest.get url
|
88
|
+
end
|
72
89
|
end
|
73
90
|
end
|
74
91
|
end
|
@@ -0,0 +1,309 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module CouchRest
|
4
|
+
class FileManager
|
5
|
+
attr_reader :db
|
6
|
+
attr_accessor :loud
|
7
|
+
|
8
|
+
LANGS = {"rb" => "ruby", "js" => "javascript"}
|
9
|
+
MIMES = {
|
10
|
+
"html" => "text/html",
|
11
|
+
"htm" => "text/html",
|
12
|
+
"png" => "image/png",
|
13
|
+
"gif" => "image/gif",
|
14
|
+
"css" => "text/css",
|
15
|
+
"js" => "test/javascript",
|
16
|
+
"txt" => "text/plain"
|
17
|
+
}
|
18
|
+
|
19
|
+
# Generate an application in the given directory.
|
20
|
+
# This is a class method because it doesn't depend on
|
21
|
+
# specifying a database.
|
22
|
+
def self.generate_app(app_dir)
|
23
|
+
templatedir = File.join(File.expand_path(File.dirname(__FILE__)), 'app-template')
|
24
|
+
FileUtils.cp_r(templatedir, app_dir)
|
25
|
+
end
|
26
|
+
|
27
|
+
# instance methods
|
28
|
+
|
29
|
+
def initialize(dbname, host="http://127.0.0.1:5984")
|
30
|
+
@db = CouchRest.new(host).database(dbname)
|
31
|
+
end
|
32
|
+
|
33
|
+
# maintain the correspondence between an fs and couch
|
34
|
+
|
35
|
+
def push_app(appdir, appname)
|
36
|
+
libs = []
|
37
|
+
viewdir = File.join(appdir,"views")
|
38
|
+
attachdir = File.join(appdir,"_attachments")
|
39
|
+
|
40
|
+
@doc = dir_to_fields(appdir)
|
41
|
+
package_forms(@doc["forms"]) if @doc['forms']
|
42
|
+
package_views(@doc["views"]) if @doc['views']
|
43
|
+
|
44
|
+
docid = "_design/#{appname}"
|
45
|
+
design = @db.get(docid) rescue {}
|
46
|
+
design.merge!(@doc)
|
47
|
+
design['_id'] = docid
|
48
|
+
# design['language'] = lang if lang
|
49
|
+
@db.save(design)
|
50
|
+
push_directory(attachdir, docid)
|
51
|
+
end
|
52
|
+
|
53
|
+
def dir_to_fields(dir)
|
54
|
+
fields = {}
|
55
|
+
(Dir["#{dir}/**/*.*"] -
|
56
|
+
Dir["#{dir}/_attachments/**/*.*"]).each do |file|
|
57
|
+
farray = file.sub(dir, '').sub(/^\//,'').split('/')
|
58
|
+
myfield = fields
|
59
|
+
while farray.length > 1
|
60
|
+
front = farray.shift
|
61
|
+
myfield[front] ||= {}
|
62
|
+
myfield = myfield[front]
|
63
|
+
end
|
64
|
+
fname, fext = farray.shift.split('.')
|
65
|
+
fguts = File.open(file).read
|
66
|
+
if fext == 'json'
|
67
|
+
myfield[fname] = JSON.parse(fguts)
|
68
|
+
else
|
69
|
+
myfield[fname] = fguts
|
70
|
+
end
|
71
|
+
end
|
72
|
+
return fields
|
73
|
+
end
|
74
|
+
|
75
|
+
def push_directory(push_dir, docid=nil)
|
76
|
+
docid ||= push_dir.split('/').reverse.find{|part|!part.empty?}
|
77
|
+
|
78
|
+
pushfiles = Dir["#{push_dir}/**/*.*"].collect do |f|
|
79
|
+
{f.split("#{push_dir}/").last => open(f).read}
|
80
|
+
end
|
81
|
+
|
82
|
+
return if pushfiles.empty?
|
83
|
+
|
84
|
+
@attachments = {}
|
85
|
+
@signatures = {}
|
86
|
+
pushfiles.each do |file|
|
87
|
+
name = file.keys.first
|
88
|
+
value = file.values.first
|
89
|
+
@signatures[name] = md5(value)
|
90
|
+
|
91
|
+
@attachments[name] = {
|
92
|
+
"data" => value,
|
93
|
+
"content_type" => MIMES[name.split('.').last]
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
doc = @db.get(docid) rescue nil
|
98
|
+
|
99
|
+
unless doc
|
100
|
+
say "creating #{docid}"
|
101
|
+
@db.save({"_id" => docid, "_attachments" => @attachments, "signatures" => @signatures})
|
102
|
+
return
|
103
|
+
end
|
104
|
+
|
105
|
+
doc["signatures"] ||= {}
|
106
|
+
doc["_attachments"] ||= {}
|
107
|
+
# remove deleted docs
|
108
|
+
to_be_removed = doc["signatures"].keys.select do |d|
|
109
|
+
!pushfiles.collect{|p| p.keys.first}.include?(d)
|
110
|
+
end
|
111
|
+
|
112
|
+
to_be_removed.each do |p|
|
113
|
+
say "deleting #{p}"
|
114
|
+
doc["signatures"].delete(p)
|
115
|
+
doc["_attachments"].delete(p)
|
116
|
+
end
|
117
|
+
|
118
|
+
# update existing docs:
|
119
|
+
doc["signatures"].each do |path, sig|
|
120
|
+
if (@signatures[path] == sig)
|
121
|
+
say "no change to #{path}. skipping..."
|
122
|
+
else
|
123
|
+
say "replacing #{path}"
|
124
|
+
doc["signatures"][path] = md5(@attachments[path]["data"])
|
125
|
+
doc["_attachments"][path].delete("stub")
|
126
|
+
doc["_attachments"][path].delete("length")
|
127
|
+
doc["_attachments"][path]["data"] = @attachments[path]["data"]
|
128
|
+
doc["_attachments"][path].merge!({"data" => @attachments[path]["data"]} )
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# add in new files
|
133
|
+
new_files = pushfiles.select{|d| !doc["signatures"].keys.include?( d.keys.first) }
|
134
|
+
|
135
|
+
new_files.each do |f|
|
136
|
+
say "creating #{f}"
|
137
|
+
path = f.keys.first
|
138
|
+
content = f.values.first
|
139
|
+
doc["signatures"][path] = md5(content)
|
140
|
+
|
141
|
+
doc["_attachments"][path] = {
|
142
|
+
"data" => content,
|
143
|
+
"content_type" => MIMES[path.split('.').last]
|
144
|
+
}
|
145
|
+
end
|
146
|
+
|
147
|
+
begin
|
148
|
+
@db.save(doc)
|
149
|
+
rescue Exception => e
|
150
|
+
say e.message
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def push_views(view_dir)
|
155
|
+
designs = {}
|
156
|
+
|
157
|
+
Dir["#{view_dir}/**/*.*"].each do |design_doc|
|
158
|
+
design_doc_parts = design_doc.split('/')
|
159
|
+
next if /^lib\..*$/.match design_doc_parts.last
|
160
|
+
pre_normalized_view_name = design_doc_parts.last.split("-")
|
161
|
+
view_name = pre_normalized_view_name[0..pre_normalized_view_name.length-2].join("-")
|
162
|
+
|
163
|
+
folder = design_doc_parts[-2]
|
164
|
+
|
165
|
+
designs[folder] ||= {}
|
166
|
+
designs[folder]["views"] ||= {}
|
167
|
+
design_lang = design_doc_parts.last.split(".").last
|
168
|
+
designs[folder]["language"] ||= LANGS[design_lang]
|
169
|
+
|
170
|
+
libs = ""
|
171
|
+
Dir["#{view_dir}/lib.#{design_lang}"].collect do |global_lib|
|
172
|
+
libs << open(global_lib).read
|
173
|
+
libs << "\n"
|
174
|
+
end
|
175
|
+
Dir["#{view_dir}/#{folder}/lib.#{design_lang}"].collect do |global_lib|
|
176
|
+
libs << open(global_lib).read
|
177
|
+
libs << "\n"
|
178
|
+
end
|
179
|
+
if design_doc_parts.last =~ /-map/
|
180
|
+
designs[folder]["views"][view_name] ||= {}
|
181
|
+
designs[folder]["views"][view_name]["map"] = read(design_doc, libs)
|
182
|
+
end
|
183
|
+
|
184
|
+
if design_doc_parts.last =~ /-reduce/
|
185
|
+
designs[folder]["views"][view_name] ||= {}
|
186
|
+
designs[folder]["views"][view_name]["reduce"] = read(design_doc, libs)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# cleanup empty maps and reduces
|
191
|
+
designs.each do |name, props|
|
192
|
+
props["views"].each do |view, funcs|
|
193
|
+
next unless view.include?("reduce")
|
194
|
+
props["views"].delete(view) unless funcs.keys.include?("reduce")
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
designs.each do |k,v|
|
199
|
+
create_or_update("_design/#{k}", v)
|
200
|
+
end
|
201
|
+
|
202
|
+
designs
|
203
|
+
end
|
204
|
+
|
205
|
+
private
|
206
|
+
|
207
|
+
def read(file, libs=nil)
|
208
|
+
st = open(file).read
|
209
|
+
st.sub!(/(\/\/|#)include-lib/,libs) if libs
|
210
|
+
st
|
211
|
+
end
|
212
|
+
|
213
|
+
def create_or_update(id, fields)
|
214
|
+
existing = @db.get(id) rescue nil
|
215
|
+
|
216
|
+
if existing
|
217
|
+
updated = existing.merge(fields)
|
218
|
+
if existing != updated
|
219
|
+
say "replacing #{id}"
|
220
|
+
db.save(updated)
|
221
|
+
else
|
222
|
+
say "skipping #{id}"
|
223
|
+
end
|
224
|
+
else
|
225
|
+
say "creating #{id}"
|
226
|
+
db.save(fields.merge({"_id" => id}))
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
|
231
|
+
|
232
|
+
def package_forms(funcs)
|
233
|
+
apply_lib(funcs)
|
234
|
+
end
|
235
|
+
|
236
|
+
def package_views(views)
|
237
|
+
views.each do |view, funcs|
|
238
|
+
apply_lib(funcs)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def apply_lib(funcs)
|
243
|
+
funcs.each do |k,v|
|
244
|
+
next unless v.is_a?(String)
|
245
|
+
funcs[k] = process_include(process_require(v))
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# process requires
|
250
|
+
def process_require(f_string)
|
251
|
+
f_string.gsub /(\/\/|#)\ ?!code (.*)/ do
|
252
|
+
fields = $2.split('.')
|
253
|
+
library = @doc
|
254
|
+
fields.each do |field|
|
255
|
+
library = library[field]
|
256
|
+
break unless library
|
257
|
+
end
|
258
|
+
library
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
|
263
|
+
def process_include(f_string)
|
264
|
+
|
265
|
+
# process includes
|
266
|
+
included = {}
|
267
|
+
f_string.gsub /(\/\/|#)\ ?!json (.*)/ do
|
268
|
+
fields = $2.split('.')
|
269
|
+
library = @doc
|
270
|
+
include_to = included
|
271
|
+
count = fields.length
|
272
|
+
fields.each_with_index do |field, i|
|
273
|
+
break unless library[field]
|
274
|
+
library = library[field]
|
275
|
+
# normal case
|
276
|
+
if i+1 < count
|
277
|
+
include_to[field] = include_to[field] || {}
|
278
|
+
include_to = include_to[field]
|
279
|
+
else
|
280
|
+
# last one
|
281
|
+
include_to[field] = library
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
end
|
286
|
+
# puts included.inspect
|
287
|
+
rval = if included == {}
|
288
|
+
f_string
|
289
|
+
else
|
290
|
+
varstrings = included.collect do |k, v|
|
291
|
+
"var #{k} = #{v.to_json};"
|
292
|
+
end
|
293
|
+
# just replace the first instance of the macro
|
294
|
+
f_string.sub /(\/\/|#)\ ?!json (.*)/, varstrings.join("\n")
|
295
|
+
end
|
296
|
+
|
297
|
+
rval
|
298
|
+
end
|
299
|
+
|
300
|
+
|
301
|
+
def say words
|
302
|
+
puts words if @loud
|
303
|
+
end
|
304
|
+
|
305
|
+
def md5 string
|
306
|
+
Digest::MD5.hexdigest(string)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module CouchRest
|
4
|
+
module Mixins
|
5
|
+
module DesignDoc
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def design_doc_id
|
13
|
+
"_design/#{design_doc_slug}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def design_doc_slug
|
17
|
+
return design_doc_slug_cache if design_doc_slug_cache && design_doc_fresh
|
18
|
+
funcs = []
|
19
|
+
design_doc['views'].each do |name, view|
|
20
|
+
funcs << "#{name}/#{view['map']}#{view['reduce']}"
|
21
|
+
end
|
22
|
+
md5 = Digest::MD5.hexdigest(funcs.sort.join(''))
|
23
|
+
self.design_doc_slug_cache = "#{self.to_s}-#{md5}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def default_design_doc
|
27
|
+
{
|
28
|
+
"language" => "javascript",
|
29
|
+
"views" => {
|
30
|
+
'all' => {
|
31
|
+
'map' => "function(doc) {
|
32
|
+
if (doc['couchrest-type'] == '#{self.to_s}') {
|
33
|
+
emit(null,null);
|
34
|
+
}
|
35
|
+
}"
|
36
|
+
}
|
37
|
+
}
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def refresh_design_doc
|
42
|
+
did = design_doc_id
|
43
|
+
saved = database.get(did) rescue nil
|
44
|
+
if saved
|
45
|
+
design_doc['views'].each do |name, view|
|
46
|
+
saved['views'][name] = view
|
47
|
+
end
|
48
|
+
database.save_doc(saved)
|
49
|
+
self.design_doc = saved
|
50
|
+
else
|
51
|
+
design_doc['_id'] = did
|
52
|
+
design_doc.delete('_rev')
|
53
|
+
design_doc.database = database
|
54
|
+
design_doc.save
|
55
|
+
end
|
56
|
+
self.design_doc_fresh = true
|
57
|
+
end
|
58
|
+
|
59
|
+
end # module ClassMethods
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Mixins
|
3
|
+
module DocumentQueries
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
# Load all documents that have the "couchrest-type" field equal to the
|
12
|
+
# name of the current class. Take the standard set of
|
13
|
+
# CouchRest::Database#view options.
|
14
|
+
def all(opts = {}, &block)
|
15
|
+
self.design_doc ||= Design.new(default_design_doc)
|
16
|
+
unless design_doc_fresh
|
17
|
+
refresh_design_doc
|
18
|
+
end
|
19
|
+
view :all, opts, &block
|
20
|
+
end
|
21
|
+
|
22
|
+
# Load the first document that have the "couchrest-type" field equal to
|
23
|
+
# the name of the current class.
|
24
|
+
#
|
25
|
+
# ==== Returns
|
26
|
+
# Object:: The first object instance available
|
27
|
+
# or
|
28
|
+
# Nil:: if no instances available
|
29
|
+
#
|
30
|
+
# ==== Parameters
|
31
|
+
# opts<Hash>::
|
32
|
+
# View options, see <tt>CouchRest::Database#view</tt> options for more info.
|
33
|
+
def first(opts = {})
|
34
|
+
first_instance = self.all(opts.merge!(:limit => 1))
|
35
|
+
first_instance.empty? ? nil : first_instance.first
|
36
|
+
end
|
37
|
+
|
38
|
+
# Load a document from the database by id
|
39
|
+
def get(id)
|
40
|
+
doc = database.get id
|
41
|
+
new(doc)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Mixins
|
3
|
+
module ExtendedViews
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
# extlib is required for the following code
|
8
|
+
base.send(:class_inheritable_accessor, :design_doc)
|
9
|
+
base.send(:class_inheritable_accessor, :design_doc_slug_cache)
|
10
|
+
base.send(:class_inheritable_accessor, :design_doc_fresh)
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
|
15
|
+
# Define a CouchDB view. The name of the view will be the concatenation
|
16
|
+
# of <tt>by</tt> and the keys joined by <tt>_and_</tt>
|
17
|
+
#
|
18
|
+
# ==== Example views:
|
19
|
+
#
|
20
|
+
# class Post
|
21
|
+
# # view with default options
|
22
|
+
# # query with Post.by_date
|
23
|
+
# view_by :date, :descending => true
|
24
|
+
#
|
25
|
+
# # view with compound sort-keys
|
26
|
+
# # query with Post.by_user_id_and_date
|
27
|
+
# view_by :user_id, :date
|
28
|
+
#
|
29
|
+
# # view with custom map/reduce functions
|
30
|
+
# # query with Post.by_tags :reduce => true
|
31
|
+
# view_by :tags,
|
32
|
+
# :map =>
|
33
|
+
# "function(doc) {
|
34
|
+
# if (doc['couchrest-type'] == 'Post' && doc.tags) {
|
35
|
+
# doc.tags.forEach(function(tag){
|
36
|
+
# emit(doc.tag, 1);
|
37
|
+
# });
|
38
|
+
# }
|
39
|
+
# }",
|
40
|
+
# :reduce =>
|
41
|
+
# "function(keys, values, rereduce) {
|
42
|
+
# return sum(values);
|
43
|
+
# }"
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# <tt>view_by :date</tt> will create a view defined by this Javascript
|
47
|
+
# function:
|
48
|
+
#
|
49
|
+
# function(doc) {
|
50
|
+
# if (doc['couchrest-type'] == 'Post' && doc.date) {
|
51
|
+
# emit(doc.date, null);
|
52
|
+
# }
|
53
|
+
# }
|
54
|
+
#
|
55
|
+
# It can be queried by calling <tt>Post.by_date</tt> which accepts all
|
56
|
+
# valid options for CouchRest::Database#view. In addition, calling with
|
57
|
+
# the <tt>:raw => true</tt> option will return the view rows
|
58
|
+
# themselves. By default <tt>Post.by_date</tt> will return the
|
59
|
+
# documents included in the generated view.
|
60
|
+
#
|
61
|
+
# CouchRest::Database#view options can be applied at view definition
|
62
|
+
# time as defaults, and they will be curried and used at view query
|
63
|
+
# time. Or they can be overridden at query time.
|
64
|
+
#
|
65
|
+
# Custom views can be queried with <tt>:reduce => true</tt> to return
|
66
|
+
# reduce results. The default for custom views is to query with
|
67
|
+
# <tt>:reduce => false</tt>.
|
68
|
+
#
|
69
|
+
# Views are generated (on a per-model basis) lazily on first-access.
|
70
|
+
# This means that if you are deploying changes to a view, the views for
|
71
|
+
# that model won't be available until generation is complete. This can
|
72
|
+
# take some time with large databases. Strategies are in the works.
|
73
|
+
#
|
74
|
+
# To understand the capabilities of this view system more compeletly,
|
75
|
+
# it is recommended that you read the RSpec file at
|
76
|
+
# <tt>spec/core/model_spec.rb</tt>.
|
77
|
+
|
78
|
+
def view_by(*keys)
|
79
|
+
self.design_doc ||= Design.new(default_design_doc)
|
80
|
+
opts = keys.pop if keys.last.is_a?(Hash)
|
81
|
+
opts ||= {}
|
82
|
+
ducktype = opts.delete(:ducktype)
|
83
|
+
unless ducktype || opts[:map]
|
84
|
+
opts[:guards] ||= []
|
85
|
+
opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')"
|
86
|
+
end
|
87
|
+
keys.push opts
|
88
|
+
self.design_doc.view_by(*keys)
|
89
|
+
self.design_doc_fresh = false
|
90
|
+
end
|
91
|
+
|
92
|
+
# returns stored defaults if the there is a view named this in the design doc
|
93
|
+
def has_view?(view)
|
94
|
+
view = view.to_s
|
95
|
+
design_doc && design_doc['views'] && design_doc['views'][view]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Dispatches to any named view.
|
99
|
+
def view name, query={}, &block
|
100
|
+
unless design_doc_fresh
|
101
|
+
refresh_design_doc
|
102
|
+
end
|
103
|
+
query[:raw] = true if query[:reduce]
|
104
|
+
raw = query.delete(:raw)
|
105
|
+
fetch_view_with_docs(name, query, raw, &block)
|
106
|
+
end
|
107
|
+
|
108
|
+
def all_design_doc_versions
|
109
|
+
database.documents :startkey => "_design/#{self.to_s}-",
|
110
|
+
:endkey => "_design/#{self.to_s}-\u9999"
|
111
|
+
end
|
112
|
+
|
113
|
+
# Deletes any non-current design docs that were created by this class.
|
114
|
+
# Running this when you're deployed version of your application is steadily
|
115
|
+
# and consistently using the latest code, is the way to clear out old design
|
116
|
+
# docs. Running it to early could mean that live code has to regenerate
|
117
|
+
# potentially large indexes.
|
118
|
+
def cleanup_design_docs!
|
119
|
+
ddocs = all_design_doc_versions
|
120
|
+
ddocs["rows"].each do |row|
|
121
|
+
if (row['id'] != design_doc_id)
|
122
|
+
database.delete_doc({
|
123
|
+
"_id" => row['id'],
|
124
|
+
"_rev" => row['value']['rev']
|
125
|
+
})
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def fetch_view_with_docs name, opts, raw=false, &block
|
133
|
+
if raw
|
134
|
+
fetch_view name, opts, &block
|
135
|
+
else
|
136
|
+
begin
|
137
|
+
view = fetch_view name, opts.merge({:include_docs => true}), &block
|
138
|
+
view['rows'].collect{|r|new(r['doc'])} if view['rows']
|
139
|
+
rescue
|
140
|
+
# fallback for old versions of couchdb that don't
|
141
|
+
# have include_docs support
|
142
|
+
view = fetch_view name, opts, &block
|
143
|
+
view['rows'].collect{|r|new(database.get(r['id']))} if view['rows']
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def fetch_view view_name, opts, &block
|
149
|
+
retryable = true
|
150
|
+
begin
|
151
|
+
design_doc.view(view_name, opts, &block)
|
152
|
+
# the design doc could have been deleted by a rouge process
|
153
|
+
rescue RestClient::ResourceNotFound => e
|
154
|
+
if retryable
|
155
|
+
refresh_design_doc
|
156
|
+
retryable = false
|
157
|
+
retry
|
158
|
+
else
|
159
|
+
raise e
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end # module ClassMethods
|
165
|
+
|
166
|
+
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Mixins
|
3
|
+
module DocumentProperties
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# Stores the class properties
|
11
|
+
def properties
|
12
|
+
@@properties ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
# This is not a thread safe operation, if you have to set new properties at runtime
|
16
|
+
# make sure to use a mutex.
|
17
|
+
def property(name, options={})
|
18
|
+
unless properties.map{|p| p.name}.include?(name.to_s)
|
19
|
+
property = CouchRest::Property.new(name, options.delete(:type), options)
|
20
|
+
create_property_getter(property)
|
21
|
+
create_property_setter(property) unless property.read_only == true
|
22
|
+
properties << property
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
# defines the getter for the property
|
28
|
+
def create_property_getter(property)
|
29
|
+
meth = property.name
|
30
|
+
class_eval <<-EOS
|
31
|
+
def #{meth}
|
32
|
+
self['#{meth}']
|
33
|
+
end
|
34
|
+
EOS
|
35
|
+
|
36
|
+
if property.alias
|
37
|
+
class_eval <<-EOS
|
38
|
+
alias #{property.alias.to_sym} #{meth.to_sym}
|
39
|
+
EOS
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# defines the setter for the property
|
44
|
+
def create_property_setter(property)
|
45
|
+
meth = property.name
|
46
|
+
class_eval <<-EOS
|
47
|
+
def #{meth}=(value)
|
48
|
+
self['#{meth}'] = value
|
49
|
+
end
|
50
|
+
EOS
|
51
|
+
|
52
|
+
if property.alias
|
53
|
+
class_eval <<-EOS
|
54
|
+
alias #{property.alias.to_sym}= #{meth.to_sym}=
|
55
|
+
EOS
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end # module ClassMethods
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Mixins
|
3
|
+
module Views
|
4
|
+
|
5
|
+
# alias for self['_id']
|
6
|
+
def id
|
7
|
+
self['_id']
|
8
|
+
end
|
9
|
+
|
10
|
+
# alias for self['_rev']
|
11
|
+
def rev
|
12
|
+
self['_rev']
|
13
|
+
end
|
14
|
+
|
15
|
+
# returns true if the document has never been saved
|
16
|
+
def new_document?
|
17
|
+
!rev
|
18
|
+
end
|
19
|
+
|
20
|
+
# Saves the document to the db using create or update. Also runs the :save
|
21
|
+
# callbacks. Sets the <tt>_id</tt> and <tt>_rev</tt> fields based on
|
22
|
+
# CouchDB's response.
|
23
|
+
# If <tt>bulk</tt> is <tt>true</tt> (defaults to false) the document is cached for bulk save.
|
24
|
+
def save(bulk = false)
|
25
|
+
raise ArgumentError, "doc.database required for saving" unless database
|
26
|
+
result = database.save_doc self, bulk
|
27
|
+
result['ok']
|
28
|
+
end
|
29
|
+
|
30
|
+
# Deletes the document from the database. Runs the :delete callbacks.
|
31
|
+
# Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
|
32
|
+
# document to be saved to a new <tt>_id</tt>.
|
33
|
+
# If <tt>bulk</tt> is <tt>true</tt> (defaults to false) the document won't
|
34
|
+
# actually be deleted from the db until bulk save.
|
35
|
+
def destroy(bulk = false)
|
36
|
+
raise ArgumentError, "doc.database required to destroy" unless database
|
37
|
+
result = database.delete_doc(self, bulk)
|
38
|
+
if result['ok']
|
39
|
+
self['_rev'] = nil
|
40
|
+
self['_id'] = nil
|
41
|
+
end
|
42
|
+
result['ok']
|
43
|
+
end
|
44
|
+
|
45
|
+
def copy(dest)
|
46
|
+
raise ArgumentError, "doc.database required to copy" unless database
|
47
|
+
result = database.copy_doc(self, dest)
|
48
|
+
result['ok']
|
49
|
+
end
|
50
|
+
|
51
|
+
def move(dest)
|
52
|
+
raise ArgumentError, "doc.database required to copy" unless database
|
53
|
+
result = database.move_doc(self, dest)
|
54
|
+
result['ok']
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
begin
|
3
|
+
gem 'extlib'
|
4
|
+
require 'extlib'
|
5
|
+
rescue
|
6
|
+
puts "CouchRest::Model requires extlib. This is left out of the gemspec on purpose."
|
7
|
+
raise
|
8
|
+
end
|
9
|
+
require 'mime/types'
|
10
|
+
require File.join(File.dirname(__FILE__), "property")
|
11
|
+
require File.join(File.dirname(__FILE__), '..', 'mixins', 'extended_document_mixins')
|
12
|
+
|
13
|
+
module CouchRest
|
14
|
+
|
15
|
+
# Same as CouchRest::Document but with properties and validations
|
16
|
+
class ExtendedDocument < Document
|
17
|
+
include CouchRest::Mixins::DocumentQueries
|
18
|
+
include CouchRest::Mixins::DocumentProperties
|
19
|
+
include CouchRest::Mixins::ExtendedViews
|
20
|
+
include CouchRest::Mixins::DesignDoc
|
21
|
+
|
22
|
+
|
23
|
+
# Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
|
24
|
+
# on the document whenever saving occurs. CouchRest uses a pretty
|
25
|
+
# decent time format by default. See Time#to_json
|
26
|
+
def self.timestamps!
|
27
|
+
before(:save) do
|
28
|
+
self['updated_at'] = Time.now
|
29
|
+
self['created_at'] = self['updated_at'] if new_document?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Name a method that will be called before the document is first saved,
|
34
|
+
# which returns a string to be used for the document's <tt>_id</tt>.
|
35
|
+
# Because CouchDB enforces a constraint that each id must be unique,
|
36
|
+
# this can be used to enforce eg: uniq usernames. Note that this id
|
37
|
+
# must be globally unique across all document types which share a
|
38
|
+
# database, so if you'd like to scope uniqueness to this class, you
|
39
|
+
# should use the class name as part of the unique id.
|
40
|
+
def self.unique_id method = nil, &block
|
41
|
+
if method
|
42
|
+
define_method :set_unique_id do
|
43
|
+
self['_id'] ||= self.send(method)
|
44
|
+
end
|
45
|
+
elsif block
|
46
|
+
define_method :set_unique_id do
|
47
|
+
uniqid = block.call(self)
|
48
|
+
raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
|
49
|
+
self['_id'] ||= uniqid
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
### instance methods
|
55
|
+
|
56
|
+
# Returns the Class properties
|
57
|
+
#
|
58
|
+
# ==== Returns
|
59
|
+
# Array:: the list of properties for the instance
|
60
|
+
def properties
|
61
|
+
self.class.properties
|
62
|
+
end
|
63
|
+
|
64
|
+
# Takes a hash as argument, and applies the values by using writer methods
|
65
|
+
# for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
|
66
|
+
# missing. In case of error, no attributes are changed.
|
67
|
+
def update_attributes_without_saving(hash)
|
68
|
+
hash.each do |k, v|
|
69
|
+
raise NoMethodError, "#{k}= method not available, use key_accessor or key_writer :#{k}" unless self.respond_to?("#{k}=")
|
70
|
+
end
|
71
|
+
hash.each do |k, v|
|
72
|
+
self.send("#{k}=",v)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Takes a hash as argument, and applies the values by using writer methods
|
77
|
+
# for each key. Raises a NoMethodError if the corresponding methods are
|
78
|
+
# missing. In case of error, no attributes are changed.
|
79
|
+
def update_attributes(hash)
|
80
|
+
update_attributes_without_saving hash
|
81
|
+
save
|
82
|
+
end
|
83
|
+
|
84
|
+
# for compatibility with old-school frameworks
|
85
|
+
alias :new_record? :new_document?
|
86
|
+
|
87
|
+
# Overridden to set the unique ID.
|
88
|
+
# Returns a boolean value
|
89
|
+
def save(bulk = false)
|
90
|
+
set_unique_id if new_document? && self.respond_to?(:set_unique_id)
|
91
|
+
result = database.save_doc(self, bulk)
|
92
|
+
result["ok"] == true
|
93
|
+
end
|
94
|
+
|
95
|
+
# Saves the document to the db using create or update. Raises an exception
|
96
|
+
# if the document is not saved properly.
|
97
|
+
def save!
|
98
|
+
raise "#{self.inspect} failed to save" unless self.save
|
99
|
+
end
|
100
|
+
|
101
|
+
# Deletes the document from the database. Runs the :destroy callbacks.
|
102
|
+
# Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
|
103
|
+
# document to be saved to a new <tt>_id</tt>.
|
104
|
+
def destroy
|
105
|
+
result = database.delete_doc self
|
106
|
+
if result['ok']
|
107
|
+
self['_rev'] = nil
|
108
|
+
self['_id'] = nil
|
109
|
+
end
|
110
|
+
result['ok']
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module CouchRest
|
2
|
+
|
3
|
+
# Basic attribute support adding getter/setter + validation
|
4
|
+
class Property
|
5
|
+
attr_reader :name, :type, :validation_format, :required, :read_only, :alias
|
6
|
+
|
7
|
+
# attribute to define
|
8
|
+
def initialize(name, type = String, options = {})
|
9
|
+
@name = name.to_s
|
10
|
+
@type = type
|
11
|
+
parse_options(options)
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
private
|
17
|
+
def parse_options(options)
|
18
|
+
return if options.empty?
|
19
|
+
@required = true if (options[:required] && (options[:required] == true))
|
20
|
+
@validation_format = options[:format] if options[:format]
|
21
|
+
@read_only = options[:read_only] if options[:read_only]
|
22
|
+
@alias = options[:alias] if options
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -101,6 +101,10 @@ describe CouchRest::Database do
|
|
101
101
|
rs = @db.view('first/test')
|
102
102
|
rs['rows'].select{|r|r['key'] == 'wild' && r['value'] == 'and random'}.length.should == 1
|
103
103
|
end
|
104
|
+
it "should return the raw result" do
|
105
|
+
rs = @db.view('first/test', :raw => true)
|
106
|
+
rs.should be_kind_of(String)
|
107
|
+
end
|
104
108
|
it "should work with a range" do
|
105
109
|
rs = @db.view('first/test', :startkey => "b", :endkey => "z")
|
106
110
|
rs['rows'].length.should == 2
|
@@ -117,6 +121,14 @@ describe CouchRest::Database do
|
|
117
121
|
rs = @db.view('first/test', :keys => ["another", "wild"])
|
118
122
|
rs['rows'].length.should == 2
|
119
123
|
end
|
124
|
+
it "should work with multi-keys in raw mode" do
|
125
|
+
rs = @db.view('first/test', :keys => ["another", "wild"], :raw => true)
|
126
|
+
rs.should be_kind_of(String)
|
127
|
+
end
|
128
|
+
it "should work with stripes" do
|
129
|
+
rs = @db.view('first/test', :stripes => [{:startkey => "another", :endkey => "anothes"}, {:startkey => "wild", :endkey => "wile"}])
|
130
|
+
rs['rows'].length.should == 2
|
131
|
+
end
|
120
132
|
it "should accept a block" do
|
121
133
|
rows = []
|
122
134
|
rs = @db.view('first/test', :include_docs => true) do |row|
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../../spec_helper'
|
2
|
+
|
3
|
+
describe CouchRest::Server do
|
4
|
+
|
5
|
+
before(:all) do
|
6
|
+
@couch = CouchRest::Server.new
|
7
|
+
end
|
8
|
+
|
9
|
+
after(:all) do
|
10
|
+
@couch.available_databases.each do |ref, db|
|
11
|
+
db.delete!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "available databases" do
|
16
|
+
it "should let you add more databases" do
|
17
|
+
@couch.available_databases.should be_empty
|
18
|
+
@couch.define_available_database(:default, "cr-server-test-db")
|
19
|
+
@couch.available_databases.keys.should include(:default)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should verify that a database is available" do
|
23
|
+
@couch.available_database?(:default).should be_true
|
24
|
+
@couch.available_database?("cr-server-test-db").should be_true
|
25
|
+
@couch.available_database?(:matt).should be_false
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should let you set a default database" do
|
29
|
+
@couch.default_database = 'cr-server-test-default-db'
|
30
|
+
@couch.available_database?(:default).should be_true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
|
2
|
+
|
3
|
+
# check the following file to see how to use the spec'd features.
|
4
|
+
require File.join(FIXTURE_PATH, 'more', 'card')
|
5
|
+
|
6
|
+
describe "ExtendedDocument properties" do
|
7
|
+
|
8
|
+
before(:each) do
|
9
|
+
@card = Card.new(:first_name => "matt")
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should be accessible from the object" do
|
13
|
+
@card.properties.should be_an_instance_of(Array)
|
14
|
+
@card.properties.map{|p| p.name}.should include("first_name")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should let you access a property value (getter)" do
|
18
|
+
@card.first_name.should == "matt"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should let you set a property value (setter)" do
|
22
|
+
@card.last_name = "Aimonetti"
|
23
|
+
@card.last_name.should == "Aimonetti"
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should not let you set a property value if it's read only" do
|
27
|
+
lambda{@card.read_only_value = "test"}.should raise_error
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should let you use an alias for an attribute" do
|
31
|
+
@card.last_name = "Aimonetti"
|
32
|
+
@card.family_name.should == "Aimonetti"
|
33
|
+
@card.family_name.should == @card.last_name
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: derfred-couchrest
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.12.6
|
4
|
+
version: 0.12.6.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- J. Chris Anderson
|
@@ -14,6 +14,7 @@ default_executable:
|
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: json
|
17
|
+
type: :runtime
|
17
18
|
version_requirement:
|
18
19
|
version_requirements: !ruby/object:Gem::Requirement
|
19
20
|
requirements:
|
@@ -23,6 +24,7 @@ dependencies:
|
|
23
24
|
version:
|
24
25
|
- !ruby/object:Gem::Dependency
|
25
26
|
name: rest-client
|
27
|
+
type: :runtime
|
26
28
|
version_requirement:
|
27
29
|
version_requirements: !ruby/object:Gem::Requirement
|
28
30
|
requirements:
|
@@ -32,6 +34,7 @@ dependencies:
|
|
32
34
|
version:
|
33
35
|
- !ruby/object:Gem::Dependency
|
34
36
|
name: mime-types
|
37
|
+
type: :runtime
|
35
38
|
version_requirement:
|
36
39
|
version_requirements: !ruby/object:Gem::Requirement
|
37
40
|
requirements:
|
@@ -83,9 +86,21 @@ files:
|
|
83
86
|
- lib/couchrest/core/server.rb
|
84
87
|
- lib/couchrest/core/view.rb
|
85
88
|
- lib/couchrest/helper
|
89
|
+
- lib/couchrest/helper/file_manager.rb
|
86
90
|
- lib/couchrest/helper/pager.rb
|
87
91
|
- lib/couchrest/helper/streamer.rb
|
92
|
+
- lib/couchrest/mixins
|
93
|
+
- lib/couchrest/mixins/design_doc.rb
|
94
|
+
- lib/couchrest/mixins/document_queries.rb
|
95
|
+
- lib/couchrest/mixins/extended_document_mixins.rb
|
96
|
+
- lib/couchrest/mixins/extended_views.rb
|
97
|
+
- lib/couchrest/mixins/properties.rb
|
98
|
+
- lib/couchrest/mixins/views.rb
|
99
|
+
- lib/couchrest/mixins.rb
|
88
100
|
- lib/couchrest/monkeypatches.rb
|
101
|
+
- lib/couchrest/more
|
102
|
+
- lib/couchrest/more/extended_document.rb
|
103
|
+
- lib/couchrest/more/property.rb
|
89
104
|
- lib/couchrest.rb
|
90
105
|
- spec/couchrest
|
91
106
|
- spec/couchrest/core
|
@@ -94,14 +109,19 @@ files:
|
|
94
109
|
- spec/couchrest/core/design_spec.rb
|
95
110
|
- spec/couchrest/core/document_spec.rb
|
96
111
|
- spec/couchrest/core/model_spec.rb
|
112
|
+
- spec/couchrest/core/server_spec.rb
|
97
113
|
- spec/couchrest/helpers
|
98
114
|
- spec/couchrest/helpers/pager_spec.rb
|
99
115
|
- spec/couchrest/helpers/streamer_spec.rb
|
116
|
+
- spec/couchrest/more
|
117
|
+
- spec/couchrest/more/property_spec.rb
|
100
118
|
- spec/fixtures
|
101
119
|
- spec/fixtures/attachments
|
102
120
|
- spec/fixtures/attachments/couchdb.png
|
103
121
|
- spec/fixtures/attachments/README
|
104
122
|
- spec/fixtures/attachments/test.html
|
123
|
+
- spec/fixtures/more
|
124
|
+
- spec/fixtures/more/card.rb
|
105
125
|
- spec/fixtures/views
|
106
126
|
- spec/fixtures/views/lib.js
|
107
127
|
- spec/fixtures/views/test_view
|