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.
@@ -25,7 +25,7 @@ require 'couchrest/monkeypatches'
25
25
 
26
26
  # = CouchDB, close to the metal
27
27
  module CouchRest
28
- VERSION = '0.12.6'
28
+ VERSION = '0.12.6.3'
29
29
 
30
30
  autoload :Server, 'couchrest/core/server'
31
31
  autoload :Database, 'couchrest/core/database'
@@ -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
- if keys
66
- CouchRest.post(url, {:keys => keys})
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
- CouchRest.get url
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,3 @@
1
+ mixins_dir = File.join(File.dirname(__FILE__), 'mixins')
2
+
3
+ require File.join(mixins_dir, 'views')
@@ -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,4 @@
1
+ require File.join(File.dirname(__FILE__), 'properties')
2
+ require File.join(File.dirname(__FILE__), 'document_queries')
3
+ require File.join(File.dirname(__FILE__), 'extended_views')
4
+ require File.join(File.dirname(__FILE__), 'design_doc')
@@ -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
@@ -0,0 +1,7 @@
1
+ class Card < CouchRest::ExtendedDocument
2
+ use_database TEST_SERVER.default_database
3
+ property :first_name
4
+ property :last_name, :alias => :family_name
5
+ property :read_only_value, :read_only => true
6
+
7
+ 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