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.
@@ -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