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