couchrest_model 1.0.0 → 1.1.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +1 -1
  2. data/Gemfile.lock +19 -20
  3. data/README.md +145 -20
  4. data/VERSION +1 -1
  5. data/couchrest_model.gemspec +2 -3
  6. data/history.txt +14 -0
  7. data/lib/couchrest/model/associations.rb +4 -4
  8. data/lib/couchrest/model/base.rb +5 -0
  9. data/lib/couchrest/model/callbacks.rb +1 -2
  10. data/lib/couchrest/model/collection.rb +1 -1
  11. data/lib/couchrest/model/designs/view.rb +486 -0
  12. data/lib/couchrest/model/designs.rb +81 -0
  13. data/lib/couchrest/model/document_queries.rb +1 -1
  14. data/lib/couchrest/model/persistence.rb +25 -16
  15. data/lib/couchrest/model/properties.rb +5 -1
  16. data/lib/couchrest/model/property.rb +2 -2
  17. data/lib/couchrest/model/proxyable.rb +152 -0
  18. data/lib/couchrest/model/typecast.rb +1 -1
  19. data/lib/couchrest/model/validations/casted_model.rb +3 -1
  20. data/lib/couchrest/model/validations/locale/en.yml +1 -1
  21. data/lib/couchrest/model/validations/uniqueness.rb +6 -7
  22. data/lib/couchrest/model/validations.rb +1 -0
  23. data/lib/couchrest/model/views.rb +11 -9
  24. data/lib/couchrest_model.rb +3 -0
  25. data/spec/couchrest/assocations_spec.rb +2 -2
  26. data/spec/couchrest/base_spec.rb +15 -1
  27. data/spec/couchrest/casted_model_spec.rb +30 -12
  28. data/spec/couchrest/class_proxy_spec.rb +2 -2
  29. data/spec/couchrest/collection_spec.rb +89 -0
  30. data/spec/couchrest/designs/view_spec.rb +766 -0
  31. data/spec/couchrest/designs_spec.rb +110 -0
  32. data/spec/couchrest/persistence_spec.rb +36 -7
  33. data/spec/couchrest/property_spec.rb +15 -0
  34. data/spec/couchrest/proxyable_spec.rb +329 -0
  35. data/spec/couchrest/{validations.rb → validations_spec.rb} +1 -3
  36. data/spec/couchrest/view_spec.rb +19 -91
  37. data/spec/fixtures/base.rb +8 -6
  38. data/spec/fixtures/more/article.rb +1 -1
  39. data/spec/fixtures/more/course.rb +4 -2
  40. metadata +21 -76
  41. data/lib/couchrest/model/view.rb +0 -190
data/.gitignore CHANGED
@@ -2,7 +2,7 @@
2
2
  html/*
3
3
  pkg
4
4
  *.swp
5
- .rvmrc
5
+ .rvmrc*
6
6
  .bundle
7
7
  couchdb.std*
8
8
  *.*~
data/Gemfile.lock CHANGED
@@ -1,52 +1,51 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- couchrest_model (1.0.0)
4
+ couchrest_model (1.1.0.beta)
5
5
  activemodel (~> 3.0.0)
6
- couchrest (~> 1.0.1)
6
+ couchrest (~> 1.0.2)
7
7
  mime-types (~> 1.15)
8
8
  railties (~> 3.0.0)
9
- rspec (>= 2.0.0)
10
9
  tzinfo (~> 0.3.22)
11
10
 
12
11
  GEM
13
12
  remote: http://rubygems.org/
14
13
  specs:
15
14
  abstract (1.0.0)
16
- actionpack (3.0.3)
17
- activemodel (= 3.0.3)
18
- activesupport (= 3.0.3)
15
+ actionpack (3.0.4)
16
+ activemodel (= 3.0.4)
17
+ activesupport (= 3.0.4)
19
18
  builder (~> 2.1.2)
20
19
  erubis (~> 2.6.6)
21
20
  i18n (~> 0.4)
22
21
  rack (~> 1.2.1)
23
22
  rack-mount (~> 0.6.13)
24
- rack-test (~> 0.5.6)
23
+ rack-test (~> 0.5.7)
25
24
  tzinfo (~> 0.3.23)
26
- activemodel (3.0.3)
27
- activesupport (= 3.0.3)
25
+ activemodel (3.0.4)
26
+ activesupport (= 3.0.4)
28
27
  builder (~> 2.1.2)
29
28
  i18n (~> 0.4)
30
- activesupport (3.0.3)
29
+ activesupport (3.0.4)
31
30
  builder (2.1.2)
32
- couchrest (1.0.1)
33
- json (>= 1.4.6)
34
- mime-types (>= 1.15)
35
- rest-client (>= 1.5.1)
31
+ couchrest (1.0.2)
32
+ json (~> 1.5.1)
33
+ mime-types (~> 1.15)
34
+ rest-client (~> 1.6.1)
36
35
  diff-lcs (1.1.2)
37
36
  erubis (2.6.6)
38
37
  abstract (>= 1.0.0)
39
38
  i18n (0.5.0)
40
- json (1.4.6)
39
+ json (1.5.1)
41
40
  mime-types (1.16)
42
41
  rack (1.2.1)
43
42
  rack-mount (0.6.13)
44
43
  rack (>= 1.0.0)
45
44
  rack-test (0.5.7)
46
45
  rack (>= 1.0)
47
- railties (3.0.3)
48
- actionpack (= 3.0.3)
49
- activesupport (= 3.0.3)
46
+ railties (3.0.4)
47
+ actionpack (= 3.0.4)
48
+ activesupport (= 3.0.4)
50
49
  rake (>= 0.8.7)
51
50
  thor (~> 0.14.4)
52
51
  rake (0.8.7)
@@ -61,14 +60,14 @@ GEM
61
60
  diff-lcs (~> 1.1.2)
62
61
  rspec-mocks (2.3.0)
63
62
  thor (0.14.6)
64
- tzinfo (0.3.23)
63
+ tzinfo (0.3.24)
65
64
 
66
65
  PLATFORMS
67
66
  ruby
68
67
 
69
68
  DEPENDENCIES
70
69
  activemodel (~> 3.0.0)
71
- couchrest (~> 1.0.1)
70
+ couchrest (~> 1.0.2)
72
71
  couchrest_model!
73
72
  mime-types (~> 1.15)
74
73
  rack-test (>= 0.5.7)
data/README.md CHANGED
@@ -9,7 +9,7 @@ for validations and callbacks.
9
9
  If your project is still running Rails 2.3, you'll have to continue using ExtendedDocument as
10
10
  it is not possible to load ActiveModel into programs that do not use ActiveSupport 3.0.
11
11
 
12
- CouchRest Model is only tested on CouchDB 1.0.0 or newer.
12
+ CouchRest Model is only properly tested on CouchDB version 1.0 or newer.
13
13
 
14
14
  ## Install
15
15
 
@@ -19,15 +19,20 @@ CouchRest Model is only tested on CouchDB 1.0.0 or newer.
19
19
 
20
20
  ### Bundler
21
21
 
22
- If you're using bundler, just define a line similar to the following in your project's Gemfile:
22
+ If you're using bundler, define a line similar to the following in your project's Gemfile:
23
23
 
24
24
  gem 'couchrest_model'
25
25
 
26
- You might also consider using the latest git repository. All tests should pass in the master code branch
27
- but no guarantees!
26
+ You might also consider using the latest git repository. We try to make sure the current version in git is stable and at the very least all tests should pass.
28
27
 
29
28
  gem 'couchrest_model', :git => 'git://github.com/couchrest/couchrest_model.git'
30
29
 
30
+ ### Setup
31
+
32
+ There is currently no standard way for telling CouchRest Model how it should access your database, this is something we're still working on. For the time being, the easiest way is to set a COUCHDB_DATABASE global variable to an instance of CouchRest Database, and call `use_database COUCHDB_DATABASE` in each model.
33
+
34
+ TODO: Add an example!
35
+
31
36
  ### Development
32
37
 
33
38
  CouchRest Model now comes with a Gemfile to help with development. If you want to make changes to the code, download a copy then run:
@@ -94,7 +99,7 @@ be type casted and other options such as the default value. These replace your t
94
99
  `add_column` methods found in relational database migrations.
95
100
 
96
101
  Attributes with a property definition will have setter and getter methods defined for them. Any other attibute
97
- can be set in the same way you'd update a Hash, this funcionality is inherited from CouchRest Documents.
102
+ can be set as if the model were a Hash, this funcionality is inherited from CouchRest Documents.
98
103
 
99
104
  Here are a few examples of the way properties are used:
100
105
 
@@ -216,7 +221,7 @@ you'd like to use. For example:
216
221
  property :toys, [CatToy]
217
222
  end
218
223
 
219
- @cat = Cat.new(:name => 'Felix', :toys => [{:name => 'mouse', :purchases => 1.month.ago}])
224
+ @cat = Cat.new(:name => 'Felix', :toys => [{:name => 'mouse', :purchased => 1.month.ago}])
220
225
  @cat.toys.first.class == CatToy
221
226
  @cat.toys.first.name == 'mouse'
222
227
 
@@ -232,9 +237,9 @@ you'd like to model, CouchRest Model supports creating anonymous classes:
232
237
  class Cat < CouchRest::Model::Base
233
238
  property :name, String
234
239
 
235
- property :toys do |toy|
236
- toy.property :name, String
237
- toy.property :rating, Integer
240
+ property :toys do
241
+ property :name, String
242
+ property :rating, Integer
238
243
  end
239
244
  end
240
245
 
@@ -242,7 +247,136 @@ you'd like to model, CouchRest Model supports creating anonymous classes:
242
247
  @cat.toys.last.rating == 5
243
248
  @cat.toys.last.name == 'catnip ball'
244
249
 
245
- Anonymous classes will *only* create arrays of objects.
250
+ Anonymous classes will *only* create arrays of objects. If you're more of the traditional type, a block parameter
251
+ can be provided allowing you to use this variable before each method call inside the anonymous class. This is useful
252
+ if you need to access variables outside of the block.
253
+
254
+ ## Views
255
+
256
+ CouchDB views can be quite difficult to get grips with at first as they are quite different from what you'd expect with SQL queries in a normal Relational Database. Checkout some of the [CouchDB documentation on views](http://wiki.apache.org/couchdb/Introduction_to_CouchDB_views) to get to grips with the basics. The key is to remember that CouchDB will only generate indexes from which you can extract consecutive rows of data, filtering other than between two points in a data set is not possible.
257
+
258
+ CouchRest Model has great support for views, and since version 1.1.0 we added support for a View objects that make accessing your data even easier.
259
+
260
+ ### The Old Way
261
+
262
+ Here's an example of adding a view to our Cat class:
263
+
264
+ class Cat < CouchRest::Model::Base
265
+ property :name, String
266
+ property :toys, [CatToy]
267
+
268
+ view_by :name
269
+ end
270
+
271
+ The `view_by` method will create a view in the Cat's design document called "by_name". This will allow searches to be made for the Cat's name attribute. Calling `Cat.by_name` will send a query of to the database and return an array of all the Cat objects available. Internally, a map function is generated automatically and stored in CouchDB's design document for the current model, it'll look something like the following:
272
+
273
+ function(doc) {
274
+ if (doc['couchrest-type'] == 'Cat' && doc['name']) {
275
+ emit(doc.name, null);
276
+ }
277
+ }
278
+
279
+ By default, a special view called `all` is created and added to all couchrest models that allows you access to all the documents in the database that match the model. By default, these will be ordered by each documents id field.
280
+
281
+ It is also possible to create views of multiple keys, for example:
282
+
283
+ view_by :birthday, :name
284
+
285
+ This will create an view of all the cats' birthdays and their names called `by_birthday_and_name`.
286
+
287
+ Sometimes the automatically generate map function might not be sufficient for more complicated queries. To customize, add the :map and :reduce functions when creating the view:
288
+
289
+ view_by :tags,
290
+ :map =>
291
+ "function(doc) {
292
+ if (doc['model'] == 'Post' && doc.tags) {
293
+ doc.tags.forEach(function(tag){
294
+ emit(doc.tag, 1);
295
+ });
296
+ }
297
+ }",
298
+ :reduce =>
299
+ "function(keys, values, rereduce) {
300
+ return sum(values);
301
+ }"
302
+
303
+ Calling a view will return document objects by default, to get access to the raw CouchDB result add the `:raw => true` option to get a hash instead. Custom views can also be queried with `:reduce => true` to return reduce results. The default is to query with `:reduce => false`.
304
+
305
+ 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
306
+ that model won't be available until generation is complete. This can take some time with large databases. Strategies are in the works.
307
+
308
+ ### View Objects
309
+
310
+ Since CouchRest Model 1.1.0 it is now possible to create views that return objects chainable objects, similar to those you'd find in the Sequel Ruby library or Rails 3's Arel. Heres an example of creating a few views:
311
+
312
+ class Post < CouchRest::Model::Base
313
+ property :title
314
+ property :body
315
+ property :posted_at, DateTime
316
+ property :tags, [String]
317
+
318
+ design do
319
+ view :by_title
320
+ view :by_posted_at_and_title
321
+ view :tag_list,
322
+ :map =>
323
+ "function(doc) {
324
+ if (doc['model'] == 'Post' && doc.tags) {
325
+ doc.tags.forEach(function(tag){
326
+ emit(doc.tag, 1);
327
+ });
328
+ }
329
+ }",
330
+ :reduce =>
331
+ "function(keys, values, rereduce) {
332
+ return sum(values);
333
+ }"
334
+ end
335
+
336
+ You'll see that this new syntax requires all views to be defined inside a design block. Unlike the old version, the keys to be used in a query are determined from the name of the view, not the other way round. Acessing data is the fun part:
337
+
338
+ # Prepare a query:
339
+ view = Post.by_posted_at_and_title.skip(5).limit(10)
340
+
341
+ # Fetch the results:
342
+ view.each do |post|
343
+ puts "Title: #{post.title}"
344
+ end
345
+
346
+ # Grab the CouchDB result information with the same object:
347
+ view.total_rows => 10
348
+ view.offset => 5
349
+
350
+ # Re-use and add new filters
351
+ filter = view.startkey([1.month.ago]).endkey([Date.current, {}])
352
+
353
+ # Fetch row results without the documents:
354
+ filter.rows.each do |row|
355
+ puts "Row value: #{row.value} Doc ID: #{row.id}"
356
+ end
357
+
358
+ # Lazily load documents (take last row from previous example):
359
+ row.doc => Fetch last Post document from database
360
+
361
+ # Using reduced queries is also easy:
362
+ tag_usage = Post.tag_list.reduce.group_level(1)
363
+ tag_usage.rows.each do |row|
364
+ puts "Tag: #{row.key} Uses: #{row.value}"
365
+ end
366
+
367
+ #### Pagination
368
+
369
+ The view objects have built in support for pagination based on the [kaminari](https://github.com/amatsuda/kaminari) gem. Methods are provided to match those required by the library to peform pagination.
370
+
371
+ For your view to support paginating the results, it must use a reduce function that provides a total count of the documents in the result set. By default, auto-generated views include a reduce function that supports this.
372
+
373
+ Use pagination as follows:
374
+
375
+ # Prepare a query:
376
+ @posts = Post.by_title.page(params[:page]).per(10)
377
+
378
+ # In your view, with the kaminari gem loaded:
379
+ paginate @posts
246
380
 
247
381
 
248
382
  ## Assocations
@@ -253,7 +387,7 @@ Two types at the moment:
253
387
 
254
388
  collection_of :tags
255
389
 
256
- This is a somewhat controvesial feature of CouchRest Model that some document database purists may fringe at. CouchDB does not yet povide many features to support relationships between documents but the fact of that matter is that its a very useful paradigm for modelling data systems.
390
+ This is a somewhat controvesial feature of CouchRest Model that some document database purists may cringe at. CouchDB does not yet povide many features to support relationships between documents but the fact of that matter is that its a very useful paradigm for modelling data systems.
257
391
 
258
392
  In the near future we hope to add support for a `has_many` relationship that takes of the _Linked Documents_ feature that arrived in CouchDB 0.11.
259
393
 
@@ -394,15 +528,6 @@ Options currently avilable are:
394
528
  None at the moment...
395
529
 
396
530
 
397
- ## Ruby on Rails
398
-
399
- CouchRest Model is compatible with rails and provides some ActiveRecord-like methods.
400
-
401
- The CouchRest companion rails project [http://github.com/hpoydar/couchrest-rails](http://github.com/hpoydar/couchrest-rails) is great for providing default connection details for your database. At the time of writting however it does not provide explicit support for CouchRest Model.
402
-
403
- CouchRest Model and the original CouchRest ExtendedDocument do not share the same namespace, as such you should not have any problems using them both at the same time. This might help with migrations.
404
-
405
-
406
531
  ## Testing
407
532
 
408
533
  The most complete documentation is the spec/ directory. To validate your CouchRest install, from the project root directory run `rake`, or `autotest` (requires RSpec and optionally ZenTest for autotest support).
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0
1
+ 1.1.0.beta
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{couchrest_model}
5
- s.version = "1.0.0"
5
+ s.version = `cat VERSION`.strip
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos", "Will Leinweber", "Sam Lown"]
@@ -23,12 +23,11 @@ Gem::Specification.new do |s|
23
23
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24
24
  s.require_paths = ["lib"]
25
25
 
26
- s.add_dependency(%q<couchrest>, "~> 1.0.1")
26
+ s.add_dependency(%q<couchrest>, "~> 1.0.2")
27
27
  s.add_dependency(%q<mime-types>, "~> 1.15")
28
28
  s.add_dependency(%q<activemodel>, "~> 3.0.0")
29
29
  s.add_dependency(%q<tzinfo>, "~> 0.3.22")
30
30
  s.add_dependency(%q<railties>, "~> 3.0.0")
31
- s.add_dependency(%q<rspec>, ">= 2.0.0")
32
31
  s.add_development_dependency(%q<rspec>, ">= 2.0.0")
33
32
  s.add_development_dependency(%q<rack-test>, ">= 0.5.7")
34
33
  end
data/history.txt CHANGED
@@ -1,3 +1,17 @@
1
+ == 1.1.0.beta
2
+
3
+ * Epic enhancements:
4
+ * Added "View" object for dynamic view queries
5
+ * Added easy to use proxy_for and proxied_by class methods for proxying data
6
+
7
+ * Minor enhancements:
8
+ * A yield parameter in an anonymous casted model property block is no longer required (@samlown)
9
+ * Narrow the rescued exception to avoid catching class evaluation errors that has nothing to to with the association (thanks Simone Carletti)
10
+ * Fix validate uniqueness test that was never executed (thanks Simone Carletti)
11
+ * Adds a #reload method to reload document attributes (thanks Simone Carletti)
12
+ * Numeric types can be casted from strings with leading or trailing whitespace (thanks chrisdurtschi)
13
+ * CollectionProxy no longer provided by default with simple views (pending deprication)
14
+
1
15
  == CouchRest Model 1.0.0
2
16
 
3
17
  * Major enhancements
@@ -25,8 +25,8 @@ module CouchRest
25
25
 
26
26
  begin
27
27
  opts[:class] = opts[:class_name].constantize
28
- rescue
29
- raise "Unable to convert class name into Constant for #{self.name}##{attrib}"
28
+ rescue NameError
29
+ raise NameError, "Unable to convert class name into Constant for #{self.name}##{attrib}"
30
30
  end
31
31
 
32
32
  prop = property(opts[:foreign_key], opts)
@@ -109,7 +109,7 @@ module CouchRest
109
109
  base = options[:proxy] || options[:class_name]
110
110
  class_eval <<-EOS, __FILE__, __LINE__ + 1
111
111
  def #{attrib}
112
- @#{attrib} ||= #{options[:foreign_key]}.nil? ? nil : #{base}.get(self.#{options[:foreign_key]})
112
+ @#{attrib} ||= #{options[:foreign_key]}.nil? ? nil : (model_proxy || #{base}).get(self.#{options[:foreign_key]})
113
113
  end
114
114
  EOS
115
115
  end
@@ -140,7 +140,7 @@ module CouchRest
140
140
  class_eval <<-EOS, __FILE__, __LINE__ + 1
141
141
  def #{attrib}(reload = false)
142
142
  return @#{attrib} unless @#{attrib}.nil? or reload
143
- ary = self.#{options[:foreign_key]}.collect{|i| #{base}.get(i)}
143
+ ary = self.#{options[:foreign_key]}.collect{|i| (model_proxy || #{base}).get(i)}
144
144
  @#{attrib} = ::CouchRest::CollectionOfProxy.new(ary, self, '#{options[:foreign_key]}')
145
145
  end
146
146
  EOS
@@ -12,10 +12,12 @@ module CouchRest
12
12
  include CouchRest::Model::DesignDoc
13
13
  include CouchRest::Model::ExtendedAttachments
14
14
  include CouchRest::Model::ClassProxy
15
+ include CouchRest::Model::Proxyable
15
16
  include CouchRest::Model::Collection
16
17
  include CouchRest::Model::PropertyProtection
17
18
  include CouchRest::Model::Associations
18
19
  include CouchRest::Model::Validations
20
+ include CouchRest::Model::Designs
19
21
 
20
22
  def self.subclasses
21
23
  @subclasses ||= []
@@ -45,9 +47,12 @@ module CouchRest
45
47
  # Options supported:
46
48
  #
47
49
  # * :directly_set_attributes: true when data comes directly from database
50
+ # * :database: provide an alternative database
48
51
  #
49
52
  def initialize(doc = {}, options = {})
50
53
  doc = prepare_all_attributes(doc, options)
54
+ # set the instances database, if provided
55
+ self.database = options[:database] unless options[:database].nil?
51
56
  super(doc)
52
57
  unless self['_id'] && self['_rev']
53
58
  self[self.model_type_key] = self.class.to_s
@@ -12,8 +12,7 @@ module CouchRest #:nodoc:
12
12
  :create,
13
13
  :destroy,
14
14
  :save,
15
- :update,
16
- :validate
15
+ :update
17
16
 
18
17
  end
19
18
 
@@ -222,7 +222,7 @@ module CouchRest
222
222
  if @container_class.nil?
223
223
  results
224
224
  else
225
- results['rows'].collect { |row| @container_class.create_from_database(row['doc']) } unless results['rows'].nil?
225
+ results['rows'].collect { |row| @container_class.build_from_database(row['doc']) } unless results['rows'].nil?
226
226
  end
227
227
  end
228
228