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.
- 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
|