jchris-couchrest 0.9.8 → 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -48,4 +48,9 @@ Creating and Querying Views:
48
48
  })
49
49
  puts @db.view('first/test')['rows'].inspect
50
50
 
51
+ == CouchRest::Model
51
52
 
53
+ CouchRest::Model is a module designed along the lines of DataMapper::Resource. By
54
+ including it in your class, suddenly you get all sorts of magic sugar, so that
55
+ working with CouchDB in your Rails or Merb app is no harder than working with the
56
+ standard SQL alternatives. See the CouchRest::Model documentation for and example article class that illustrates usage.
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require 'spec/rake/spectask'
5
5
 
6
6
  spec = Gem::Specification.new do |s|
7
7
  s.name = "couchrest"
8
- s.version = "0.9.8"
8
+ s.version = "0.9.9"
9
9
  s.date = "2008-09-11"
10
10
  s.summary = "Lean and RESTful interface to CouchDB."
11
11
  s.email = "jchris@grabb.it"
@@ -22,42 +22,41 @@ spec = Gem::Specification.new do |s|
22
22
  s.executables << 'couchapp'
23
23
  s.add_dependency("json", ">= 1.1.2")
24
24
  s.add_dependency("rest-client", ">= 0.5")
25
+ s.add_dependency("extlib", ">= 0.9.6")
25
26
  end
26
27
 
27
- namespace :github do # thanks merb!
28
- desc "Update Github Gemspec"
29
- task :update_gemspec do
30
- skip_fields = %w(new_platform original_platform)
31
- integer_fields = %w(specification_version)
28
+ desc "Update Github Gemspec"
29
+ task :gemspec do
30
+ skip_fields = %w(new_platform original_platform)
31
+ integer_fields = %w(specification_version)
32
32
 
33
- result = "Gem::Specification.new do |s|\n"
34
- spec.instance_variables.each do |ivar|
35
- value = spec.instance_variable_get(ivar)
36
- name = ivar.split("@").last
37
- next if skip_fields.include?(name) || value.nil? || value == "" || (value.respond_to?(:empty?) && value.empty?)
38
- if name == "dependencies"
39
- value.each do |d|
40
- dep, *ver = d.to_s.split(" ")
41
- result << " s.add_dependency #{dep.inspect}, [#{ /\(([^\,]*)/ . match(ver.join(" "))[1].inspect}]\n"
42
- end
43
- else
44
- case value
45
- when Array
46
- value = name != "files" ? value.inspect : value.inspect.split(",").join(",\n")
47
- when Fixnum
48
- # leave as-is
49
- when String
50
- value = value.to_i if integer_fields.include?(name)
51
- value = value.inspect
52
- else
53
- value = value.to_s.inspect
54
- end
55
- result << " s.#{name} = #{value}\n"
33
+ result = "Gem::Specification.new do |s|\n"
34
+ spec.instance_variables.each do |ivar|
35
+ value = spec.instance_variable_get(ivar)
36
+ name = ivar.split("@").last
37
+ next if skip_fields.include?(name) || value.nil? || value == "" || (value.respond_to?(:empty?) && value.empty?)
38
+ if name == "dependencies"
39
+ value.each do |d|
40
+ dep, *ver = d.to_s.split(" ")
41
+ result << " s.add_dependency #{dep.inspect}, [#{ /\(([^\,]*)/ . match(ver.join(" "))[1].inspect}]\n"
56
42
  end
43
+ else
44
+ case value
45
+ when Array
46
+ value = name != "files" ? value.inspect : value.inspect.split(",").join(",\n")
47
+ when Fixnum
48
+ # leave as-is
49
+ when String
50
+ value = value.to_i if integer_fields.include?(name)
51
+ value = value.inspect
52
+ else
53
+ value = value.to_s.inspect
54
+ end
55
+ result << " s.#{name} = #{value}\n"
57
56
  end
58
- result << "end"
59
- File.open(File.join(File.dirname(__FILE__), "#{spec.name}.gemspec"), "w"){|f| f << result}
60
57
  end
58
+ result << "end"
59
+ File.open(File.join(File.dirname(__FILE__), "#{spec.name}.gemspec"), "w"){|f| f << result}
61
60
  end
62
61
 
63
62
  desc "Run all specs"
@@ -67,7 +66,7 @@ end
67
66
 
68
67
  desc "Print specdocs"
69
68
  Spec::Rake::SpecTask.new(:doc) do |t|
70
- t.spec_opts = ["--format", "specdoc", "--dry-run"]
69
+ t.spec_opts = ["--format", "specdoc"]
71
70
  t.spec_files = FileList['spec/*_spec.rb']
72
71
  end
73
72
 
@@ -80,8 +79,4 @@ Rake::RDocTask.new do |rdoc|
80
79
  end
81
80
 
82
81
  desc "Generate the gemspec"
83
-
84
-
85
-
86
-
87
82
  task :default => :spec
@@ -5,6 +5,12 @@ module CouchRest
5
5
  class Database
6
6
  attr_reader :server, :host, :name, :root
7
7
 
8
+ # Create a CouchRest::Database adapter for the supplied CouchRest::Server and database name.
9
+ #
10
+ # ==== Parameters
11
+ # server<CouchRest::Server>:: database host
12
+ # name<String>:: database name
13
+ #
8
14
  def initialize server, name
9
15
  @name = name
10
16
  @server = server
@@ -12,52 +18,48 @@ module CouchRest
12
18
  @root = "#{host}/#{name}"
13
19
  end
14
20
 
21
+ # returns the database's uri
15
22
  def to_s
16
23
  @root
17
24
  end
18
25
 
26
+ # GET the database info from CouchDB
19
27
  def info
20
28
  CouchRest.get @root
21
29
  end
22
30
 
31
+ # Query the <tt>_all_docs</tt> view. Accepts all the same arguments as view.
23
32
  def documents params = nil
24
33
  url = CouchRest.paramify_url "#{@root}/_all_docs", params
25
34
  CouchRest.get url
26
35
  end
27
36
 
37
+ # POST a temporary view function to CouchDB for querying. This is not recommended, as you don't get any performance benefit from CouchDB's materialized views. Can be quite slow on large databases.
28
38
  def temp_view funcs, params = nil
29
39
  url = CouchRest.paramify_url "#{@root}/_temp_view", params
30
40
  JSON.parse(RestClient.post(url, funcs.to_json, {"Content-Type" => 'application/json'}))
31
41
  end
32
42
 
43
+ # Query a CouchDB view as defined by a <tt>_design</tt> document. Accepts paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi
33
44
  def view name, params = nil
34
45
  url = CouchRest.paramify_url "#{@root}/_view/#{name}", params
35
46
  CouchRest.get url
36
47
  end
37
-
38
- # experimental
39
- def search params = nil
40
- url = CouchRest.paramify_url "#{@root}/_search", params
41
- CouchRest.get url
42
- end
43
- # experimental
44
- def action action, params = nil
45
- url = CouchRest.paramify_url "#{@root}/_action/#{action}", params
46
- CouchRest.get url
47
- end
48
48
 
49
+ # GET a document from CouchDB, by id. Returns a Ruby Hash.
49
50
  def get id
50
51
  slug = CGI.escape(id)
51
52
  CouchRest.get "#{@root}/#{slug}"
52
53
  end
53
54
 
55
+ # GET an attachment directly from CouchDB
54
56
  def fetch_attachment doc, name
55
57
  doc = CGI.escape(doc)
56
58
  name = CGI.escape(name)
57
59
  RestClient.get "#{@root}/#{doc}/#{name}"
58
60
  end
59
61
 
60
- # PUT or POST depending on presence of _id attribute
62
+ # Save a document to CouchDB. This will use the <tt>_id</tt> field from the document as the id for PUT, or request a new UUID from CouchDB, if no <tt>_id</tt> is present on the document. IDs are attached to documents on the client side because POST has the curious property of being automatically retried by proxies in the event of network segmentation and lost responses.
61
63
  def save doc
62
64
  if doc['_attachments']
63
65
  doc['_attachments'] = encode_attachments(doc['_attachments'])
@@ -75,6 +77,7 @@ module CouchRest
75
77
  end
76
78
  end
77
79
 
80
+ # POST an array of documents to CouchDB. If any of the documents are missing ids, supply one from the uuid cache.
78
81
  def bulk_save docs
79
82
  ids, noids = docs.partition{|d|d['_id']}
80
83
  uuid_count = [noids.length, @server.uuid_batch_count].max
@@ -85,11 +88,13 @@ module CouchRest
85
88
  CouchRest.post "#{@root}/_bulk_docs", {:docs => docs}
86
89
  end
87
90
 
91
+ # DELETE the document from CouchDB that has the given <tt>_id</tt> and <tt>_rev</tt>.
88
92
  def delete doc
89
93
  slug = CGI.escape(doc['_id'])
90
94
  CouchRest.delete "#{@root}/#{slug}?rev=#{doc['_rev']}"
91
95
  end
92
96
 
97
+ # DELETE the database itself. This is not undoable and could be rather catastrophic. Use with care!
93
98
  def delete!
94
99
  CouchRest.delete @root
95
100
  end
@@ -0,0 +1,352 @@
1
+ # = CouchRest::Model - ORM, the CouchDB way
2
+ module CouchRest
3
+ # = CouchRest::Model - ORM, the CouchDB way
4
+ #
5
+ # CouchRest::Model provides an ORM-like interface for CouchDB documents. It avoids all usage of <tt>method_missing</tt>, and tries to strike a balance between usability and magic. See CouchRest::Model::MagicViews#view_by for documentation about the view-generation system. For the other class methods, inspiried by DataMapper and ActiveRecord, see CouchRest::Model::ClassMethods. The InstanceMethods are pretty basic.
6
+ #
7
+ # ==== Example
8
+ #
9
+ # This is an example class using CouchRest::Model. It is taken from the spec/couchrest/core/model_spec.rb file, which may be even more up to date than this example.
10
+ #
11
+ # class Article
12
+ # include CouchRest::Model
13
+ # use_database CouchRest.database!('http://localhost:5984/couchrest-model-test')
14
+ # unique_id :slug
15
+ #
16
+ # view_by :date, :descending => true
17
+ # view_by :user_id, :date
18
+ #
19
+ # view_by :tags,
20
+ # :map =>
21
+ # "function(doc) {
22
+ # if (doc.type == 'Article' && doc.tags) {
23
+ # doc.tags.forEach(function(tag){
24
+ # emit(tag, 1);
25
+ # });
26
+ # }
27
+ # }",
28
+ # :reduce =>
29
+ # "function(keys, values, rereduce) {
30
+ # return sum(values);
31
+ # }"
32
+ #
33
+ # key_writer :date
34
+ # key_reader :slug, :created_at, :updated_at
35
+ # key_accessor :title, :tags
36
+ #
37
+ # timestamps!
38
+ #
39
+ # before(:create, :generate_slug_from_title)
40
+ # def generate_slug_from_title
41
+ # doc['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
42
+ # end
43
+ # end
44
+ module Model
45
+ class << self
46
+ # this is the CouchRest::Database that model classes will use unless they override it with <tt>use_database</tt>
47
+ attr_accessor :default_database
48
+ end
49
+
50
+ # instance methods on the model classes
51
+ module InstanceMethods
52
+ attr_accessor :doc
53
+
54
+ def initialize keys = {}
55
+ self.doc = {}
56
+ keys.each do |k,v|
57
+ doc[k.to_s] = v
58
+ end
59
+ unless doc['_id'] && doc['_rev']
60
+ init_doc
61
+ end
62
+ end
63
+
64
+ # returns the database used by this model's class
65
+ def database
66
+ self.class.database
67
+ end
68
+
69
+ # alias for doc['_id']
70
+ def id
71
+ doc['_id']
72
+ end
73
+
74
+ # alias for doc['_rev']
75
+ def rev
76
+ doc['_rev']
77
+ end
78
+
79
+ # returns true if the doc has never been saved
80
+ def new_record?
81
+ !doc['_rev']
82
+ end
83
+
84
+ # save the doc to the db using create or update
85
+ def save
86
+ if new_record?
87
+ create
88
+ else
89
+ update
90
+ end
91
+ end
92
+
93
+ protected
94
+
95
+ def create
96
+ set_unique_id if respond_to?(:set_unique_id) # hack
97
+ save_doc
98
+ end
99
+
100
+ def update
101
+ save_doc
102
+ end
103
+
104
+ private
105
+
106
+ def save_doc
107
+ result = database.save doc
108
+ if result['ok']
109
+ doc['_id'] = result['id']
110
+ doc['_rev'] = result['rev']
111
+ end
112
+ result['ok']
113
+ end
114
+
115
+ def init_doc
116
+ doc['type'] = self.class.to_s
117
+ end
118
+ end # module InstanceMethods
119
+
120
+ # Class methods for models that include CouchRest::Model
121
+ module ClassMethods
122
+ # override the CouchRest::Model-wide default_database
123
+ def use_database db
124
+ @database = db
125
+ end
126
+
127
+ # returns the CouchRest::Database instance that this class uses
128
+ def database
129
+ @database || CouchRest::Model.default_database
130
+ end
131
+
132
+ # load a document from the database
133
+ def get id
134
+ doc = database.get id
135
+ new(doc)
136
+ end
137
+
138
+ # Defines methods for reading and writing from fields in the document. Uses key_writer and key_reader internally.
139
+ def key_accessor *keys
140
+ key_writer *keys
141
+ key_reader *keys
142
+ end
143
+
144
+ # For each argument key, define a method <tt>key=</tt> that sets the corresponding field on the CouchDB document.
145
+ def key_writer *keys
146
+ keys.each do |method|
147
+ key = method.to_s
148
+ define_method "#{method}=" do |value|
149
+ doc[key] = value
150
+ end
151
+ end
152
+ end
153
+
154
+ # For each argument key, define a method <tt>key</tt> that reads the corresponding field on the CouchDB document.
155
+ def key_reader *keys
156
+ keys.each do |method|
157
+ key = method.to_s
158
+ define_method method do
159
+ doc[key]
160
+ end
161
+ end
162
+ end
163
+
164
+ # Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields on the document whenever saving occurs. CouchRest uses a pretty decent time format by default. See Time#to_json
165
+ def timestamps!
166
+ before(:create) do
167
+ doc['updated_at'] = doc['created_at'] = Time.now
168
+ end
169
+ before(:update) do
170
+ doc['updated_at'] = Time.now
171
+ end
172
+ end
173
+
174
+ # Name a method that will be called before the document is first saved, which returns a string to be used for the document's <tt>_id</tt>. Because CouchDB enforces a constraint that each id must be unique, this can be used to enforce eg: uniq usernames. Note that this id must be globally unique across all document types which share a database, so if you'd like to scope uniqueness to this class, you should use the class name as part of the unique id.
175
+ def unique_id method
176
+ define_method :set_unique_id do
177
+ doc['_id'] ||= self.send(method)
178
+ end
179
+ end
180
+
181
+ end # module ClassMethods
182
+
183
+ module MagicViews
184
+
185
+ # Define a CouchDB view. The name of the view will be the concatenation of <tt>by</tt> and the keys joined by <tt>_and_</tt>
186
+ #
187
+ # ==== Example views:
188
+ #
189
+ # class Post
190
+ # # view with default options
191
+ # # query with Post.by_date
192
+ # view_by :date, :descending => true
193
+ #
194
+ # # view with compound sort-keys
195
+ # # query with Post.by_user_id_and_date
196
+ # view_by :user_id, :date
197
+ #
198
+ # # view with custom map/reduce functions
199
+ # # query with Post.by_tags :reduce => true
200
+ # view_by :tags,
201
+ # :map =>
202
+ # "function(doc) {
203
+ # if (doc.type == 'Post' && doc.tags) {
204
+ # doc.tags.forEach(function(tag){
205
+ # emit(doc.tag, 1);
206
+ # });
207
+ # }
208
+ # }",
209
+ # :reduce =>
210
+ # "function(keys, values, rereduce) {
211
+ # return sum(values);
212
+ # }"
213
+ # end
214
+ #
215
+ # <tt>view_by :date</tt> will create a view defined by this Javascript function:
216
+ #
217
+ # function(doc) {
218
+ # if (doc.type == 'Post' && doc.date) {
219
+ # emit(doc.date, null);
220
+ # }
221
+ # }
222
+ #
223
+ # It can be queried by calling <tt>Post.by_date</tt> which accepts all valid options for CouchRest::Database#view. In addition, calling with the <tt>:raw => true</tt> option will return the view rows themselves. By default <tt>Post.by_date</tt> will return the documents included in the generated view.
224
+ #
225
+ # CouchRest::Database#view options can be applied at view definition time as defaults, and they will be curried and used at view query time. Or they can be overridden at query time.
226
+ #
227
+ # Custom views can be queried with <tt>:reduce => true</tt> to return reduce results. The default for custom views is to query with <tt>:reduce => false</tt>.
228
+ #
229
+ # Views are generated (on a per-model basis) lazily on first-access. This means that if you are deploying changes to a view, the views for that model won't be available until generation is complete. This can take some time with large databases. Strategies are in the works.
230
+ #
231
+ # To understand the capabilities of this view system more compeletly, it is recommended that you read the RSpec file at <tt>spec/core/model.rb</tt>.
232
+ def view_by *keys
233
+ opts = keys.pop if keys.last.is_a?(Hash)
234
+ opts ||= {}
235
+ type = self.to_s
236
+
237
+ method_name = "by_#{keys.join('_and_')}"
238
+ @@design_doc ||= default_design_doc
239
+
240
+ if opts[:map]
241
+ view = {}
242
+ view['map'] = opts.delete(:map)
243
+ if opts[:reduce]
244
+ view['reduce'] = opts.delete(:reduce)
245
+ opts[:reduce] = false
246
+ end
247
+ @@design_doc['views'][method_name] = view
248
+ else
249
+ doc_keys = keys.collect{|k|"doc['#{k}']"}
250
+ key_protection = doc_keys.join(' && ')
251
+ key_emit = doc_keys.length == 1 ? "#{doc_keys.first}" : "[#{doc_keys.join(', ')}]"
252
+ map_function = <<-JAVASCRIPT
253
+ function(doc) {
254
+ if (doc.type == '#{type}' && #{key_protection}) {
255
+ emit(#{key_emit}, null);
256
+ }
257
+ }
258
+ JAVASCRIPT
259
+ @@design_doc['views'][method_name] = {
260
+ 'map' => map_function
261
+ }
262
+ end
263
+
264
+ @@design_doc_fresh = false
265
+
266
+ self.meta_class.instance_eval do
267
+ define_method method_name do |*args|
268
+ query = opts.merge(args[0] || {})
269
+ query[:raw] = true if query[:reduce]
270
+ unless @@design_doc_fresh
271
+ refresh_design_doc
272
+ end
273
+ raw = query.delete(:raw)
274
+ view_name = "#{type}/#{method_name}"
275
+
276
+ view = fetch_view(view_name, query)
277
+ if raw
278
+ view
279
+ else
280
+ # TODO this can be optimized once the include-docs patch is applied
281
+ view['rows'].collect{|r|new(database.get(r['id']))}
282
+ end
283
+ end
284
+ end
285
+ end
286
+
287
+ private
288
+
289
+ def fetch_view view_name, opts
290
+ retryable = true
291
+ begin
292
+ database.view(view_name, opts)
293
+ # the design doc could have been deleted by a rouge process
294
+ rescue RestClient::ResourceNotFound => e
295
+ if retryable
296
+ refresh_design_doc
297
+ retryable = false
298
+ retry
299
+ else
300
+ raise e
301
+ end
302
+ end
303
+ end
304
+
305
+ def design_doc_id
306
+ "_design/#{self.to_s}"
307
+ end
308
+
309
+ def default_design_doc
310
+ {
311
+ "_id" => design_doc_id,
312
+ "language" => "javascript",
313
+ "views" => {}
314
+ }
315
+ end
316
+
317
+ def refresh_design_doc
318
+ saved = database.get(design_doc_id) rescue nil
319
+ if saved
320
+ @@design_doc['views'].each do |name, view|
321
+ saved['views'][name] = view
322
+ end
323
+ database.save(saved)
324
+ else
325
+ database.save(@@design_doc)
326
+ end
327
+ @@design_doc_fresh = true
328
+ end
329
+
330
+ end # module MagicViews
331
+
332
+ module Callbacks
333
+ def self.included(model)
334
+ model.class_eval <<-EOS, __FILE__, __LINE__
335
+ include Extlib::Hook
336
+ register_instance_hooks :save, :create, :update #, :destroy
337
+ EOS
338
+ end
339
+ end # module Callbacks
340
+
341
+ # bookkeeping section
342
+
343
+ # load the code into the model class
344
+ def self.included(model)
345
+ model.send(:include, InstanceMethods)
346
+ model.extend ClassMethods
347
+ model.extend MagicViews
348
+ model.send(:include, Callbacks)
349
+ end
350
+
351
+ end # module Model
352
+ end # module CouchRest
@@ -6,37 +6,39 @@ module CouchRest
6
6
  @uuid_batch_count = uuid_batch_count
7
7
  end
8
8
 
9
- # list all databases on the server
9
+ # List all databases on the server
10
10
  def databases
11
11
  CouchRest.get "#{@uri}/_all_dbs"
12
12
  end
13
13
 
14
+ # Returns a CouchRest::Database for the given name
14
15
  def database name
15
16
  CouchRest::Database.new(self, name)
16
17
  end
17
18
 
18
- # creates the database if it doesn't exist
19
+ # Creates the database if it doesn't exist
19
20
  def database! name
20
21
  create_db(name) rescue nil
21
22
  database name
22
23
  end
23
24
 
24
- # get the welcome message
25
+ # GET the welcome message
25
26
  def info
26
27
  CouchRest.get "#{@uri}/"
27
28
  end
28
29
 
29
- # create a database
30
+ # Create a database
30
31
  def create_db name
31
32
  CouchRest.put "#{@uri}/#{name}"
32
33
  database name
33
34
  end
34
35
 
35
- # restart the couchdb instance
36
+ # Restart the CouchDB instance
36
37
  def restart!
37
38
  CouchRest.post "#{@uri}/_restart"
38
39
  end
39
40
 
41
+ # Retrive an unused UUID from CouchDB. Server instances manage caching a list of unused UUIDs.
40
42
  def next_uuid count = @uuid_batch_count
41
43
  @uuids ||= []
42
44
  if @uuids.empty?
@@ -5,6 +5,7 @@ module CouchRest
5
5
  @db = db
6
6
  end
7
7
 
8
+ # Stream a view, yielding one row at a time. Shells out to <tt>curl</tt> to keep RAM usage low when you have millions of rows.
8
9
  def view name, params = nil
9
10
  urlst = /^_/.match(name) ? "#{@db.root}/#{name}" : "#{@db.root}/_view/#{name}"
10
11
  url = CouchRest.paramify_url urlst, params
@@ -1,18 +1,20 @@
1
-
2
- # this file must be loaded after the JSON gem
3
-
1
+ # This file must be loaded after the JSON gem and any other library that beats up the Time class.
4
2
  class Time
5
- # this date format sorts lexicographically
6
- # and is compatible with Javascript's new Date(time_string) constructor
7
- # note that sorting will break if you store times from multiple timezones
8
- # I like to add a ENV['TZ'] = 'UTC' to my apps
3
+ # This date format sorts lexicographically
4
+ # and is compatible with Javascript's <tt>new Date(time_string)</tt> constructor.
5
+ # Note this this format stores all dates in UTC so that collation
6
+ # order is preserved. (There's no longer a need to set <tt>ENV['TZ'] = 'UTC'</tt>
7
+ # in your application.)
9
8
 
10
9
  def to_json(options = nil)
11
- %("#{strftime("%Y/%m/%d %H:%M:%S %z")}")
10
+ u = self.utc
11
+ %("#{u.strftime("%Y/%m/%d %H:%M:%S +0000")}")
12
12
  end
13
13
 
14
- # this works to decode the outputted time format
15
- # copied from ActiveSupport
14
+ # Decodes the JSON time format to a UTC time.
15
+ # Based on Time.parse from ActiveSupport. ActiveSupport's version
16
+ # is more complete, returning a time in your current timezone,
17
+ # rather than keeping the time in UTC. YMMV.
16
18
  # def self.parse string, fallback=nil
17
19
  # d = DateTime.parse(string).new_offset
18
20
  # self.utc(d.year, d.month, d.day, d.hour, d.min, d.sec)
data/lib/couchrest.rb CHANGED
@@ -15,6 +15,7 @@
15
15
  require "rubygems"
16
16
  require 'json'
17
17
  require 'rest_client'
18
+ require 'extlib'
18
19
 
19
20
  $:.unshift File.dirname(__FILE__) unless
20
21
  $:.include?(File.dirname(__FILE__)) ||
@@ -23,9 +24,11 @@ $:.unshift File.dirname(__FILE__) unless
23
24
 
24
25
  require 'couchrest/monkeypatches'
25
26
 
27
+ # = CouchDB, close to the metal
26
28
  module CouchRest
27
29
  autoload :Server, 'couchrest/core/server'
28
30
  autoload :Database, 'couchrest/core/database'
31
+ autoload :Model, 'couchrest/core/model'
29
32
  autoload :Pager, 'couchrest/helper/pager'
30
33
  autoload :FileManager, 'couchrest/helper/file_manager'
31
34
  autoload :Streamer, 'couchrest/helper/streamer'
@@ -76,19 +79,15 @@ module CouchRest
76
79
  # creates it if it isn't already there
77
80
  # returns it after it's been created
78
81
  def database! url
79
- uri = URI.parse url
80
- path = uri.path
81
- uri.path = ''
82
- cr = CouchRest.new(uri.to_s)
83
- cr.database!(path)
82
+ parsed = parse url
83
+ cr = CouchRest.new(parsed[:host])
84
+ cr.database!(parsed[:database])
84
85
  end
85
86
 
86
87
  def database url
87
- uri = URI.parse url
88
- path = uri.path
89
- uri.path = ''
90
- cr = CouchRest.new(uri.to_s)
91
- cr.database(path)
88
+ parsed = parse url
89
+ cr = CouchRest.new(parsed[:host])
90
+ cr.database(parsed[:database])
92
91
  end
93
92
 
94
93
  def put uri, doc = nil
@@ -0,0 +1,292 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ class Basic
4
+ include CouchRest::Model
5
+ end
6
+
7
+ class Article
8
+ include CouchRest::Model
9
+ use_database CouchRest.database!('http://localhost:5984/couchrest-model-test')
10
+ unique_id :slug
11
+
12
+ view_by :date, :descending => true
13
+ view_by :user_id, :date
14
+
15
+ view_by :tags,
16
+ :map =>
17
+ "function(doc) {
18
+ if (doc.type == 'Article' && doc.tags) {
19
+ doc.tags.forEach(function(tag){
20
+ emit(tag, 1);
21
+ });
22
+ }
23
+ }",
24
+ :reduce =>
25
+ "function(keys, values, rereduce) {
26
+ return sum(values);
27
+ }"
28
+
29
+ key_writer :date
30
+ key_reader :slug, :created_at, :updated_at
31
+ key_accessor :title, :tags
32
+
33
+ timestamps!
34
+
35
+ before(:create, :generate_slug_from_title)
36
+ def generate_slug_from_title
37
+ doc['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
38
+ end
39
+ end
40
+
41
+ describe CouchRest::Model do
42
+ before(:all) do
43
+ @cr = CouchRest.new(COUCHHOST)
44
+ @db = @cr.database(TESTDB)
45
+ @db.delete! rescue nil
46
+ @db = @cr.create_db(TESTDB) rescue nil
47
+ @adb = @cr.database('couchrest-model-test')
48
+ @adb.delete! rescue nil
49
+ CouchRest.database!('http://localhost:5984/couchrest-model-test')
50
+ CouchRest::Model.default_database = CouchRest.database!('http://localhost:5984/couchrest-test')
51
+ end
52
+
53
+ it "should use the default database" do
54
+ Basic.database.info['db_name'].should == 'couchrest-test'
55
+ end
56
+
57
+ it "should override the default db" do
58
+ Article.database.info['db_name'].should == 'couchrest-model-test'
59
+ end
60
+
61
+ describe "a new model" do
62
+ it "should be a new_record" do
63
+ @obj = Basic.new
64
+ @obj.should be_a_new_record
65
+ end
66
+ end
67
+
68
+ describe "a model with key_accessors" do
69
+ it "should allow reading keys" do
70
+ @art = Article.new
71
+ @art.doc['title'] = 'My Article Title'
72
+ @art.title.should == 'My Article Title'
73
+ end
74
+ it "should allow setting keys" do
75
+ @art = Article.new
76
+ @art.title = 'My Article Title'
77
+ @art.doc['title'].should == 'My Article Title'
78
+ end
79
+ end
80
+
81
+ describe "a model with key_writers" do
82
+ it "should allow setting keys" do
83
+ @art = Article.new
84
+ t = Time.now
85
+ @art.date = t
86
+ @art.doc['date'].should == t
87
+ end
88
+ it "should not allow reading keys" do
89
+ @art = Article.new
90
+ t = Time.now
91
+ @art.date = t
92
+ lambda{@art.date}.should raise_error
93
+ end
94
+ end
95
+
96
+ describe "a model with key_readers" do
97
+ it "should allow reading keys" do
98
+ @art = Article.new
99
+ @art.doc['slug'] = 'my-slug'
100
+ @art.slug.should == 'my-slug'
101
+ end
102
+ it "should not allow setting keys" do
103
+ @art = Article.new
104
+ lambda{@art.slug = 'My Article Title'}.should raise_error
105
+ end
106
+ end
107
+
108
+ describe "getting a model" do
109
+ before(:all) do
110
+ @art = Article.new(:title => 'All About Getting')
111
+ @art.save
112
+ end
113
+ it "should load and instantiate it" do
114
+ foundart = Article.get @art.id
115
+ foundart.title.should == "All About Getting"
116
+ end
117
+ end
118
+
119
+ describe "saving a model" do
120
+ before(:all) do
121
+ @obj = Basic.new
122
+ @obj.save.should == true
123
+ end
124
+
125
+ it "should save the doc" do
126
+ doc = @obj.database.get @obj.id
127
+ doc['_id'].should == @obj.id
128
+ end
129
+
130
+ it "should be set for resaving" do
131
+ rev = @obj.rev
132
+ @obj.doc['another-key'] = "some value"
133
+ @obj.save
134
+ @obj.rev.should_not == rev
135
+ end
136
+
137
+ it "should set the id" do
138
+ @obj.id.should be_an_instance_of String
139
+ end
140
+
141
+ it "should set the type" do
142
+ @obj.doc['type'].should == 'Basic'
143
+ end
144
+ end
145
+
146
+ describe "saving a model with a unique_id configured" do
147
+ before(:each) do
148
+ @art = Article.new
149
+ @old = Article.database.get('this-is-the-title') rescue nil
150
+ Article.database.delete(@old) if @old
151
+ end
152
+
153
+ it "should require the title" do
154
+ lambda{@art.save}.should raise_error
155
+ @art.title = 'This is the title'
156
+ @art.save.should == true
157
+ end
158
+
159
+ it "should not change the slug on update" do
160
+ @art.title = 'This is the title'
161
+ @art.save.should == true
162
+ @art.title = 'new title'
163
+ @art.save.should == true
164
+ @art.slug.should == 'this-is-the-title'
165
+ end
166
+
167
+ it "should raise an error when the slug is taken" do
168
+ @art.title = 'This is the title'
169
+ @art.save.should == true
170
+ @art2 = Article.new(:title => 'This is the title!')
171
+ lambda{@art2.save}.should raise_error
172
+ end
173
+
174
+ it "should set the slug" do
175
+ @art.title = 'This is the title'
176
+ @art.save.should == true
177
+ @art.slug.should == 'this-is-the-title'
178
+ end
179
+
180
+ it "should set the id" do
181
+ @art.title = 'This is the title'
182
+ @art.save.should == true
183
+ @art.id.should == 'this-is-the-title'
184
+ end
185
+ end
186
+
187
+ describe "a model with timestamps" do
188
+ before(:all) do
189
+ @art = Article.new(:title => "Saving this")
190
+ @art.save
191
+ end
192
+ it "should set the time on create" do
193
+ (Time.now - @art.created_at).should < 2
194
+ foundart = Article.get @art.id
195
+ foundart.created_at.should == foundart.updated_at
196
+ end
197
+ it "should set the time on update" do
198
+ @art.save
199
+ @art.created_at.should < @art.updated_at
200
+ end
201
+ end
202
+
203
+ describe "a model with simple views and a default param" do
204
+ before(:all) do
205
+ written_at = Time.now - 24 * 3600 * 7
206
+ @titles = ["this and that", "also interesting", "more fun", "some junk"]
207
+ @titles.each do |title|
208
+ a = Article.new(:title => title)
209
+ a.date = written_at
210
+ a.save
211
+ written_at += 24 * 3600
212
+ end
213
+ end
214
+
215
+ it "should create the design doc" do
216
+ Article.by_date rescue nil
217
+ doc = Article.database.get("_design/Article")
218
+ doc['views']['by_date'].should_not be_nil
219
+ end
220
+
221
+ it "should return the matching raw view result" do
222
+ view = Article.by_date :raw => true
223
+ view['rows'].length.should == 4
224
+ end
225
+
226
+ it "should return the matching objects (with descending)" do
227
+ articles = Article.by_date
228
+ articles.collect{|a|a.title}.should == @titles.reverse
229
+ end
230
+
231
+ it "should allow you to override default args" do
232
+ articles = Article.by_date :descending => false
233
+ articles.collect{|a|a.title}.should == @titles
234
+ end
235
+ end
236
+
237
+ describe "a model with a compound key view" do
238
+ before(:all) do
239
+ written_at = Time.now - 24 * 3600 * 7
240
+ @titles = ["uniq one", "even more interesting", "less fun", "not junk"]
241
+ @user_ids = ["quentin", "aaron"]
242
+ @titles.each_with_index do |title,i|
243
+ u = i % 2
244
+ a = Article.new(:title => title, :user_id => @user_ids[u])
245
+ a.date = written_at
246
+ a.save
247
+ written_at += 24 * 3600
248
+ end
249
+ end
250
+ it "should create the design doc" do
251
+ Article.by_user_id_and_date rescue nil
252
+ doc = Article.database.get("_design/Article")
253
+ doc['views']['by_date'].should_not be_nil
254
+ end
255
+ it "should sort correctly" do
256
+ articles = Article.by_user_id_and_date
257
+ articles.collect{|a|a.doc['user_id']}.should == ['aaron', 'aaron', 'quentin', 'quentin']
258
+ articles[1].title.should == 'not junk'
259
+ end
260
+ it "should be queryable with couchrest options" do
261
+ articles = Article.by_user_id_and_date :count => 1, :startkey => 'quentin'
262
+ articles.length.should == 1
263
+ articles[0].title.should == "even more interesting"
264
+ end
265
+ end
266
+
267
+ describe "with a custom view" do
268
+ before(:all) do
269
+ @titles = ["very uniq one", "even less interesting", "some fun", "really junk", "crazy bob"]
270
+ @tags = ["cool", "lame"]
271
+ @titles.each_with_index do |title,i|
272
+ u = i % 2
273
+ a = Article.new(:title => title, :tags => [@tags[u]])
274
+ a.save
275
+ end
276
+ end
277
+ it "should be available raw" do
278
+ view = Article.by_tags :raw => true
279
+ view['rows'].length.should == 5
280
+ end
281
+
282
+ it "should be default to :reduce => false" do
283
+ ars = Article.by_tags
284
+ ars.first.tags.first.should == 'cool'
285
+ end
286
+
287
+ it "should be raw when reduce is true" do
288
+ view = Article.by_tags :reduce => true, :group => true
289
+ view['rows'].find{|r|r['key'] == 'cool'}['value'].should == 3
290
+ end
291
+ end
292
+ end
@@ -139,8 +139,13 @@ describe CouchRest do
139
139
  it "should be possible without an explicit CouchRest instantiation" do
140
140
  db = CouchRest.database "http://localhost:5984/couchrest-test"
141
141
  db.should be_an_instance_of(CouchRest::Database)
142
- db.host.should == "http://localhost:5984"
142
+ db.host.should == "localhost:5984"
143
143
  end
144
+ # TODO add https support (need test environment...)
145
+ # it "should work with https" # do
146
+ # db = CouchRest.database "https://localhost:5984/couchrest-test"
147
+ # db.host.should == "https://localhost:5984"
148
+ # end
144
149
  it "should not create the database automatically" do
145
150
  db = CouchRest.database "http://localhost:5984/couchrest-test"
146
151
  lambda{db.info}.should raise_error(RestClient::ResourceNotFound)
@@ -150,6 +155,7 @@ describe CouchRest do
150
155
  describe "ensuring the db exists" do
151
156
  it "should be super easy" do
152
157
  db = CouchRest.database! "http://localhost:5984/couchrest-test-2"
158
+ db.name.should == 'couchrest-test-2'
153
159
  db.info["db_name"].should == 'couchrest-test-2'
154
160
  end
155
161
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jchris-couchrest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.8
4
+ version: 0.9.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - J. Chris Anderson
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-09-10 21:00:00 -07:00
12
+ date: 2008-09-11 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -30,6 +30,15 @@ dependencies:
30
30
  - !ruby/object:Gem::Version
31
31
  version: "0.5"
32
32
  version:
33
+ - !ruby/object:Gem::Dependency
34
+ name: extlib
35
+ version_requirement:
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.9.6
41
+ version:
33
42
  description: CouchRest provides a simple interface on top of CouchDB's RESTful HTTP API, as well as including some utility scripts for managing views and attachments.
34
43
  email: jchris@grabb.it
35
44
  executables:
@@ -70,6 +79,7 @@ files:
70
79
  - lib/couchrest/commands/push.rb
71
80
  - lib/couchrest/core
72
81
  - lib/couchrest/core/database.rb
82
+ - lib/couchrest/core/model.rb
73
83
  - lib/couchrest/core/server.rb
74
84
  - lib/couchrest/helper
75
85
  - lib/couchrest/helper/file_manager.rb
@@ -82,6 +92,9 @@ files:
82
92
  - lib/couchrest/monkeypatches.rb
83
93
  - lib/couchrest.rb
84
94
  - spec/couchapp_spec.rb
95
+ - spec/couchrest
96
+ - spec/couchrest/core
97
+ - spec/couchrest/core/model_spec.rb
85
98
  - spec/couchrest_spec.rb
86
99
  - spec/database_spec.rb
87
100
  - spec/file_manager_spec.rb
@@ -95,6 +108,12 @@ files:
95
108
  - spec/fixtures/couchapp/views/example-map.js
96
109
  - spec/fixtures/couchapp/views/example-reduce.js
97
110
  - spec/fixtures/couchapp-test
111
+ - spec/fixtures/couchapp-test/my-app
112
+ - spec/fixtures/couchapp-test/my-app/attachments
113
+ - spec/fixtures/couchapp-test/my-app/attachments/index.html
114
+ - spec/fixtures/couchapp-test/my-app/views
115
+ - spec/fixtures/couchapp-test/my-app/views/example-map.js
116
+ - spec/fixtures/couchapp-test/my-app/views/example-reduce.js
98
117
  - spec/fixtures/views
99
118
  - spec/fixtures/views/lib.js
100
119
  - spec/fixtures/views/test_view