openlogic-couchrest_model 1.0.0

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 (107) hide show
  1. data/.gitignore +11 -0
  2. data/.rspec +4 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +176 -0
  5. data/README.md +137 -0
  6. data/Rakefile +38 -0
  7. data/THANKS.md +21 -0
  8. data/VERSION +1 -0
  9. data/benchmarks/dirty.rb +118 -0
  10. data/couchrest_model.gemspec +36 -0
  11. data/history.md +309 -0
  12. data/init.rb +1 -0
  13. data/lib/couchrest/model.rb +10 -0
  14. data/lib/couchrest/model/associations.rb +231 -0
  15. data/lib/couchrest/model/base.rb +129 -0
  16. data/lib/couchrest/model/callbacks.rb +28 -0
  17. data/lib/couchrest/model/casted_array.rb +83 -0
  18. data/lib/couchrest/model/casted_by.rb +33 -0
  19. data/lib/couchrest/model/casted_hash.rb +84 -0
  20. data/lib/couchrest/model/class_proxy.rb +135 -0
  21. data/lib/couchrest/model/collection.rb +273 -0
  22. data/lib/couchrest/model/configuration.rb +67 -0
  23. data/lib/couchrest/model/connection.rb +70 -0
  24. data/lib/couchrest/model/core_extensions/hash.rb +9 -0
  25. data/lib/couchrest/model/core_extensions/time_parsing.rb +66 -0
  26. data/lib/couchrest/model/design_doc.rb +128 -0
  27. data/lib/couchrest/model/designs.rb +91 -0
  28. data/lib/couchrest/model/designs/view.rb +513 -0
  29. data/lib/couchrest/model/dirty.rb +39 -0
  30. data/lib/couchrest/model/document_queries.rb +99 -0
  31. data/lib/couchrest/model/embeddable.rb +78 -0
  32. data/lib/couchrest/model/errors.rb +25 -0
  33. data/lib/couchrest/model/extended_attachments.rb +83 -0
  34. data/lib/couchrest/model/persistence.rb +178 -0
  35. data/lib/couchrest/model/properties.rb +228 -0
  36. data/lib/couchrest/model/property.rb +114 -0
  37. data/lib/couchrest/model/property_protection.rb +71 -0
  38. data/lib/couchrest/model/proxyable.rb +183 -0
  39. data/lib/couchrest/model/support/couchrest_database.rb +13 -0
  40. data/lib/couchrest/model/support/couchrest_design.rb +33 -0
  41. data/lib/couchrest/model/typecast.rb +154 -0
  42. data/lib/couchrest/model/validations.rb +80 -0
  43. data/lib/couchrest/model/validations/casted_model.rb +16 -0
  44. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  45. data/lib/couchrest/model/validations/uniqueness.rb +69 -0
  46. data/lib/couchrest/model/views.rb +151 -0
  47. data/lib/couchrest/railtie.rb +24 -0
  48. data/lib/couchrest_model.rb +66 -0
  49. data/lib/rails/generators/couchrest_model.rb +16 -0
  50. data/lib/rails/generators/couchrest_model/config/config_generator.rb +18 -0
  51. data/lib/rails/generators/couchrest_model/config/templates/couchdb.yml +21 -0
  52. data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
  53. data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
  54. data/spec/.gitignore +1 -0
  55. data/spec/fixtures/attachments/README +3 -0
  56. data/spec/fixtures/attachments/couchdb.png +0 -0
  57. data/spec/fixtures/attachments/test.html +11 -0
  58. data/spec/fixtures/config/couchdb.yml +10 -0
  59. data/spec/fixtures/models/article.rb +36 -0
  60. data/spec/fixtures/models/base.rb +164 -0
  61. data/spec/fixtures/models/card.rb +19 -0
  62. data/spec/fixtures/models/cat.rb +23 -0
  63. data/spec/fixtures/models/client.rb +6 -0
  64. data/spec/fixtures/models/course.rb +27 -0
  65. data/spec/fixtures/models/event.rb +8 -0
  66. data/spec/fixtures/models/invoice.rb +14 -0
  67. data/spec/fixtures/models/key_chain.rb +5 -0
  68. data/spec/fixtures/models/membership.rb +4 -0
  69. data/spec/fixtures/models/person.rb +11 -0
  70. data/spec/fixtures/models/project.rb +6 -0
  71. data/spec/fixtures/models/question.rb +7 -0
  72. data/spec/fixtures/models/sale_entry.rb +9 -0
  73. data/spec/fixtures/models/sale_invoice.rb +14 -0
  74. data/spec/fixtures/models/service.rb +10 -0
  75. data/spec/fixtures/models/user.rb +22 -0
  76. data/spec/fixtures/views/lib.js +3 -0
  77. data/spec/fixtures/views/test_view/lib.js +3 -0
  78. data/spec/fixtures/views/test_view/only-map.js +4 -0
  79. data/spec/fixtures/views/test_view/test-map.js +3 -0
  80. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  81. data/spec/functional/validations_spec.rb +8 -0
  82. data/spec/spec_helper.rb +60 -0
  83. data/spec/unit/active_model_lint_spec.rb +30 -0
  84. data/spec/unit/assocations_spec.rb +242 -0
  85. data/spec/unit/attachment_spec.rb +176 -0
  86. data/spec/unit/base_spec.rb +537 -0
  87. data/spec/unit/casted_spec.rb +72 -0
  88. data/spec/unit/class_proxy_spec.rb +167 -0
  89. data/spec/unit/collection_spec.rb +86 -0
  90. data/spec/unit/configuration_spec.rb +77 -0
  91. data/spec/unit/connection_spec.rb +148 -0
  92. data/spec/unit/core_extensions/time_parsing.rb +77 -0
  93. data/spec/unit/design_doc_spec.rb +241 -0
  94. data/spec/unit/designs/view_spec.rb +831 -0
  95. data/spec/unit/designs_spec.rb +134 -0
  96. data/spec/unit/dirty_spec.rb +436 -0
  97. data/spec/unit/embeddable_spec.rb +498 -0
  98. data/spec/unit/inherited_spec.rb +33 -0
  99. data/spec/unit/persistence_spec.rb +481 -0
  100. data/spec/unit/property_protection_spec.rb +192 -0
  101. data/spec/unit/property_spec.rb +481 -0
  102. data/spec/unit/proxyable_spec.rb +376 -0
  103. data/spec/unit/subclass_spec.rb +85 -0
  104. data/spec/unit/typecast_spec.rb +521 -0
  105. data/spec/unit/validations_spec.rb +140 -0
  106. data/spec/unit/view_spec.rb +367 -0
  107. metadata +301 -0
@@ -0,0 +1,228 @@
1
+ # encoding: utf-8
2
+ module CouchRest
3
+ module Model
4
+ module Properties
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute(:properties) unless self.respond_to?(:properties)
9
+ class_attribute(:properties_by_name) unless self.respond_to?(:properties_by_name)
10
+ self.properties ||= []
11
+ self.properties_by_name ||= {}
12
+ raise "You can only mixin Properties in a class responding to [] and []=, if you tried to mixin CastedModel, make sure your class inherits from Hash or responds to the proper methods" unless (method_defined?(:[]) && method_defined?(:[]=))
13
+ end
14
+
15
+ # Provide an attribute hash ready to be sent to CouchDB but with
16
+ # all the nil attributes removed.
17
+ def as_couch_json
18
+ super.delete_if{|k,v| v.nil?}
19
+ end
20
+
21
+ # Returns the Class properties with their values
22
+ #
23
+ # ==== Returns
24
+ # Array:: the list of properties with their values
25
+ def properties_with_values
26
+ props = {}
27
+ properties.each { |property| props[property.name] = read_attribute(property.name) }
28
+ props
29
+ end
30
+
31
+ # Read the casted value of an attribute defined with a property.
32
+ #
33
+ # ==== Returns
34
+ # Object:: the casted attibutes value.
35
+ def read_attribute(property)
36
+ self[find_property!(property).to_s]
37
+ end
38
+
39
+ # Store a casted value in the current instance of an attribute defined
40
+ # with a property and update dirty status
41
+ def write_attribute(property, value)
42
+ prop = find_property!(property)
43
+ value = prop.is_a?(String) ? value : prop.cast(self, value)
44
+ couchrest_attribute_will_change!(prop.name) if use_dirty? && self[prop.name] != value
45
+ self[prop.name] = value
46
+ end
47
+
48
+ # Takes a hash as argument, and applies the values by using writer methods
49
+ # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
50
+ # missing. In case of error, no attributes are changed.
51
+ def update_attributes_without_saving(hash)
52
+ # Remove any protected and update all the rest. Any attributes
53
+ # which do not have a property will simply be ignored.
54
+ attrs = remove_protected_attributes(hash)
55
+ directly_set_attributes(attrs)
56
+ end
57
+ alias :attributes= :update_attributes_without_saving
58
+
59
+ # 'attributes' needed for Dirty
60
+ alias :attributes :properties_with_values
61
+
62
+ def set_attributes(hash)
63
+ attrs = remove_protected_attributes(hash)
64
+ directly_set_attributes(attrs)
65
+ end
66
+
67
+ protected
68
+
69
+ def find_property(property)
70
+ property.is_a?(Property) ? property : self.class.properties_by_name[property.to_s]
71
+ end
72
+
73
+ # The following methods should be accessable by the Model::Base Class, but not by anything else!
74
+ def apply_all_property_defaults
75
+ return if self.respond_to?(:new?) && (new? == false)
76
+ # TODO: cache the default object
77
+ # Never mark default options as dirty!
78
+ dirty, self.disable_dirty = self.disable_dirty, true
79
+ self.class.properties.each do |property|
80
+ write_attribute(property, property.default_value)
81
+ end
82
+ self.disable_dirty = dirty
83
+ end
84
+
85
+ def prepare_all_attributes(attrs = {}, options = {})
86
+ self.disable_dirty = !!options[:directly_set_attributes]
87
+ apply_all_property_defaults
88
+ if options[:directly_set_attributes]
89
+ directly_set_read_only_attributes(attrs)
90
+ directly_set_attributes(attrs, true)
91
+ else
92
+ attrs = remove_protected_attributes(attrs)
93
+ directly_set_attributes(attrs)
94
+ end
95
+ self.disable_dirty = false
96
+ self
97
+ end
98
+
99
+ def find_property!(property)
100
+ prop = find_property(property)
101
+ raise ArgumentError, "Missing property definition for #{property.to_s}" if prop.nil?
102
+ prop
103
+ end
104
+
105
+ # Set all the attributes and return a hash with the attributes
106
+ # that have not been accepted.
107
+ def directly_set_attributes(hash, mass_assign = false)
108
+ return if hash.nil?
109
+ hash.reject do |key, value|
110
+ if self.respond_to?("#{key}=")
111
+ self.send("#{key}=", value)
112
+ elsif mass_assign || mass_assign_any_attribute
113
+ self[key] = value
114
+ end
115
+ end
116
+ end
117
+
118
+ def directly_set_read_only_attributes(hash)
119
+ property_list = self.properties.map{|p| p.name}
120
+ hash.each do |attribute_name, attribute_value|
121
+ next if self.respond_to?("#{attribute_name}=")
122
+ if property_list.include?(attribute_name)
123
+ write_attribute(attribute_name, hash.delete(attribute_name))
124
+ end
125
+ end
126
+ end
127
+
128
+
129
+
130
+ module ClassMethods
131
+
132
+ def property(name, *options, &block)
133
+ raise "Invalid property definition, '#{name}' already used for CouchRest Model type field" if name.to_s == model_type_key.to_s && CouchRest::Model::Base >= self
134
+ opts = { }
135
+ type = options.shift
136
+ if type.class != Hash
137
+ opts[:type] = type
138
+ opts.merge!(options.shift || {})
139
+ else
140
+ opts.update(type)
141
+ end
142
+ existing_property = self.properties.find{|p| p.name == name.to_s}
143
+ if existing_property.nil? || (existing_property.default != opts[:default])
144
+ define_property(name, opts, &block)
145
+ end
146
+ end
147
+
148
+ # Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
149
+ # on the document whenever saving occurs.
150
+ #
151
+ # These properties are casted as Time objects, so they should always
152
+ # be set to UTC.
153
+ def timestamps!
154
+ property(:updated_at, Time, :read_only => true, :protected => true, :auto_validation => false)
155
+ property(:created_at, Time, :read_only => true, :protected => true, :auto_validation => false)
156
+
157
+ set_callback :save, :before do |object|
158
+ write_attribute('updated_at', Time.now)
159
+ write_attribute('created_at', Time.now) if object.new?
160
+ end
161
+ end
162
+
163
+ protected
164
+
165
+ # This is not a thread safe operation, if you have to set new properties at runtime
166
+ # make sure a mutex is used.
167
+ def define_property(name, options={}, &block)
168
+ # check if this property is going to casted
169
+ type = options.delete(:type) || options.delete(:cast_as)
170
+ if block_given?
171
+ type = Class.new do
172
+ include Embeddable
173
+ end
174
+ if block.arity == 1 # Traditional, with options
175
+ type.class_eval { yield type }
176
+ else
177
+ type.instance_exec(&block)
178
+ end
179
+ type = [type] # inject as an array
180
+ end
181
+ property = Property.new(name, type, options)
182
+ create_property_getter(property)
183
+ create_property_setter(property) unless property.read_only == true
184
+ if property.type_class.respond_to?(:validates_casted_model)
185
+ validates_casted_model property.name
186
+ end
187
+ properties << property
188
+ properties_by_name[property.to_s] = property
189
+ property
190
+ end
191
+
192
+ # defines the getter for the property (and optional aliases)
193
+ def create_property_getter(property)
194
+ define_method(property.name) do
195
+ read_attribute(property.name)
196
+ end
197
+
198
+ if ['boolean', TrueClass.to_s.downcase].include?(property.type.to_s.downcase)
199
+ define_method("#{property.name}?") do
200
+ value = read_attribute(property.name)
201
+ !(value.nil? || value == false)
202
+ end
203
+ end
204
+
205
+ if property.alias
206
+ alias_method(property.alias, property.name.to_sym)
207
+ end
208
+ end
209
+
210
+ # defines the setter for the property (and optional aliases)
211
+ def create_property_setter(property)
212
+ name = property.name
213
+
214
+ define_method("#{name}=") do |value|
215
+ write_attribute(name, value)
216
+ end
217
+
218
+ if property.alias
219
+ alias_method "#{property.alias}=", "#{name}="
220
+ end
221
+ end
222
+
223
+ end # module ClassMethods
224
+
225
+ end
226
+ end
227
+ end
228
+
@@ -0,0 +1,114 @@
1
+ # encoding: utf-8
2
+ module CouchRest::Model
3
+ class Property
4
+
5
+ include ::CouchRest::Model::Typecast
6
+
7
+ attr_reader :name, :type, :type_class, :read_only, :alias, :default, :casted, :init_method, :options
8
+
9
+ # Attribute to define.
10
+ # All Properties are assumed casted unless the type is nil.
11
+ def initialize(name, type = nil, options = {})
12
+ @name = name.to_s
13
+ @casted = true
14
+ parse_type(type)
15
+ parse_options(options)
16
+ self
17
+ end
18
+
19
+ def to_s
20
+ name
21
+ end
22
+
23
+ # Cast the provided value using the properties details.
24
+ def cast(parent, value)
25
+ return value unless casted
26
+ if type.is_a?(Array)
27
+ if value.nil?
28
+ value = []
29
+ elsif [Hash, HashWithIndifferentAccess].include?(value.class)
30
+ # Assume provided as a params hash where key is index
31
+ value = parameter_hash_to_array(value)
32
+ elsif !value.is_a?(Array)
33
+ raise "Expecting an array or keyed hash for property #{parent.class.name}##{self.name}"
34
+ end
35
+ arr = value.collect { |data| cast_value(parent, data) }
36
+ # allow casted_by calls to be passed up chain by wrapping in CastedArray
37
+ CastedArray.new(arr, self, parent)
38
+ elsif (type == Object || type == Hash) && (value.is_a?(Hash))
39
+ # allow casted_by calls to be passed up chain by wrapping in CastedHash
40
+ CastedHash[value, self, parent]
41
+ elsif !value.nil?
42
+ cast_value(parent, value)
43
+ end
44
+ end
45
+
46
+ # Cast an individual value
47
+ def cast_value(parent, value)
48
+ value = typecast_value(value, self)
49
+ associate_casted_value_to_parent(parent, value)
50
+ end
51
+
52
+ def default_value
53
+ return if default.nil?
54
+ if default.class == Proc
55
+ default.call
56
+ else
57
+ # TODO identify cause of mutex errors
58
+ Marshal.load(Marshal.dump(default))
59
+ end
60
+ end
61
+
62
+ # Initialize a new instance of a property's type ready to be
63
+ # used. If a proc is defined for the init method, it will be used instead of
64
+ # a normal call to the class.
65
+ def build(*args)
66
+ raise StandardError, "Cannot build property without a class" if @type_class.nil?
67
+ if @init_method.is_a?(Proc)
68
+ @init_method.call(*args)
69
+ else
70
+ @type_class.send(@init_method, *args)
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def parameter_hash_to_array(source)
77
+ value = [ ]
78
+ source.keys.each do |k|
79
+ value[k.to_i] = source[k]
80
+ end
81
+ value.compact
82
+ end
83
+
84
+ def associate_casted_value_to_parent(parent, value)
85
+ value.casted_by = parent if value.respond_to?(:casted_by)
86
+ value.casted_by_property = self if value.respond_to?(:casted_by_property)
87
+ value
88
+ end
89
+
90
+ def parse_type(type)
91
+ if type.nil?
92
+ @casted = false
93
+ @type = nil
94
+ @type_class = nil
95
+ else
96
+ base = type.is_a?(Array) ? type.first : type
97
+ base = Object if base.nil?
98
+ raise "Defining a property type as a #{type.class.name.humanize} is not supported in CouchRest Model!" if base.class != Class
99
+ @type_class = base
100
+ @type = type
101
+ end
102
+ end
103
+
104
+ def parse_options(options)
105
+ @validation_format = options.delete(:format) if options[:format]
106
+ @read_only = options.delete(:read_only) if options[:read_only]
107
+ @alias = options.delete(:alias) if options[:alias]
108
+ @default = options.delete(:default) unless options[:default].nil?
109
+ @init_method = options[:init_method] ? options.delete(:init_method) : 'new'
110
+ @options = options
111
+ end
112
+
113
+ end
114
+ end
@@ -0,0 +1,71 @@
1
+ module CouchRest
2
+ module Model
3
+ module PropertyProtection
4
+ extend ActiveSupport::Concern
5
+
6
+ # Property protection from mass assignment to CouchRest::Model properties
7
+ #
8
+ # Protected methods will be removed from
9
+ # * new
10
+ # * update_attributes
11
+ # * upate_attributes_without_saving
12
+ # * attributes=
13
+ #
14
+ # There are two modes of protection
15
+ # 1) Declare accessible poperties, and assume all unspecified properties are protected
16
+ # property :name, :accessible => true
17
+ # property :admin # this will be automatically protected
18
+ #
19
+ # 2) Declare protected properties, and assume all unspecified properties are accessible
20
+ # property :name # this will not be protected
21
+ # property :admin, :protected => true
22
+ #
23
+ # 3) Mix and match, and assume all unspecified properties are protected.
24
+ # property :name, :accessible => true
25
+ # property :admin, :protected => true # ignored
26
+ # property :phone # this will be automatically protected
27
+ #
28
+ # Note: the timestamps! method protectes the created_at and updated_at properties
29
+
30
+
31
+ def self.included(base)
32
+ base.extend(ClassMethods)
33
+ end
34
+
35
+ module ClassMethods
36
+ def accessible_properties
37
+ props = properties.select { |prop| prop.options[:accessible] }
38
+ if props.empty?
39
+ props = properties.select { |prop| !prop.options[:protected] }
40
+ end
41
+ props
42
+ end
43
+
44
+ def protected_properties
45
+ accessibles = accessible_properties
46
+ properties.reject { |prop| accessibles.include?(prop) }
47
+ end
48
+ end
49
+
50
+ def accessible_properties
51
+ self.class.accessible_properties
52
+ end
53
+
54
+ def protected_properties
55
+ self.class.protected_properties
56
+ end
57
+
58
+ # Return a new copy of the attributes hash with protected attributes
59
+ # removed.
60
+ def remove_protected_attributes(attributes)
61
+ protected_names = protected_properties.map { |prop| prop.name }
62
+ return attributes if protected_names.empty? or attributes.nil?
63
+
64
+ attributes.reject do |property_name, property_value|
65
+ protected_names.include?(property_name.to_s)
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,183 @@
1
+ module CouchRest
2
+ module Model
3
+ # :nodoc: Because I like inventing words
4
+ module Proxyable
5
+ extend ActiveSupport::Concern
6
+
7
+ def proxy_database
8
+ raise StandardError, "Please set the #proxy_database_method" if self.class.proxy_database_method.nil?
9
+ @proxy_database ||= self.class.prepare_database(self.send(self.class.proxy_database_method))
10
+ end
11
+
12
+ module ClassMethods
13
+
14
+
15
+ # Define a collection that will use the base model for the database connection
16
+ # details.
17
+ def proxy_for(assoc_name, options = {})
18
+ db_method = options[:database_method] || "proxy_database"
19
+ options[:class_name] ||= assoc_name.to_s.singularize.camelize
20
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
21
+ def #{assoc_name}
22
+ @#{assoc_name} ||= CouchRest::Model::Proxyable::ModelProxy.new(::#{options[:class_name]}, self, self.class.to_s.underscore, #{db_method})
23
+ end
24
+ EOS
25
+ end
26
+
27
+ # Tell this model which other model to use a base for the database
28
+ # connection to use.
29
+ def proxied_by(model_name, options = {})
30
+ raise "Model can only be proxied once or ##{model_name} already defined" if method_defined?(model_name) || !proxy_owner_method.nil?
31
+ self.proxy_owner_method = model_name
32
+ attr_accessor :model_proxy
33
+ attr_accessor model_name
34
+ overwrite_database_reader(model_name)
35
+ end
36
+
37
+ # Define an a class variable accessor ready to be inherited and unique
38
+ # for each Class using the base.
39
+ # Perhaps there is a shorter way of writing this.
40
+ def proxy_owner_method=(name); @proxy_owner_method = name; end
41
+ def proxy_owner_method; @proxy_owner_method; end
42
+
43
+ # Define the name of a method to call to determine the name of
44
+ # the database to use as a proxy.
45
+ def proxy_database_method(name = nil)
46
+ @proxy_database_method = name if name
47
+ @proxy_database_method
48
+ end
49
+
50
+ private
51
+
52
+ # Ensure that no attempt is made to autoload a database connection
53
+ # by overwriting it to provide a basic accessor.
54
+ def overwrite_database_reader(model_name)
55
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
56
+ def self.database
57
+ raise StandardError, "#{self.to_s} database must be accessed via '#{model_name}' proxy"
58
+ end
59
+ EOS
60
+ end
61
+
62
+ end
63
+
64
+ class ModelProxy
65
+
66
+ attr_reader :model, :owner, :owner_name, :database
67
+
68
+ def initialize(model, owner, owner_name, database)
69
+ @model = model
70
+ @owner = owner
71
+ @owner_name = owner_name
72
+ @database = database
73
+ end
74
+
75
+ # Base
76
+ def new(attrs = {}, options = {}, &block)
77
+ proxy_block_update(:new, attrs, options, &block)
78
+ end
79
+
80
+ def build_from_database(attrs = {}, options = {}, &block)
81
+ proxy_block_update(:build_from_database, attrs, options, &block)
82
+ end
83
+
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
106
+
107
+ def count(opts = {})
108
+ @model.count({:database => @database}.merge(opts))
109
+ end
110
+
111
+ def first(opts = {})
112
+ proxy_update(@model.first({:database => @database}.merge(opts)))
113
+ end
114
+
115
+ def last(opts = {})
116
+ proxy_update(@model.last({:database => @database}.merge(opts)))
117
+ end
118
+
119
+ def get(id)
120
+ proxy_update(@model.get(id, @database))
121
+ end
122
+ alias :find :get
123
+
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
148
+
149
+ def save_design_doc(db = nil)
150
+ @model.save_design_doc(db || @database)
151
+ end
152
+
153
+
154
+ protected
155
+
156
+ # Update the document's proxy details, specifically, the fields that
157
+ # link back to the original document.
158
+ def proxy_update(doc)
159
+ if doc && doc.is_a?(model)
160
+ doc.database = @database
161
+ doc.model_proxy = self
162
+ doc.send("#{owner_name}=", owner)
163
+ end
164
+ doc
165
+ end
166
+
167
+ def proxy_update_all(docs)
168
+ docs.each do |doc|
169
+ proxy_update(doc)
170
+ end
171
+ end
172
+
173
+ def proxy_block_update(method, *args, &block)
174
+ model.send(method, *args) do |doc|
175
+ proxy_update(doc)
176
+ yield doc if block_given?
177
+ end
178
+ end
179
+
180
+ end
181
+ end
182
+ end
183
+ end