couchrest_model 1.0.0 → 1.1.0.beta

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.
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
@@ -58,8 +58,8 @@ module CouchRest::Model
58
58
  if default.class == Proc
59
59
  default.call
60
60
  else
61
- # Marshal.load(Marshal.dump(default)) # Removed as there are no failing tests and caused mutex errors
62
- default
61
+ # TODO identify cause of mutex errors
62
+ Marshal.load(Marshal.dump(default))
63
63
  end
64
64
  end
65
65
 
@@ -0,0 +1,152 @@
1
+ module CouchRest
2
+ module Model
3
+ # :nodoc: Because I like inventing words
4
+ module Proxyable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ attr_accessor :model_proxy
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ # Define a collection that will use the base model for the database connection
14
+ # details.
15
+ def proxy_for(model_name, options = {})
16
+ db_method = options[:database_method] || "proxy_database"
17
+ options[:class_name] ||= model_name.to_s.singularize.camelize
18
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
19
+ def #{model_name}
20
+ unless respond_to?('#{db_method}')
21
+ raise "Missing ##{db_method} method for proxy"
22
+ end
23
+ @#{model_name} ||= CouchRest::Model::Proxyable::ModelProxy.new(::#{options[:class_name]}, self, self.class.to_s.underscore, #{db_method})
24
+ end
25
+ EOS
26
+ end
27
+
28
+ def proxied_by(model_name, options = {})
29
+ raise "Model can only be proxied once or ##{model_name} already defined" if method_defined?(model_name)
30
+ attr_accessor model_name
31
+ end
32
+ end
33
+
34
+ class ModelProxy
35
+
36
+ attr_reader :model, :owner, :owner_name, :database
37
+
38
+ def initialize(model, owner, owner_name, database)
39
+ @model = model
40
+ @owner = owner
41
+ @owner_name = owner_name
42
+ @database = database
43
+ end
44
+
45
+ # Base
46
+
47
+ def new(*args)
48
+ proxy_update(model.new(*args))
49
+ end
50
+
51
+ def build_from_database(doc = {})
52
+ proxy_update(model.build_from_database(doc))
53
+ end
54
+
55
+ def method_missing(m, *args, &block)
56
+ if has_view?(m)
57
+ if model.respond_to?(m)
58
+ return model.send(m, *args).proxy(self)
59
+ else
60
+ query = args.shift || {}
61
+ return view(m, query, *args, &block)
62
+ end
63
+ elsif m.to_s =~ /^find_(by_.+)/
64
+ view_name = $1
65
+ if has_view?(view_name)
66
+ return first_from_view(view_name, *args)
67
+ end
68
+ end
69
+ super
70
+ end
71
+
72
+ # DocumentQueries
73
+
74
+ def all(opts = {}, &block)
75
+ proxy_update_all(@model.all({:database => @database}.merge(opts), &block))
76
+ end
77
+
78
+ def count(opts = {})
79
+ @model.count({:database => @database}.merge(opts))
80
+ end
81
+
82
+ def first(opts = {})
83
+ proxy_update(@model.first({:database => @database}.merge(opts)))
84
+ end
85
+
86
+ def last(opts = {})
87
+ proxy_update(@model.last({:database => @database}.merge(opts)))
88
+ end
89
+
90
+ def get(id)
91
+ proxy_update(@model.get(id, @database))
92
+ end
93
+ alias :find :get
94
+
95
+ # Views
96
+
97
+ def has_view?(view)
98
+ @model.has_view?(view)
99
+ end
100
+
101
+ def view_by(*args)
102
+ @model.view_by(*args)
103
+ end
104
+
105
+ def view(name, query={}, &block)
106
+ proxy_update_all(@model.view(name, {:database => @database}.merge(query), &block))
107
+ end
108
+
109
+ def first_from_view(name, *args)
110
+ # add to first hash available, or add to end
111
+ (args.last.is_a?(Hash) ? args.last : (args << {}).last)[:database] = @database
112
+ proxy_update(@model.first_from_view(name, *args))
113
+ end
114
+
115
+ # DesignDoc
116
+
117
+ def design_doc
118
+ @model.design_doc
119
+ end
120
+
121
+ def refresh_design_doc(db = nil)
122
+ @model.refresh_design_doc(db || @database)
123
+ end
124
+
125
+ def save_design_doc(db = nil)
126
+ @model.save_design_doc(db || @database)
127
+ end
128
+
129
+
130
+ protected
131
+
132
+ # Update the document's proxy details, specifically, the fields that
133
+ # link back to the original document.
134
+ def proxy_update(doc)
135
+ if doc
136
+ doc.database = @database if doc.respond_to?(:database=)
137
+ doc.model_proxy = self if doc.respond_to?(:model_proxy=)
138
+ doc.send("#{owner_name}=", owner) if doc.respond_to?("#{owner_name}=")
139
+ end
140
+ doc
141
+ end
142
+
143
+ def proxy_update_all(docs)
144
+ docs.each do |doc|
145
+ proxy_update(doc)
146
+ end
147
+ end
148
+
149
+ end
150
+ end
151
+ end
152
+ end
@@ -79,7 +79,7 @@ module CouchRest
79
79
  # Match numeric string
80
80
  def typecast_to_numeric(value, method)
81
81
  if value.respond_to?(:to_str)
82
- if value.gsub(/,/, '.').gsub(/\.(?!\d*\Z)/, '').to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
82
+ if value.strip.gsub(/,/, '.').gsub(/\.(?!\d*\Z)/, '').to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
83
83
  $1.send(method)
84
84
  else
85
85
  value
@@ -6,7 +6,9 @@ module CouchRest
6
6
  def validate_each(document, attribute, value)
7
7
  values = value.is_a?(Array) ? value : [value]
8
8
  return if values.collect {|doc| doc.nil? || doc.valid? }.all?
9
- document.errors.add(attribute, :invalid, :default => options[:message], :value => value)
9
+ error_options = { :value => value }
10
+ error_options[:message] = options[:message] if options[:message]
11
+ document.errors.add(attribute, :invalid, error_options)
10
12
  end
11
13
  end
12
14
  end
@@ -1,5 +1,5 @@
1
1
  en:
2
2
  errors:
3
3
  messages:
4
- taken: "is already taken"
4
+ taken: "has already been taken"
5
5
 
@@ -9,19 +9,19 @@ module CouchRest
9
9
 
10
10
  # Ensure we have a class available so we can check for a usable view
11
11
  # or add one if necessary.
12
- def setup(klass)
13
- @klass = klass
12
+ def setup(model)
13
+ @model = model
14
14
  end
15
15
 
16
-
17
16
  def validate_each(document, attribute, value)
18
17
  view_name = options[:view].nil? ? "by_#{attribute}" : options[:view]
18
+ model = document.model_proxy || @model
19
19
  # Determine the base of the search
20
- base = options[:proxy].nil? ? @klass : document.instance_eval(options[:proxy])
20
+ base = options[:proxy].nil? ? model : document.instance_eval(options[:proxy])
21
21
 
22
22
  if base.respond_to?(:has_view?) && !base.has_view?(view_name)
23
23
  raise "View #{document.class.name}.#{options[:view]} does not exist!" unless options[:view].nil?
24
- @klass.view_by attribute
24
+ model.view_by attribute
25
25
  end
26
26
 
27
27
  docs = base.view(view_name, :key => value, :limit => 2, :include_docs => false)['rows']
@@ -32,11 +32,10 @@ module CouchRest
32
32
  end
33
33
 
34
34
  if docs.length > 0
35
- document.errors.add(attribute, :taken, :default => options[:message], :value => value)
35
+ document.errors.add(attribute, :taken, options.merge(:value => value))
36
36
  end
37
37
  end
38
38
 
39
-
40
39
  end
41
40
 
42
41
  end
@@ -15,6 +15,7 @@ module CouchRest
15
15
  extend ActiveSupport::Concern
16
16
  included do
17
17
  include ActiveModel::Validations
18
+ include ActiveModel::Validations::Callbacks
18
19
  end
19
20
 
20
21
 
@@ -85,9 +85,14 @@ module CouchRest
85
85
  end
86
86
 
87
87
  # returns stored defaults if there is a view named this in the design doc
88
- def has_view?(view)
89
- view = view.to_s
90
- design_doc && design_doc['views'] && design_doc['views'][view]
88
+ def has_view?(name)
89
+ design_doc && design_doc.has_view?(name)
90
+ end
91
+
92
+ # Check if the view can be reduced by checking to see if it has a
93
+ # reduce function.
94
+ def can_reduce_view?(name)
95
+ design_doc && design_doc.can_reduce_view?(name)
91
96
  end
92
97
 
93
98
  # Dispatches to any named view.
@@ -127,12 +132,9 @@ module CouchRest
127
132
  if raw || (opts.has_key?(:include_docs) && opts[:include_docs] == false)
128
133
  fetch_view(db, name, opts, &block)
129
134
  else
130
- if block.nil?
131
- collection_proxy_for(design_doc, name, opts.merge({:database => db, :include_docs => true}))
132
- else
133
- view = fetch_view db, name, opts.merge({:include_docs => true}), &block
134
- view['rows'].collect{|r|create_from_database(r['doc'])} if view['rows']
135
- end
135
+ opts = opts.merge(:include_docs => true)
136
+ view = fetch_view db, name, opts, &block
137
+ view['rows'].collect{|r| build_from_database(r['doc'])} if view['rows']
136
138
  end
137
139
  end
138
140
 
@@ -37,9 +37,12 @@ require "couchrest/model/views"
37
37
  require "couchrest/model/design_doc"
38
38
  require "couchrest/model/extended_attachments"
39
39
  require "couchrest/model/class_proxy"
40
+ require "couchrest/model/proxyable"
40
41
  require "couchrest/model/collection"
41
42
  require "couchrest/model/associations"
42
43
  require "couchrest/model/configuration"
44
+ require "couchrest/model/designs"
45
+ require "couchrest/model/designs/view"
43
46
 
44
47
  # Monkey patches applied to couchrest
45
48
  require "couchrest/model/support/couchrest"
@@ -44,11 +44,11 @@ describe "Assocations" do
44
44
  end
45
45
 
46
46
  it "should raise error if class name does not exist" do
47
- lambda {
47
+ lambda do
48
48
  class TestBadAssoc < CouchRest::Model::Base
49
49
  belongs_to :test_bad_item
50
50
  end
51
- }.should raise_error
51
+ end.should raise_error(NameError, /TestBadAssoc#test_bad_item/)
52
52
  end
53
53
 
54
54
  it "should allow override of foreign key" do
@@ -34,10 +34,16 @@ describe "Model Base" do
34
34
  @obj.should be_new_record
35
35
  end
36
36
 
37
- it "should not failed on a nil value in argument" do
37
+ it "should not fail with nil argument" do
38
38
  @obj = Basic.new(nil)
39
39
  @obj.should_not be_nil
40
40
  end
41
+
42
+ it "should allow the database to be set" do
43
+ @obj = Basic.new(nil, :database => 'database')
44
+ @obj.database.should eql('database')
45
+ end
46
+
41
47
  end
42
48
 
43
49
  describe "ActiveModel compatability Basic" do
@@ -184,6 +190,14 @@ describe "Model Base" do
184
190
  obj = WithDefaultValues.new(:preset => 'test')
185
191
  obj.preset = 'test'
186
192
  end
193
+
194
+ it "should keep default values for new instances" do
195
+ obj = WithDefaultValues.new
196
+ obj.preset[:alpha] = 123
197
+ obj.preset.should == {:right => 10, :top_align => false, :alpha => 123}
198
+ another = WithDefaultValues.new
199
+ another.preset.should == {:right => 10, :top_align => false}
200
+ end
187
201
 
188
202
  it "should work with a default empty array" do
189
203
  obj = WithDefaultValues.new(:tags => ['spec'])
@@ -24,19 +24,24 @@ class DummyModel < CouchRest::Model::Base
24
24
  property :sub_models do |child|
25
25
  child.property :title
26
26
  end
27
+ property :param_free_sub_models do
28
+ property :title
29
+ end
27
30
  end
28
31
 
29
32
  class WithCastedCallBackModel < Hash
30
33
  include CouchRest::Model::CastedModel
31
34
  property :name
32
- property :run_before_validate
33
- property :run_after_validate
35
+ property :run_before_validation
36
+ property :run_after_validation
37
+
38
+ validates_presence_of :run_before_validation
34
39
 
35
- before_validate do |object|
36
- object.run_before_validate = true
40
+ before_validation do |object|
41
+ object.run_before_validation = true
37
42
  end
38
- after_validate do |object|
39
- object.run_after_validate = true
43
+ after_validation do |object|
44
+ object.run_after_validation = true
40
45
  end
41
46
  end
42
47
 
@@ -98,6 +103,14 @@ describe CouchRest::Model::CastedModel do
98
103
  @obj.sub_models << {:title => 'test'}
99
104
  @obj.sub_models.first.title.should eql('test')
100
105
  end
106
+ it "should be empty intitally (without params)" do
107
+ @obj.param_free_sub_models.should_not be_nil
108
+ @obj.param_free_sub_models.should be_empty
109
+ end
110
+ it "should be updatable using a hash (without params)" do
111
+ @obj.param_free_sub_models << {:title => 'test'}
112
+ @obj.param_free_sub_models.first.title.should eql('test')
113
+ end
101
114
  end
102
115
 
103
116
  describe "casted as attribute" do
@@ -284,6 +297,11 @@ describe CouchRest::Model::CastedModel do
284
297
  @toy2.errors.should be_empty
285
298
  @toy3.errors.should_not be_empty
286
299
  end
300
+
301
+ it "should not use dperecated ActiveModel options" do
302
+ ActiveSupport::Deprecation.should_not_receive(:warn)
303
+ @cat.should_not be_valid
304
+ end
287
305
  end
288
306
 
289
307
  describe "on a casted model property" do
@@ -423,15 +441,15 @@ describe CouchRest::Model::CastedModel do
423
441
  end
424
442
 
425
443
  describe "validate" do
426
- it "should run before_validate before validating" do
427
- @model.run_before_validate.should be_nil
444
+ it "should run before_validation before validating" do
445
+ @model.run_before_validation.should be_nil
428
446
  @model.should be_valid
429
- @model.run_before_validate.should be_true
447
+ @model.run_before_validation.should be_true
430
448
  end
431
- it "should run after_validate after validating" do
432
- @model.run_after_validate.should be_nil
449
+ it "should run after_validation after validating" do
450
+ @model.run_after_validation.should be_nil
433
451
  @model.should be_valid
434
- @model.run_after_validate.should be_true
452
+ @model.run_after_validation.should be_true
435
453
  end
436
454
  end
437
455
  end
@@ -85,12 +85,12 @@ describe "Proxy Class" do
85
85
  end
86
86
  it "should get first" do
87
87
  u = @us.first
88
- u.title.should =~ /\A...\z/
88
+ u.should == @us.all.first
89
89
  end
90
90
 
91
91
  it "should get last" do
92
92
  u = @us.last
93
- u.title.should == "aaa"
93
+ u.should == @us.all.last
94
94
  end
95
95
 
96
96
  it "should set database on first retreived document" do
@@ -0,0 +1,89 @@
1
+ require File.expand_path("../../spec_helper", __FILE__)
2
+ require File.join(FIXTURE_PATH, 'more', 'article')
3
+
4
+ describe "Collections" do
5
+
6
+ before(:all) do
7
+ reset_test_db!
8
+ Article.refresh_design_doc
9
+ titles = ["very uniq one", "really interesting", "some fun",
10
+ "really awesome", "crazy bob", "this rocks", "super rad"]
11
+ titles.each_with_index do |title,i|
12
+ a = Article.new(:title => title, :date => Date.today)
13
+ a.save
14
+ end
15
+
16
+ titles = ["yesterday very uniq one", "yesterday really interesting", "yesterday some fun",
17
+ "yesterday really awesome", "yesterday crazy bob", "yesterday this rocks"]
18
+ titles.each_with_index do |title,i|
19
+ a = Article.new(:title => title, :date => Date.today - 1)
20
+ a.save
21
+ end
22
+ end
23
+ it "should return a proxy that looks like an array of 7 Article objects" do
24
+ articles = Article.collection_proxy_for('Article', 'by_date', :descending => true,
25
+ :key => Date.today, :include_docs => true)
26
+ articles.class.should == Array
27
+ articles.size.should == 7
28
+ end
29
+ it "should provide a class method for paginate" do
30
+ articles = Article.paginate(:design_doc => 'Article', :view_name => 'by_date',
31
+ :per_page => 3, :descending => true, :key => Date.today, :include_docs => true)
32
+ articles.size.should == 3
33
+
34
+ articles = Article.paginate(:design_doc => 'Article', :view_name => 'by_date',
35
+ :per_page => 3, :page => 2, :descending => true, :key => Date.today, :include_docs => true)
36
+ articles.size.should == 3
37
+
38
+ articles = Article.paginate(:design_doc => 'Article', :view_name => 'by_date',
39
+ :per_page => 3, :page => 3, :descending => true, :key => Date.today, :include_docs => true)
40
+ articles.size.should == 1
41
+ end
42
+ it "should provide a class method for paginated_each" do
43
+ options = { :design_doc => 'Article', :view_name => 'by_date',
44
+ :per_page => 3, :page => 1, :descending => true, :key => Date.today,
45
+ :include_docs => true }
46
+ Article.paginated_each(options) do |a|
47
+ a.should_not be_nil
48
+ end
49
+ end
50
+ it "should provide a class method to get a collection for a view" do
51
+ articles = Article.find_all_article_details(:key => Date.today)
52
+ articles.class.should == Array
53
+ articles.size.should == 7
54
+ end
55
+ it "should get a subset of articles using paginate" do
56
+ articles = Article.collection_proxy_for('Article', 'by_date', :key => Date.today, :include_docs => true)
57
+ articles.paginate(:page => 1, :per_page => 3).size.should == 3
58
+ articles.paginate(:page => 2, :per_page => 3).size.should == 3
59
+ articles.paginate(:page => 3, :per_page => 3).size.should == 1
60
+ end
61
+ it "should get all articles, a few at a time, using paginated each" do
62
+ articles = Article.collection_proxy_for('Article', 'by_date', :key => Date.today, :include_docs => true)
63
+ articles.paginated_each(:per_page => 3) do |a|
64
+ a.should_not be_nil
65
+ end
66
+ end
67
+
68
+ it "should raise an exception if design_doc is not provided" do
69
+ lambda{Article.collection_proxy_for(nil, 'by_date')}.should raise_error
70
+ lambda{Article.paginate(:view_name => 'by_date')}.should raise_error
71
+ end
72
+ it "should raise an exception if view_name is not provided" do
73
+ lambda{Article.collection_proxy_for('Article', nil)}.should raise_error
74
+ lambda{Article.paginate(:design_doc => 'Article')}.should raise_error
75
+ end
76
+ it "should be able to span multiple keys" do
77
+ articles = Article.collection_proxy_for('Article', 'by_date', :startkey => Date.today - 1, :endkey => Date.today, :include_docs => true)
78
+ articles.paginate(:page => 1, :per_page => 3).size.should == 3
79
+ articles.paginate(:page => 3, :per_page => 3).size.should == 3
80
+ articles.paginate(:page => 5, :per_page => 3).size.should == 1
81
+ end
82
+ it "should pass database parameter to pager" do
83
+ proxy = mock(:proxy)
84
+ proxy.stub!(:paginate)
85
+ ::CouchRest::Model::Collection::CollectionProxy.should_receive(:new).with('database', anything(), anything(), anything(), anything()).and_return(proxy)
86
+ Article.paginate(:design_doc => 'Article', :view_name => 'by_date', :database => 'database')
87
+ end
88
+
89
+ end