couchrest_model 1.1.2 → 1.2.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 (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