mattetti-couchrest 0.33 → 0.34

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,14 +1,5 @@
1
1
  module CouchRest
2
2
  module Mixins
3
- module PaginatedResults
4
- def amount_pages
5
- @amount_pages ||= 0
6
- end
7
- def amount_pages=(value)
8
- @amount_pages = value
9
- end
10
- end
11
-
12
3
  module Collection
13
4
 
14
5
  def self.included(base)
@@ -93,8 +84,6 @@ module CouchRest
93
84
  DEFAULT_PAGE = 1
94
85
  DEFAULT_PER_PAGE = 30
95
86
 
96
- attr_accessor :amount_pages
97
-
98
87
  # Create a new CollectionProxy to represent the specified view. If a
99
88
  # container class is specified, the proxy will create an object of the
100
89
  # given type for each row that comes back from the view. If no
@@ -122,11 +111,8 @@ module CouchRest
122
111
  def paginate(options = {})
123
112
  page, per_page = parse_options(options)
124
113
  results = @database.view(@view_name, pagination_options(page, per_page))
125
- @amount_pages ||= (results['total_rows'].to_f / per_page.to_f).ceil
126
114
  remember_where_we_left_off(results, page)
127
115
  results = convert_to_container_array(results)
128
- results.extend(PaginatedResults)
129
- results.amount_pages = @amount_pages
130
116
  results
131
117
  end
132
118
 
@@ -204,8 +190,9 @@ module CouchRest
204
190
  def pagination_options(page, per_page)
205
191
  view_options = @view_options.clone
206
192
  if @last_key && @last_docid && @last_page == page - 1
207
- view_options.delete(:key)
208
- options = { :startkey => @last_key, :startkey_docid => @last_docid, :limit => per_page, :skip => 1 }
193
+ key = view_options.delete(:key)
194
+ end_key = view_options[:endkey] || key
195
+ options = { :startkey => @last_key, :endkey => end_key, :startkey_docid => @last_docid, :limit => per_page, :skip => 1 }
209
196
  else
210
197
  options = { :limit => per_page, :skip => per_page * (page - 1) }
211
198
  end
@@ -64,7 +64,7 @@ module CouchRest
64
64
  def set_attachment_attr(args)
65
65
  content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file])
66
66
  self['_attachments'][args[:name]] = {
67
- 'content-type' => content_type,
67
+ 'content_type' => content_type,
68
68
  'data' => encode_attachment(args[:file].read)
69
69
  }
70
70
  end
@@ -27,7 +27,7 @@ module CouchRest
27
27
  class IncludeError < StandardError; end
28
28
 
29
29
  def self.included(base)
30
- base.class_eval <<-EOS, __FILE__, __LINE__
30
+ base.class_eval <<-EOS, __FILE__, __LINE__ + 1
31
31
  extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
32
32
  self.properties ||= []
33
33
  EOS
@@ -36,7 +36,7 @@ module CouchRest
36
36
  end
37
37
 
38
38
  def apply_defaults
39
- return if self.respond_to?(:new_document?) && (new_document? == false)
39
+ return if self.respond_to?(:new?) && (new? == false)
40
40
  return unless self.class.respond_to?(:properties)
41
41
  return if self.class.properties.empty?
42
42
  # TODO: cache the default object
@@ -56,50 +56,76 @@ module CouchRest
56
56
  def cast_keys
57
57
  return unless self.class.properties
58
58
  self.class.properties.each do |property|
59
- next unless property.casted
60
- key = self.has_key?(property.name) ? property.name : property.name.to_sym
61
- # Don't cast the property unless it has a value
62
- next unless self[key]
63
- target = property.type
64
- if target.is_a?(Array)
65
- klass = ::CouchRest.constantize(target[0])
66
- self[property.name] = self[key].collect do |value|
67
- # Auto parse Time objects
68
- obj = ( (property.init_method == 'new') && klass == Time) ? Time.parse(value) : klass.send(property.init_method, value)
69
- obj.casted_by = self if obj.respond_to?(:casted_by)
70
- obj
59
+ cast_property(property)
60
+ end
61
+ end
62
+
63
+ def cast_property(property, assigned=false)
64
+ return unless property.casted
65
+ key = self.has_key?(property.name) ? property.name : property.name.to_sym
66
+ # Don't cast the property unless it has a value
67
+ return unless self[key]
68
+ if property.type.is_a?(Array)
69
+ klass = ::CouchRest.constantize(property.type[0])
70
+ arr = self[key].dup.collect do |value|
71
+ unless value.instance_of?(klass)
72
+ value = convert_property_value(property, klass, value)
71
73
  end
74
+ associate_casted_to_parent(value, assigned)
75
+ value
76
+ end
77
+ self[key] = klass != String ? CastedArray.new(arr) : arr
78
+ self[key].casted_by = self if self[key].respond_to?(:casted_by)
79
+ else
80
+ if property.type == 'boolean'
81
+ klass = TrueClass
72
82
  else
73
- # Auto parse Time objects
74
- self[property.name] = if ((property.init_method == 'new') && target == 'Time')
75
- # Using custom time parsing method because Ruby's default method is toooo slow
76
- self[key].is_a?(String) ? Time.mktime_with_offset(self[key].dup) : self[key]
77
- # Float instances don't get initialized with #new
78
- elsif ((property.init_method == 'new') && target == 'Float')
79
- cast_float(self[key])
80
- # 'boolean' type is simply used to generate a property? accessor method
81
- elsif ((property.init_method == 'new') && target == 'boolean')
82
- self[key]
83
- else
84
- # Let people use :send as a Time parse arg
85
- klass = ::CouchRest.constantize(target)
86
- klass.send(property.init_method, self[key].dup)
87
- end
88
- self[property.name].casted_by = self if self[property.name].respond_to?(:casted_by)
89
- end
83
+ klass = ::CouchRest.constantize(property.type)
84
+ end
90
85
 
91
- end
92
-
93
- def cast_float(value)
94
- begin
95
- Float(value)
96
- rescue
97
- value
86
+ unless self[key].instance_of?(klass)
87
+ self[key] = convert_property_value(property, klass, self[property.name])
98
88
  end
89
+ associate_casted_to_parent(self[property.name], assigned)
99
90
  end
100
91
 
101
92
  end
102
93
 
94
+ def associate_casted_to_parent(casted, assigned)
95
+ casted.casted_by = self if casted.respond_to?(:casted_by)
96
+ casted.document_saved = true if !assigned && casted.respond_to?(:document_saved)
97
+ end
98
+
99
+ def convert_property_value(property, klass, value)
100
+ if ((property.init_method == 'new') && klass == Time)
101
+ # Using custom time parsing method because Ruby's default method is toooo slow
102
+ value.is_a?(String) ? Time.mktime_with_offset(value.dup) : value
103
+ # Float instances don't get initialized with #new
104
+ elsif ((property.init_method == 'new') && klass == Float)
105
+ cast_float(value)
106
+ # 'boolean' type is simply used to generate a property? accessor method
107
+ elsif ((property.init_method == 'new') && klass == TrueClass)
108
+ value
109
+ else
110
+ klass.send(property.init_method, value.dup)
111
+ end
112
+ end
113
+
114
+ def cast_property_by_name(property_name)
115
+ return unless self.class.properties
116
+ property = self.class.properties.detect{|property| property.name == property_name}
117
+ return unless property
118
+ cast_property(property, true)
119
+ end
120
+
121
+ def cast_float(value)
122
+ begin
123
+ Float(value)
124
+ rescue
125
+ value
126
+ end
127
+ end
128
+
103
129
  module ClassMethods
104
130
 
105
131
  def property(name, options={})
@@ -125,7 +151,7 @@ module CouchRest
125
151
  # defines the getter for the property (and optional aliases)
126
152
  def create_property_getter(property)
127
153
  # meth = property.name
128
- class_eval <<-EOS, __FILE__, __LINE__
154
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
129
155
  def #{property.name}
130
156
  self['#{property.name}']
131
157
  end
@@ -144,7 +170,7 @@ module CouchRest
144
170
  end
145
171
 
146
172
  if property.alias
147
- class_eval <<-EOS, __FILE__, __LINE__
173
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
148
174
  alias #{property.alias.to_sym} #{property.name.to_sym}
149
175
  EOS
150
176
  end
@@ -152,16 +178,17 @@ module CouchRest
152
178
 
153
179
  # defines the setter for the property (and optional aliases)
154
180
  def create_property_setter(property)
155
- meth = property.name
181
+ property_name = property.name
156
182
  class_eval <<-EOS
157
- def #{meth}=(value)
158
- self['#{meth}'] = value
183
+ def #{property_name}=(value)
184
+ self['#{property_name}'] = value
185
+ cast_property_by_name('#{property_name}')
159
186
  end
160
187
  EOS
161
188
 
162
189
  if property.alias
163
190
  class_eval <<-EOS
164
- alias #{property.alias.to_sym}= #{meth.to_sym}=
191
+ alias #{property.alias.to_sym}= #{property_name.to_sym}=
165
192
  EOS
166
193
  end
167
194
  end
@@ -50,7 +50,10 @@ module CouchRest
50
50
 
51
51
  def self.included(base)
52
52
  base.extlib_inheritable_accessor(:auto_validation)
53
- base.class_eval <<-EOS, __FILE__, __LINE__
53
+ base.class_eval <<-EOS, __FILE__, __LINE__ + 1
54
+ # Callbacks
55
+ define_callbacks :validate
56
+
54
57
  # Turn off auto validation by default
55
58
  self.auto_validation ||= false
56
59
 
@@ -71,9 +74,10 @@ module CouchRest
71
74
  EOS
72
75
 
73
76
  base.extend(ClassMethods)
74
- base.class_eval <<-EOS, __FILE__, __LINE__
77
+ base.class_eval <<-EOS, __FILE__, __LINE__ + 1
78
+ define_callbacks :validate
75
79
  if method_defined?(:_run_save_callbacks)
76
- save_callback :before, :check_validations
80
+ set_callback :save, :before, :check_validations
77
81
  end
78
82
  EOS
79
83
  base.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
@@ -115,8 +119,7 @@ module CouchRest
115
119
  # Check if a resource is valid in a given context
116
120
  #
117
121
  def valid?(context = :default)
118
- result = self.class.validators.execute(context, self)
119
- result && validate_casted_arrays
122
+ recursive_valid?(self, context, true)
120
123
  end
121
124
 
122
125
  # checking on casted objects
@@ -133,29 +136,24 @@ module CouchRest
133
136
  result
134
137
  end
135
138
 
136
- # Begin a recursive walk of the model checking validity
137
- #
138
- def all_valid?(context = :default)
139
- recursive_valid?(self, context, true)
140
- end
141
-
142
139
  # Do recursive validity checking
143
140
  #
144
141
  def recursive_valid?(target, context, state)
145
142
  valid = state
146
- target.instance_variables.each do |ivar|
147
- ivar_value = target.instance_variable_get(ivar)
148
- if ivar_value.validatable?
149
- valid = valid && recursive_valid?(ivar_value, context, valid)
150
- elsif ivar_value.respond_to?(:each)
151
- ivar_value.each do |item|
143
+ target.each do |key, prop|
144
+ if prop.is_a?(Array)
145
+ prop.each do |item|
152
146
  if item.validatable?
153
- valid = valid && recursive_valid?(item, context, valid)
147
+ valid = recursive_valid?(item, context, valid) && valid
154
148
  end
155
149
  end
150
+ elsif prop.validatable?
151
+ valid = recursive_valid?(prop, context, valid) && valid
156
152
  end
157
153
  end
158
- return valid && target.valid?
154
+ target._run_validate_callbacks do
155
+ target.class.validators.execute(context, target) && valid
156
+ end
159
157
  end
160
158
 
161
159
 
@@ -212,21 +210,12 @@ module CouchRest
212
210
  def create_context_instance_methods(context)
213
211
  name = "valid_for_#{context.to_s}?" # valid_for_signup?
214
212
  if !self.instance_methods.include?(name)
215
- class_eval <<-EOS, __FILE__, __LINE__
213
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
216
214
  def #{name} # def valid_for_signup?
217
215
  valid?('#{context.to_s}'.to_sym) # valid?('signup'.to_sym)
218
216
  end # end
219
217
  EOS
220
218
  end
221
-
222
- all = "all_valid_for_#{context.to_s}?" # all_valid_for_signup?
223
- if !self.instance_methods.include?(all)
224
- class_eval <<-EOS, __FILE__, __LINE__
225
- def #{all} # def all_valid_for_signup?
226
- all_valid?('#{context.to_s}'.to_sym) # all_valid?('signup'.to_sym)
227
- end # end
228
- EOS
229
- end
230
219
  end
231
220
 
232
221
  # Create a new validator of the given klazz and push it onto the
@@ -5,8 +5,10 @@ module CouchRest
5
5
  module CastedModel
6
6
 
7
7
  def self.included(base)
8
+ base.send(:include, ::CouchRest::Callbacks)
8
9
  base.send(:include, ::CouchRest::Mixins::Properties)
9
10
  base.send(:attr_accessor, :casted_by)
11
+ base.send(:attr_accessor, :document_saved)
10
12
  end
11
13
 
12
14
  def initialize(keys={})
@@ -26,5 +28,31 @@ module CouchRest
26
28
  def [] key
27
29
  super(key.to_s)
28
30
  end
31
+
32
+ # Gets a reference to the top level extended
33
+ # document that a model is saved inside of
34
+ def base_doc
35
+ return nil unless @casted_by
36
+ @casted_by.base_doc
37
+ end
38
+
39
+ # False if the casted model has already
40
+ # been saved in the containing document
41
+ def new?
42
+ !@document_saved
43
+ end
44
+ alias :new_record? :new?
45
+
46
+ # Sets the attributes from a hash
47
+ def update_attributes_without_saving(hash)
48
+ hash.each do |k, v|
49
+ raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=")
50
+ end
51
+ hash.each do |k, v|
52
+ self.send("#{k}=",v)
53
+ end
54
+ end
55
+ alias :attributes= :update_attributes_without_saving
56
+
29
57
  end
30
- end
58
+ end
@@ -21,7 +21,7 @@ module CouchRest
21
21
 
22
22
  def self.inherited(subklass)
23
23
  subklass.send(:include, CouchRest::Mixins::Properties)
24
- subklass.class_eval <<-EOS, __FILE__, __LINE__
24
+ subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
25
25
  def self.inherited(subklass)
26
26
  subklass.properties = self.properties.dup
27
27
  end
@@ -33,10 +33,10 @@ module CouchRest
33
33
  attr_accessor :casted_by
34
34
 
35
35
  # Callbacks
36
- define_callbacks :create
37
- define_callbacks :save
38
- define_callbacks :update
39
- define_callbacks :destroy
36
+ define_callbacks :create, "result == :halt"
37
+ define_callbacks :save, "result == :halt"
38
+ define_callbacks :update, "result == :halt"
39
+ define_callbacks :destroy, "result == :halt"
40
40
 
41
41
  def initialize(passed_keys={})
42
42
  apply_defaults # defined in CouchRest::Mixins::Properties
@@ -76,17 +76,17 @@ module CouchRest
76
76
  # on the document whenever saving occurs. CouchRest uses a pretty
77
77
  # decent time format by default. See Time#to_json
78
78
  def self.timestamps!
79
- class_eval <<-EOS, __FILE__, __LINE__
79
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
80
80
  property(:updated_at, :read_only => true, :cast_as => 'Time', :auto_validation => false)
81
81
  property(:created_at, :read_only => true, :cast_as => 'Time', :auto_validation => false)
82
82
 
83
- save_callback :before do |object|
83
+ set_callback :save, :before do |object|
84
84
  object['updated_at'] = Time.now
85
- object['created_at'] = object['updated_at'] if object.new_document?
85
+ object['created_at'] = object['updated_at'] if object.new?
86
86
  end
87
87
  EOS
88
88
  end
89
-
89
+
90
90
  # Name a method that will be called before the document is first saved,
91
91
  # which returns a string to be used for the document's <tt>_id</tt>.
92
92
  # Because CouchDB enforces a constraint that each id must be unique,
@@ -128,17 +128,36 @@ module CouchRest
128
128
  self.class.properties
129
129
  end
130
130
 
131
+ # Gets a reference to the actual document in the DB
132
+ # Calls up to the next document if there is one,
133
+ # Otherwise we're at the top and we return self
134
+ def base_doc
135
+ return self if base_doc?
136
+ @casted_by.base_doc
137
+ end
138
+
139
+ # Checks if we're the top document
140
+ def base_doc?
141
+ !@casted_by
142
+ end
143
+
131
144
  # Takes a hash as argument, and applies the values by using writer methods
132
145
  # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
133
146
  # missing. In case of error, no attributes are changed.
134
147
  def update_attributes_without_saving(hash)
135
- hash.each do |k, v|
148
+ # remove attributes that cannot be updated, silently ignoring them
149
+ # which matches Rails behavior when, for instance, setting created_at.
150
+ # make a copy, we don't want to change arguments
151
+ attrs = hash.dup
152
+ %w[_id _rev created_at updated_at].each {|attr| attrs.delete(attr)}
153
+ attrs.each do |k, v|
136
154
  raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=")
137
155
  end
138
- hash.each do |k, v|
156
+ attrs.each do |k, v|
139
157
  self.send("#{k}=",v)
140
158
  end
141
159
  end
160
+ alias :attributes= :update_attributes_without_saving
142
161
 
143
162
  # Takes a hash as argument, and applies the values by using writer methods
144
163
  # for each key. Raises a NoMethodError if the corresponding methods are
@@ -149,7 +168,8 @@ module CouchRest
149
168
  end
150
169
 
151
170
  # for compatibility with old-school frameworks
152
- alias :new_record? :new_document?
171
+ alias :new_record? :new?
172
+ alias :new_document? :new?
153
173
 
154
174
  # Trigger the callbacks (before, after, around)
155
175
  # and create the document
@@ -170,7 +190,7 @@ module CouchRest
170
190
  # unlike save, create returns the newly created document
171
191
  def create_without_callbacks(bulk =false)
172
192
  raise ArgumentError, "a document requires a database to be created to (The document or the #{self.class} default database were not set)" unless database
173
- set_unique_id if new_document? && self.respond_to?(:set_unique_id)
193
+ set_unique_id if new? && self.respond_to?(:set_unique_id)
174
194
  result = database.save_doc(self, bulk)
175
195
  (result["ok"] == true) ? self : false
176
196
  end
@@ -185,7 +205,7 @@ module CouchRest
185
205
  # only if the document isn't new
186
206
  def update(bulk = false)
187
207
  caught = catch(:halt) do
188
- if self.new_document?
208
+ if self.new?
189
209
  save(bulk)
190
210
  else
191
211
  _run_update_callbacks do
@@ -201,7 +221,7 @@ module CouchRest
201
221
  # and save the document
202
222
  def save(bulk = false)
203
223
  caught = catch(:halt) do
204
- if self.new_document?
224
+ if self.new?
205
225
  _run_save_callbacks do
206
226
  save_without_callbacks(bulk)
207
227
  end
@@ -215,15 +235,17 @@ module CouchRest
215
235
  # Returns a boolean value
216
236
  def save_without_callbacks(bulk = false)
217
237
  raise ArgumentError, "a document requires a database to be saved to (The document or the #{self.class} default database were not set)" unless database
218
- set_unique_id if new_document? && self.respond_to?(:set_unique_id)
238
+ set_unique_id if new? && self.respond_to?(:set_unique_id)
219
239
  result = database.save_doc(self, bulk)
220
- result["ok"] == true
240
+ mark_as_saved
241
+ true
221
242
  end
222
243
 
223
244
  # Saves the document to the db using save. Raises an exception
224
245
  # if the document is not saved properly.
225
246
  def save!
226
247
  raise "#{self.inspect} failed to save" unless self.save
248
+ true
227
249
  end
228
250
 
229
251
  # Deletes the document from the database. Runs the :destroy callbacks.
@@ -242,5 +264,22 @@ module CouchRest
242
264
  end
243
265
  end
244
266
 
267
+ protected
268
+
269
+ # Set document_saved flag on all casted models to true
270
+ def mark_as_saved
271
+ self.each do |key, prop|
272
+ if prop.is_a?(Array)
273
+ prop.each do |item|
274
+ if item.respond_to?(:document_saved)
275
+ item.send(:document_saved=, true)
276
+ end
277
+ end
278
+ elsif prop.respond_to?(:document_saved)
279
+ prop.send(:document_saved=, true)
280
+ end
281
+ end
282
+ end
283
+
245
284
  end
246
285
  end