derfred-couchrest 0.12.6 → 0.12.6.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/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
|