couchrest_model 1.1.2 → 1.2.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/README.md +8 -2
  2. data/VERSION +1 -1
  3. data/couchrest_model.gemspec +2 -1
  4. data/history.md +8 -0
  5. data/lib/couchrest/model/base.rb +0 -20
  6. data/lib/couchrest/model/configuration.rb +2 -0
  7. data/lib/couchrest/model/core_extensions/time_parsing.rb +35 -9
  8. data/lib/couchrest/model/designs/design.rb +182 -0
  9. data/lib/couchrest/model/designs/view.rb +91 -48
  10. data/lib/couchrest/model/designs.rb +72 -19
  11. data/lib/couchrest/model/document_queries.rb +15 -45
  12. data/lib/couchrest/model/properties.rb +43 -2
  13. data/lib/couchrest/model/proxyable.rb +20 -54
  14. data/lib/couchrest/model/typecast.rb +1 -1
  15. data/lib/couchrest/model/validations/uniqueness.rb +7 -6
  16. data/lib/couchrest_model.rb +1 -5
  17. data/spec/fixtures/models/article.rb +22 -20
  18. data/spec/fixtures/models/base.rb +15 -7
  19. data/spec/fixtures/models/course.rb +7 -4
  20. data/spec/fixtures/models/project.rb +4 -1
  21. data/spec/fixtures/models/sale_entry.rb +5 -3
  22. data/spec/unit/base_spec.rb +51 -5
  23. data/spec/unit/core_extensions/time_parsing.rb +41 -0
  24. data/spec/unit/designs/design_spec.rb +291 -0
  25. data/spec/unit/designs/view_spec.rb +135 -40
  26. data/spec/unit/designs_spec.rb +341 -30
  27. data/spec/unit/dirty_spec.rb +67 -0
  28. data/spec/unit/inherited_spec.rb +2 -2
  29. data/spec/unit/property_protection_spec.rb +3 -1
  30. data/spec/unit/property_spec.rb +43 -3
  31. data/spec/unit/proxyable_spec.rb +57 -98
  32. data/spec/unit/subclass_spec.rb +14 -5
  33. data/spec/unit/validations_spec.rb +14 -12
  34. metadata +172 -129
  35. data/lib/couchrest/model/class_proxy.rb +0 -135
  36. data/lib/couchrest/model/collection.rb +0 -273
  37. data/lib/couchrest/model/design_doc.rb +0 -115
  38. data/lib/couchrest/model/support/couchrest_design.rb +0 -33
  39. data/lib/couchrest/model/views.rb +0 -148
  40. data/spec/unit/class_proxy_spec.rb +0 -167
  41. data/spec/unit/collection_spec.rb +0 -86
  42. data/spec/unit/design_doc_spec.rb +0 -212
  43. data/spec/unit/view_spec.rb +0 -352
@@ -19,15 +19,32 @@ module CouchRest
19
19
  module Designs
20
20
  extend ActiveSupport::Concern
21
21
 
22
+
22
23
  module ClassMethods
23
24
 
24
25
  # Add views and other design document features
25
26
  # to the current model.
26
- def design(*args, &block)
27
- mapper = DesignMapper.new(self)
28
- mapper.create_view_method(:all)
27
+ def design(prefix = nil, &block)
28
+
29
+ # Store ourselves a copy of this design spec incase any other model inherits.
30
+ (@_design_blocks ||= [ ]) << {:args => [prefix], :block => block}
29
31
 
32
+ mapper = DesignMapper.new(self, prefix)
30
33
  mapper.instance_eval(&block) if block_given?
34
+
35
+ # Create an 'all' view if no prefix and one has not been defined already
36
+ mapper.view(:all) if prefix.nil? and !mapper.design_doc.has_view?(:all)
37
+ end
38
+
39
+ def inherited(model)
40
+ super
41
+
42
+ # Go through our design blocks and re-implement them in the child.
43
+ unless @_design_blocks.nil?
44
+ @_design_blocks.each do |row|
45
+ model.design(*row[:args], &row[:block])
46
+ end
47
+ end
31
48
  end
32
49
 
33
50
  # Override the default page pagination value:
@@ -47,22 +64,43 @@ module CouchRest
47
64
  @_default_per_page || 25
48
65
  end
49
66
 
67
+ def design_docs
68
+ @_design_docs ||= []
69
+ end
70
+
50
71
  end
51
72
 
52
- #
73
+ # Map method calls defined in a design block to actions
74
+ # in the Design Document.
53
75
  class DesignMapper
54
76
 
55
- attr_accessor :model
77
+ # Basic mapper attributes
78
+ attr_accessor :model, :method, :prefix
56
79
 
57
- def initialize(model)
58
- self.model = model
80
+ # Temporary variable storing the design doc
81
+ attr_accessor :design_doc
82
+
83
+ def initialize(model, prefix = nil)
84
+ self.model = model
85
+ self.prefix = prefix
86
+ self.method = Design.method_name(prefix)
87
+
88
+ create_model_design_doc_reader
89
+ self.design_doc = model.send(method) || assign_model_design_doc
59
90
  end
60
91
 
61
- # Define a view and generate a method that will provide a new
62
- # View instance when requested.
92
+ def disable_auto_update
93
+ design_doc.auto_update = false
94
+ end
95
+
96
+ def enable_auto_update
97
+ design_doc.auto_update = true
98
+ end
99
+
100
+ # Add the specified view to the design doc the definition was made in
101
+ # and create quick access methods in the model.
63
102
  def view(name, opts = {})
64
- View.create(model, name, opts)
65
- create_view_method(name)
103
+ design_doc.create_view(name, opts)
66
104
  end
67
105
 
68
106
  # Really simple design function that allows a filter
@@ -72,16 +110,31 @@ module CouchRest
72
110
  # No methods are created here, the design is simply updated.
73
111
  # See the CouchDB API for more information on how to use this.
74
112
  def filter(name, function)
75
- filters = (self.model.design_doc['filters'] ||= {})
76
- filters[name.to_s] = function
113
+ design_doc.create_filter(name, function)
77
114
  end
78
115
 
79
- def create_view_method(name)
80
- model.class_eval <<-EOS, __FILE__, __LINE__ + 1
81
- def self.#{name}(opts = {})
82
- CouchRest::Model::Designs::View.new(self, opts, '#{name}')
83
- end
84
- EOS
116
+ # Convenience wrapper to access model's type key option.
117
+ def model_type_key
118
+ model.model_type_key
119
+ end
120
+
121
+ protected
122
+
123
+ # Create accessor in model and assign a new design doc.
124
+ # New design doc is returned ready to use.
125
+ def create_model_design_doc_reader
126
+ model.instance_eval "def #{method}; @#{method}; end"
127
+ end
128
+
129
+ def assign_model_design_doc
130
+ doc = Design.new(model, prefix)
131
+ model.instance_variable_set("@#{method}", doc)
132
+ model.design_docs << doc
133
+
134
+ # Set defaults
135
+ doc.auto_update = model.auto_update_design_doc
136
+
137
+ doc
85
138
  end
86
139
 
87
140
  end
@@ -5,52 +5,22 @@ module CouchRest
5
5
 
6
6
  module ClassMethods
7
7
 
8
- # Load all documents that have the model_type_key's field equal to the
9
- # name of the current class. Take the standard set of
10
- # CouchRest::Database#view options.
11
- def all(opts = {}, &block)
12
- view(:all, opts, &block)
8
+ # Wrapper for the master design documents all method to provide
9
+ # a total count of entries.
10
+ def count
11
+ all.count
13
12
  end
14
-
15
- # Returns the number of documents that have the model_type_key's field
16
- # equal to the name of the current class. Takes the standard set of
17
- # CouchRest::Database#view options
18
- def count(opts = {}, &block)
19
- all({:raw => true, :limit => 0}.merge(opts), &block)['total_rows']
20
- end
21
-
22
- # Load the first document that have the model_type_key's field equal to
23
- # the name of the current class.
24
- #
25
- # ==== Returns
26
- # Object:: The first object instance available
27
- # or
28
- # Nil:: if no instances available
29
- #
30
- # ==== Parameters
31
- # opts<Hash>::
32
- # View options, see <tt>CouchRest::Database#view</tt> options for more info.
33
- def first(opts = {})
34
- first_instance = self.all(opts.merge!(:limit => 1))
35
- first_instance.empty? ? nil : first_instance.first
13
+
14
+ # Wrapper for the master design document's first method on all view.
15
+ def first
16
+ all.first
36
17
  end
37
-
38
- # Load the last document that have the model_type_key's field equal to
39
- # the name of the current class.
40
- # It's similar to method first, just adds :descending => true
41
- #
42
- # ==== Returns
43
- # Object:: The last object instance available
44
- # or
45
- # Nil:: if no instances available
46
- #
47
- # ==== Parameters
48
- # opts<Hash>::
49
- # View options, see <tt>CouchRest::Database#view</tt> options for more info.
50
- def last(opts = {})
51
- first(opts.merge!(:descending => true))
18
+
19
+ # Wrapper for the master design document's last method on all view.
20
+ def last
21
+ all.last
52
22
  end
53
-
23
+
54
24
  # Load a document from the database by id
55
25
  # No exceptions will be raised if the document isn't found
56
26
  #
@@ -91,9 +61,9 @@ module CouchRest
91
61
  raise CouchRest::Model::DocumentNotFound
92
62
  end
93
63
  alias :find! :get!
94
-
64
+
95
65
  end
96
-
66
+
97
67
  end
98
68
  end
99
69
  end
@@ -106,13 +106,22 @@ module CouchRest
106
106
  # that have not been accepted.
107
107
  def directly_set_attributes(hash, mass_assign = false)
108
108
  return if hash.nil?
109
+
110
+ multi_parameter_attributes = []
111
+
109
112
  hash.reject do |key, value|
110
- if self.respond_to?("#{key}=")
111
- self.send("#{key}=", value)
113
+ if key.to_s.include?("(")
114
+ multi_parameter_attributes << [ key, value ]
115
+ false
116
+ elsif self.respond_to?("#{key}=")
117
+ self.send("#{key}=", value)
112
118
  elsif mass_assign || mass_assign_any_attribute
119
+ couchrest_attribute_will_change!(key) if use_dirty? && self[key] != value
113
120
  self[key] = value
114
121
  end
115
122
  end
123
+
124
+ assign_multiparameter_attributes(multi_parameter_attributes, hash) unless multi_parameter_attributes.empty?
116
125
  end
117
126
 
118
127
  def directly_set_read_only_attributes(hash)
@@ -125,7 +134,39 @@ module CouchRest
125
134
  end
126
135
  end
127
136
 
137
+ def assign_multiparameter_attributes(pairs, hash)
138
+ execute_callstack_for_multiparameter_attributes(
139
+ extract_callstack_for_multiparameter_attributes(pairs), hash
140
+ )
141
+ end
142
+ def execute_callstack_for_multiparameter_attributes(callstack, hash)
143
+ callstack.each do |name, values_with_empty_parameters|
144
+ if self.respond_to?("#{name}=")
145
+ casted_attrib = send("#{name}=", values_with_empty_parameters)
146
+ unless casted_attrib.is_a?(Hash)
147
+ hash.reject { |key, value| key.include?(name.to_s)}
148
+ end
149
+ end
150
+ end
151
+ hash
152
+ end
153
+
154
+ def extract_callstack_for_multiparameter_attributes(pairs)
155
+ attributes = { }
156
+
157
+ pairs.each do |pair|
158
+ multiparameter_name, value = pair
159
+ attribute_name = multiparameter_name.split("(").first
160
+ attributes[attribute_name] = {} unless attributes.include?(attribute_name)
161
+ attributes[attribute_name][find_parameter_name(multiparameter_name)] ||= value
162
+ end
163
+ attributes
164
+ end
128
165
 
166
+ def find_parameter_name(multiparameter_name)
167
+ position = multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
168
+ {1 => :year, 2 => :month, 3 => :day, 4 => :hour, 5 => :min, 6 => :sec}[position]
169
+ end
129
170
 
130
171
  module ClassMethods
131
172
 
@@ -70,6 +70,8 @@ module CouchRest
70
70
  @owner = owner
71
71
  @owner_name = owner_name
72
72
  @database = database
73
+
74
+ create_view_methods
73
75
  end
74
76
 
75
77
  # Base
@@ -81,39 +83,18 @@ module CouchRest
81
83
  proxy_block_update(:build_from_database, attrs, options, &block)
82
84
  end
83
85
 
84
- def method_missing(m, *args, &block)
85
- if has_view?(m)
86
- if model.respond_to?(m)
87
- return model.send(m, *args).proxy(self)
88
- else
89
- query = args.shift || {}
90
- return view(m, query, *args, &block)
91
- end
92
- elsif m.to_s =~ /^find_(by_.+)/
93
- view_name = $1
94
- if has_view?(view_name)
95
- return first_from_view(view_name, *args)
96
- end
97
- end
98
- super
99
- end
100
-
101
- # DocumentQueries
102
-
103
- def all(opts = {}, &block)
104
- proxy_update_all(@model.all({:database => @database}.merge(opts), &block))
105
- end
86
+ # From DocumentQueries (The old fashioned way)
106
87
 
107
88
  def count(opts = {})
108
- @model.count({:database => @database}.merge(opts))
89
+ all(opts).count
109
90
  end
110
91
 
111
92
  def first(opts = {})
112
- proxy_update(@model.first({:database => @database}.merge(opts)))
93
+ all(opts).first
113
94
  end
114
95
 
115
96
  def last(opts = {})
116
- proxy_update(@model.last({:database => @database}.merge(opts)))
97
+ all(opts).last
117
98
  end
118
99
 
119
100
  def get(id)
@@ -121,38 +102,23 @@ module CouchRest
121
102
  end
122
103
  alias :find :get
123
104
 
124
- # Views
125
-
126
- def has_view?(view)
127
- @model.has_view?(view)
128
- end
129
-
130
- def view_by(*args)
131
- @model.view_by(*args)
132
- end
133
-
134
- def view(name, query={}, &block)
135
- proxy_update_all(@model.view(name, {:database => @database}.merge(query), &block))
136
- end
137
-
138
- def first_from_view(name, *args)
139
- # add to first hash available, or add to end
140
- (args.last.is_a?(Hash) ? args.last : (args << {}).last)[:database] = @database
141
- proxy_update(@model.first_from_view(name, *args))
142
- end
143
-
144
- # DesignDoc
145
- def design_doc
146
- @model.design_doc
147
- end
105
+ protected
148
106
 
149
- def save_design_doc(db = nil)
150
- @model.save_design_doc(db || @database)
107
+ def create_view_methods
108
+ model.design_docs.each do |doc|
109
+ doc.view_names.each do |name|
110
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
111
+ def #{name}(opts = {})
112
+ model.#{name}({:proxy => self}.merge(opts))
113
+ end
114
+ def find_#{name}(*key)
115
+ #{name}.key(*key).first()
116
+ end
117
+ EOS
118
+ end
119
+ end
151
120
  end
152
121
 
153
-
154
- protected
155
-
156
122
  # Update the document's proxy details, specifically, the fields that
157
123
  # link back to the original document.
158
124
  def proxy_update(doc)
@@ -123,7 +123,7 @@ module CouchRest
123
123
 
124
124
  # Creates a Date instance from a Hash with keys :year, :month, :day
125
125
  def typecast_hash_to_date(value)
126
- Date.new(*extract_time(value)[0, 3])
126
+ Date.new(*extract_time(value)[0, 3].map(&:to_i))
127
127
  end
128
128
 
129
129
  # Creates a Time instance from a Hash with keys :year, :month, :day,
@@ -15,9 +15,10 @@ module CouchRest
15
15
  attributes.each do |attribute|
16
16
  opts = merge_view_options(attribute)
17
17
 
18
- if model.respond_to?(:has_view?) && !model.has_view?(opts[:view_name])
19
- opts[:keys] << {:allow_nil => true}
20
- model.view_by(*opts[:keys])
18
+ unless model.respond_to?(opts[:view_name])
19
+ model.design do
20
+ view opts[:view_name], :allow_nil => true
21
+ end
21
22
  end
22
23
  end
23
24
  end
@@ -33,15 +34,15 @@ module CouchRest
33
34
  # Determine the base of the search
34
35
  base = opts[:proxy].nil? ? model : document.instance_eval(opts[:proxy])
35
36
 
36
- if base.respond_to?(:has_view?) && !base.has_view?(opts[:view_name])
37
+ unless base.respond_to?(opts[:view_name])
37
38
  raise "View #{document.class.name}.#{opts[:view_name]} does not exist for validation!"
38
39
  end
39
40
 
40
- rows = base.view(opts[:view_name], :key => values, :limit => 2, :include_docs => false)['rows']
41
+ rows = base.send(opts[:view_name], :key => values, :limit => 2, :include_docs => false).rows
41
42
  return if rows.empty?
42
43
 
43
44
  unless document.new?
44
- return if rows.find{|row| row['id'] == document.id}
45
+ return if rows.find{|row| row.id == document.id}
45
46
  end
46
47
 
47
48
  if rows.length > 0
@@ -36,20 +36,16 @@ require "couchrest/model/casted_hash"
36
36
  require "couchrest/model/validations"
37
37
  require "couchrest/model/callbacks"
38
38
  require "couchrest/model/document_queries"
39
- require "couchrest/model/views"
40
- require "couchrest/model/design_doc"
41
39
  require "couchrest/model/extended_attachments"
42
- require "couchrest/model/class_proxy"
43
40
  require "couchrest/model/proxyable"
44
- require "couchrest/model/collection"
45
41
  require "couchrest/model/associations"
46
42
  require "couchrest/model/configuration"
47
43
  require "couchrest/model/connection"
48
44
  require "couchrest/model/designs"
45
+ require "couchrest/model/designs/design"
49
46
  require "couchrest/model/designs/view"
50
47
 
51
48
  # Monkey patches applied to couchrest
52
- require "couchrest/model/support/couchrest_design"
53
49
  require "couchrest/model/support/couchrest_database"
54
50
 
55
51
  # Core Extensions
@@ -1,24 +1,26 @@
1
1
  class Article < CouchRest::Model::Base
2
2
  use_database DB
3
3
  unique_id :slug
4
-
5
- provides_collection :article_details, 'Article', 'by_date', :descending => true, :include_docs => true
6
- view_by :date, :descending => true
7
- view_by :user_id, :date
8
-
9
- view_by :tags,
10
- :map =>
11
- "function(doc) {
12
- if (doc['#{model_type_key}'] == 'Article' && doc.tags) {
13
- doc.tags.forEach(function(tag){
14
- emit(tag, 1);
15
- });
16
- }
17
- }",
18
- :reduce =>
19
- "function(keys, values, rereduce) {
20
- return sum(values);
21
- }"
4
+
5
+ design do
6
+ view :by_date # Default options not supported: :descending => true
7
+ view :by_user_id_and_date
8
+
9
+ view :by_tags,
10
+ :map =>
11
+ "function(doc) {
12
+ if (doc['#{model.model_type_key}'] == 'Article' && doc.tags) {
13
+ doc.tags.forEach(function(tag){
14
+ emit(tag, 1);
15
+ });
16
+ }
17
+ }",
18
+ :reduce =>
19
+ "function(keys, values, rereduce) {
20
+ return sum(values);
21
+ }"
22
+
23
+ end
22
24
 
23
25
  property :date, Date
24
26
  property :slug, :read_only => true
@@ -27,9 +29,9 @@ class Article < CouchRest::Model::Base
27
29
  property :tags, [String]
28
30
 
29
31
  timestamps!
30
-
32
+
31
33
  before_save :generate_slug_from_title
32
-
34
+
33
35
  def generate_slug_from_title
34
36
  self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new?
35
37
  end
@@ -1,5 +1,5 @@
1
1
  class WithDefaultValues < CouchRest::Model::Base
2
- use_database TEST_SERVER.default_database
2
+ use_database DB
3
3
  property :preset, Object, :default => {:right => 10, :top_align => false}
4
4
  property :set_by_proc, Time, :default => Proc.new{Time.now}
5
5
  property :tags, [String], :default => []
@@ -10,7 +10,7 @@ class WithDefaultValues < CouchRest::Model::Base
10
10
  end
11
11
 
12
12
  class WithSimplePropertyType < CouchRest::Model::Base
13
- use_database TEST_SERVER.default_database
13
+ use_database DB
14
14
  property :name, String
15
15
  property :preset, String, :default => 'none'
16
16
  property :tags, [String]
@@ -18,7 +18,7 @@ class WithSimplePropertyType < CouchRest::Model::Base
18
18
  end
19
19
 
20
20
  class WithCallBacks < CouchRest::Model::Base
21
- use_database TEST_SERVER.default_database
21
+ use_database DB
22
22
  property :name
23
23
  property :run_before_validation
24
24
  property :run_after_validation
@@ -85,27 +85,30 @@ end
85
85
 
86
86
  # Following two fixture classes have __intentionally__ diffent syntax for setting the validation context
87
87
  class WithContextualValidationOnCreate < CouchRest::Model::Base
88
+ use_database DB
88
89
  property(:name, String)
89
90
  validates(:name, :presence => {:on => :create})
90
91
  end
91
92
 
92
93
  class WithContextualValidationOnUpdate < CouchRest::Model::Base
94
+ use_database DB
93
95
  property(:name, String)
94
96
  validates(:name, :presence => true, :on => :update)
95
97
  end
96
98
 
97
99
  class WithTemplateAndUniqueID < CouchRest::Model::Base
98
- use_database TEST_SERVER.default_database
100
+ use_database DB
99
101
  unique_id do |model|
100
102
  model.slug
101
103
  end
102
104
  property :slug
103
105
  property :preset, :default => 'value'
104
106
  property :has_no_default
107
+ design
105
108
  end
106
109
 
107
110
  class WithGetterAndSetterMethods < CouchRest::Model::Base
108
- use_database TEST_SERVER.default_database
111
+ use_database DB
109
112
 
110
113
  property :other_arg
111
114
  def arg
@@ -118,7 +121,7 @@ class WithGetterAndSetterMethods < CouchRest::Model::Base
118
121
  end
119
122
 
120
123
  class WithAfterInitializeMethod < CouchRest::Model::Base
121
- use_database TEST_SERVER.default_database
124
+ use_database DB
122
125
 
123
126
  property :some_value
124
127
 
@@ -147,6 +150,7 @@ class WithUniqueValidationView < CouchRest::Model::Base
147
150
  end
148
151
  property :title
149
152
 
153
+ design
150
154
  validates_uniqueness_of :code, :view => 'all'
151
155
  end
152
156
 
@@ -159,4 +163,8 @@ class WithScopedUniqueValidation < CouchRest::Model::Base
159
163
  validates_uniqueness_of :title, :scope => :parent_id
160
164
  end
161
165
 
162
-
166
+ class WithDateAndTime < CouchRest::Model::Base
167
+ use_database DB
168
+ property :exec_date, Date
169
+ property :exec_time, Time
170
+ end
@@ -18,10 +18,13 @@ class Course < CouchRest::Model::Base
18
18
  property :very_active, :type => TrueClass
19
19
  property :klass, :type => Class
20
20
 
21
- view_by :title
22
- view_by :title, :active
23
- view_by :dept, :ducktype => true
21
+ design do
22
+ view :by_title
23
+ view :by_title_and_active
24
24
 
25
- view_by :active, :map => "function(d) { if (d['#{model_type_key}'] == 'Course' && d['active']) { emit(d['updated_at'], 1); }}", :reduce => "function(k,v,r) { return sum(v); }"
25
+ view :by_dept, :ducktype => true
26
+
27
+ view :by_active, :map => "function(d) { if (d['#{model_type_key}'] == 'Course' && d['active']) { emit(d['updated_at'], 1); }}", :reduce => "function(k,v,r) { return sum(v); }"
28
+ end
26
29
 
27
30
  end
@@ -2,5 +2,8 @@ class Project < CouchRest::Model::Base
2
2
  use_database DB
3
3
  property :name, String
4
4
  timestamps!
5
- view_by :name
5
+
6
+ design do
7
+ view :by_name
8
+ end
6
9
  end
@@ -3,7 +3,9 @@ class SaleEntry < CouchRest::Model::Base
3
3
 
4
4
  property :description
5
5
  property :price
6
+
7
+ design do
8
+ view :by_description
9
+ end
6
10
 
7
- view_by :description
8
-
9
- end
11
+ end