openlogic-couchrest_model 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +11 -0
- data/.rspec +4 -0
- data/Gemfile +4 -0
- data/LICENSE +176 -0
- data/README.md +137 -0
- data/Rakefile +38 -0
- data/THANKS.md +21 -0
- data/VERSION +1 -0
- data/benchmarks/dirty.rb +118 -0
- data/couchrest_model.gemspec +36 -0
- data/history.md +309 -0
- data/init.rb +1 -0
- data/lib/couchrest/model.rb +10 -0
- data/lib/couchrest/model/associations.rb +231 -0
- data/lib/couchrest/model/base.rb +129 -0
- data/lib/couchrest/model/callbacks.rb +28 -0
- data/lib/couchrest/model/casted_array.rb +83 -0
- data/lib/couchrest/model/casted_by.rb +33 -0
- data/lib/couchrest/model/casted_hash.rb +84 -0
- data/lib/couchrest/model/class_proxy.rb +135 -0
- data/lib/couchrest/model/collection.rb +273 -0
- data/lib/couchrest/model/configuration.rb +67 -0
- data/lib/couchrest/model/connection.rb +70 -0
- data/lib/couchrest/model/core_extensions/hash.rb +9 -0
- data/lib/couchrest/model/core_extensions/time_parsing.rb +66 -0
- data/lib/couchrest/model/design_doc.rb +128 -0
- data/lib/couchrest/model/designs.rb +91 -0
- data/lib/couchrest/model/designs/view.rb +513 -0
- data/lib/couchrest/model/dirty.rb +39 -0
- data/lib/couchrest/model/document_queries.rb +99 -0
- data/lib/couchrest/model/embeddable.rb +78 -0
- data/lib/couchrest/model/errors.rb +25 -0
- data/lib/couchrest/model/extended_attachments.rb +83 -0
- data/lib/couchrest/model/persistence.rb +178 -0
- data/lib/couchrest/model/properties.rb +228 -0
- data/lib/couchrest/model/property.rb +114 -0
- data/lib/couchrest/model/property_protection.rb +71 -0
- data/lib/couchrest/model/proxyable.rb +183 -0
- data/lib/couchrest/model/support/couchrest_database.rb +13 -0
- data/lib/couchrest/model/support/couchrest_design.rb +33 -0
- data/lib/couchrest/model/typecast.rb +154 -0
- data/lib/couchrest/model/validations.rb +80 -0
- data/lib/couchrest/model/validations/casted_model.rb +16 -0
- data/lib/couchrest/model/validations/locale/en.yml +5 -0
- data/lib/couchrest/model/validations/uniqueness.rb +69 -0
- data/lib/couchrest/model/views.rb +151 -0
- data/lib/couchrest/railtie.rb +24 -0
- data/lib/couchrest_model.rb +66 -0
- data/lib/rails/generators/couchrest_model.rb +16 -0
- data/lib/rails/generators/couchrest_model/config/config_generator.rb +18 -0
- data/lib/rails/generators/couchrest_model/config/templates/couchdb.yml +21 -0
- data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
- data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
- data/spec/.gitignore +1 -0
- data/spec/fixtures/attachments/README +3 -0
- data/spec/fixtures/attachments/couchdb.png +0 -0
- data/spec/fixtures/attachments/test.html +11 -0
- data/spec/fixtures/config/couchdb.yml +10 -0
- data/spec/fixtures/models/article.rb +36 -0
- data/spec/fixtures/models/base.rb +164 -0
- data/spec/fixtures/models/card.rb +19 -0
- data/spec/fixtures/models/cat.rb +23 -0
- data/spec/fixtures/models/client.rb +6 -0
- data/spec/fixtures/models/course.rb +27 -0
- data/spec/fixtures/models/event.rb +8 -0
- data/spec/fixtures/models/invoice.rb +14 -0
- data/spec/fixtures/models/key_chain.rb +5 -0
- data/spec/fixtures/models/membership.rb +4 -0
- data/spec/fixtures/models/person.rb +11 -0
- data/spec/fixtures/models/project.rb +6 -0
- data/spec/fixtures/models/question.rb +7 -0
- data/spec/fixtures/models/sale_entry.rb +9 -0
- data/spec/fixtures/models/sale_invoice.rb +14 -0
- data/spec/fixtures/models/service.rb +10 -0
- data/spec/fixtures/models/user.rb +22 -0
- data/spec/fixtures/views/lib.js +3 -0
- data/spec/fixtures/views/test_view/lib.js +3 -0
- data/spec/fixtures/views/test_view/only-map.js +4 -0
- data/spec/fixtures/views/test_view/test-map.js +3 -0
- data/spec/fixtures/views/test_view/test-reduce.js +3 -0
- data/spec/functional/validations_spec.rb +8 -0
- data/spec/spec_helper.rb +60 -0
- data/spec/unit/active_model_lint_spec.rb +30 -0
- data/spec/unit/assocations_spec.rb +242 -0
- data/spec/unit/attachment_spec.rb +176 -0
- data/spec/unit/base_spec.rb +537 -0
- data/spec/unit/casted_spec.rb +72 -0
- data/spec/unit/class_proxy_spec.rb +167 -0
- data/spec/unit/collection_spec.rb +86 -0
- data/spec/unit/configuration_spec.rb +77 -0
- data/spec/unit/connection_spec.rb +148 -0
- data/spec/unit/core_extensions/time_parsing.rb +77 -0
- data/spec/unit/design_doc_spec.rb +241 -0
- data/spec/unit/designs/view_spec.rb +831 -0
- data/spec/unit/designs_spec.rb +134 -0
- data/spec/unit/dirty_spec.rb +436 -0
- data/spec/unit/embeddable_spec.rb +498 -0
- data/spec/unit/inherited_spec.rb +33 -0
- data/spec/unit/persistence_spec.rb +481 -0
- data/spec/unit/property_protection_spec.rb +192 -0
- data/spec/unit/property_spec.rb +481 -0
- data/spec/unit/proxyable_spec.rb +376 -0
- data/spec/unit/subclass_spec.rb +85 -0
- data/spec/unit/typecast_spec.rb +521 -0
- data/spec/unit/validations_spec.rb +140 -0
- data/spec/unit/view_spec.rb +367 -0
- 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
|