mongomodel 0.1
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/LICENSE +22 -0
- data/README.md +34 -0
- data/Rakefile +47 -0
- data/bin/console +45 -0
- data/lib/mongomodel.rb +92 -0
- data/lib/mongomodel/attributes/mongo.rb +40 -0
- data/lib/mongomodel/attributes/store.rb +30 -0
- data/lib/mongomodel/attributes/typecasting.rb +51 -0
- data/lib/mongomodel/concerns/abstract_class.rb +17 -0
- data/lib/mongomodel/concerns/activemodel.rb +11 -0
- data/lib/mongomodel/concerns/associations.rb +103 -0
- data/lib/mongomodel/concerns/associations/base/association.rb +33 -0
- data/lib/mongomodel/concerns/associations/base/definition.rb +56 -0
- data/lib/mongomodel/concerns/associations/base/proxy.rb +58 -0
- data/lib/mongomodel/concerns/associations/belongs_to.rb +68 -0
- data/lib/mongomodel/concerns/associations/has_many_by_foreign_key.rb +159 -0
- data/lib/mongomodel/concerns/associations/has_many_by_ids.rb +175 -0
- data/lib/mongomodel/concerns/attribute_methods.rb +55 -0
- data/lib/mongomodel/concerns/attribute_methods/before_type_cast.rb +29 -0
- data/lib/mongomodel/concerns/attribute_methods/dirty.rb +35 -0
- data/lib/mongomodel/concerns/attribute_methods/protected.rb +127 -0
- data/lib/mongomodel/concerns/attribute_methods/query.rb +22 -0
- data/lib/mongomodel/concerns/attribute_methods/read.rb +29 -0
- data/lib/mongomodel/concerns/attribute_methods/write.rb +29 -0
- data/lib/mongomodel/concerns/attributes.rb +85 -0
- data/lib/mongomodel/concerns/callbacks.rb +294 -0
- data/lib/mongomodel/concerns/logging.rb +15 -0
- data/lib/mongomodel/concerns/pretty_inspect.rb +29 -0
- data/lib/mongomodel/concerns/properties.rb +69 -0
- data/lib/mongomodel/concerns/record_status.rb +42 -0
- data/lib/mongomodel/concerns/timestamps.rb +32 -0
- data/lib/mongomodel/concerns/validations.rb +38 -0
- data/lib/mongomodel/concerns/validations/associated.rb +46 -0
- data/lib/mongomodel/document.rb +20 -0
- data/lib/mongomodel/document/callbacks.rb +46 -0
- data/lib/mongomodel/document/dynamic_finders.rb +88 -0
- data/lib/mongomodel/document/finders.rb +82 -0
- data/lib/mongomodel/document/indexes.rb +91 -0
- data/lib/mongomodel/document/optimistic_locking.rb +48 -0
- data/lib/mongomodel/document/persistence.rb +143 -0
- data/lib/mongomodel/document/scopes.rb +161 -0
- data/lib/mongomodel/document/validations.rb +68 -0
- data/lib/mongomodel/document/validations/uniqueness.rb +78 -0
- data/lib/mongomodel/embedded_document.rb +42 -0
- data/lib/mongomodel/locale/en.yml +55 -0
- data/lib/mongomodel/support/collection.rb +109 -0
- data/lib/mongomodel/support/configuration.rb +35 -0
- data/lib/mongomodel/support/core_extensions.rb +10 -0
- data/lib/mongomodel/support/exceptions.rb +25 -0
- data/lib/mongomodel/support/mongo_options.rb +177 -0
- data/lib/mongomodel/support/types.rb +35 -0
- data/lib/mongomodel/support/types/array.rb +11 -0
- data/lib/mongomodel/support/types/boolean.rb +25 -0
- data/lib/mongomodel/support/types/custom.rb +38 -0
- data/lib/mongomodel/support/types/date.rb +20 -0
- data/lib/mongomodel/support/types/float.rb +13 -0
- data/lib/mongomodel/support/types/hash.rb +18 -0
- data/lib/mongomodel/support/types/integer.rb +13 -0
- data/lib/mongomodel/support/types/object.rb +21 -0
- data/lib/mongomodel/support/types/string.rb +9 -0
- data/lib/mongomodel/support/types/symbol.rb +9 -0
- data/lib/mongomodel/support/types/time.rb +12 -0
- data/lib/mongomodel/version.rb +3 -0
- data/spec/mongomodel/attributes/store_spec.rb +273 -0
- data/spec/mongomodel/concerns/activemodel_spec.rb +61 -0
- data/spec/mongomodel/concerns/associations/belongs_to_spec.rb +153 -0
- data/spec/mongomodel/concerns/associations/has_many_by_foreign_key_spec.rb +165 -0
- data/spec/mongomodel/concerns/associations/has_many_by_ids_spec.rb +192 -0
- data/spec/mongomodel/concerns/attribute_methods/before_type_cast_spec.rb +46 -0
- data/spec/mongomodel/concerns/attribute_methods/dirty_spec.rb +131 -0
- data/spec/mongomodel/concerns/attribute_methods/protected_spec.rb +86 -0
- data/spec/mongomodel/concerns/attribute_methods/query_spec.rb +27 -0
- data/spec/mongomodel/concerns/attribute_methods/read_spec.rb +52 -0
- data/spec/mongomodel/concerns/attribute_methods/write_spec.rb +43 -0
- data/spec/mongomodel/concerns/attributes_spec.rb +152 -0
- data/spec/mongomodel/concerns/callbacks_spec.rb +90 -0
- data/spec/mongomodel/concerns/logging_spec.rb +20 -0
- data/spec/mongomodel/concerns/pretty_inspect_spec.rb +68 -0
- data/spec/mongomodel/concerns/properties_spec.rb +29 -0
- data/spec/mongomodel/concerns/timestamps_spec.rb +170 -0
- data/spec/mongomodel/concerns/validations_spec.rb +159 -0
- data/spec/mongomodel/document/callbacks_spec.rb +80 -0
- data/spec/mongomodel/document/dynamic_finders_spec.rb +183 -0
- data/spec/mongomodel/document/finders_spec.rb +231 -0
- data/spec/mongomodel/document/indexes_spec.rb +121 -0
- data/spec/mongomodel/document/optimistic_locking_spec.rb +57 -0
- data/spec/mongomodel/document/persistence_spec.rb +319 -0
- data/spec/mongomodel/document/scopes_spec.rb +204 -0
- data/spec/mongomodel/document/validations/uniqueness_spec.rb +217 -0
- data/spec/mongomodel/document/validations_spec.rb +132 -0
- data/spec/mongomodel/document_spec.rb +74 -0
- data/spec/mongomodel/embedded_document_spec.rb +66 -0
- data/spec/mongomodel/mongomodel_spec.rb +33 -0
- data/spec/mongomodel/support/collection_spec.rb +248 -0
- data/spec/mongomodel/support/mongo_options_spec.rb +295 -0
- data/spec/mongomodel/support/property_spec.rb +83 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/specdoc.opts +6 -0
- data/spec/support/callbacks.rb +44 -0
- data/spec/support/helpers/define_class.rb +24 -0
- data/spec/support/helpers/specs_for.rb +11 -0
- data/spec/support/matchers/be_a_subclass_of.rb +5 -0
- data/spec/support/matchers/respond_to_boolean.rb +17 -0
- data/spec/support/matchers/run_callbacks.rb +20 -0
- data/spec/support/models.rb +23 -0
- data/spec/support/time.rb +6 -0
- metadata +232 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module MongoModel
|
|
2
|
+
module AttributeMethods
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
include ActiveModel::AttributeMethods
|
|
6
|
+
|
|
7
|
+
module ClassMethods
|
|
8
|
+
# Generates all the attribute related methods for defined properties
|
|
9
|
+
# accessors, mutators and query methods.
|
|
10
|
+
def define_attribute_methods
|
|
11
|
+
super(properties.keys)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def property(*args)
|
|
15
|
+
property = super
|
|
16
|
+
undefine_attribute_methods
|
|
17
|
+
property
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def method_missing(method_id, *args, &block)
|
|
22
|
+
# If we haven't generated any methods yet, generate them, then
|
|
23
|
+
# see if we've created the method we're looking for.
|
|
24
|
+
unless self.class.attribute_methods_generated?
|
|
25
|
+
self.class.define_attribute_methods
|
|
26
|
+
method_name = method_id.to_s
|
|
27
|
+
|
|
28
|
+
guard_private_attribute_method!(method_name, args)
|
|
29
|
+
|
|
30
|
+
if self.class.generated_attribute_methods.method_defined?(method_name)
|
|
31
|
+
return self.send(method_id, *args, &block)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def respond_to?(*args)
|
|
39
|
+
self.class.define_attribute_methods
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def clone_attribute_value(attribute_name)
|
|
44
|
+
value = read_attribute(attribute_name)
|
|
45
|
+
value.duplicable? ? value.clone : value
|
|
46
|
+
rescue TypeError, NoMethodError
|
|
47
|
+
value
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
protected
|
|
51
|
+
def attribute_method?(attr_name)
|
|
52
|
+
properties.has_key?(attr_name)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module MongoModel
|
|
2
|
+
module AttributeMethods
|
|
3
|
+
module BeforeTypeCast
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
attribute_method_suffix "_before_type_cast"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Returns an attribute value before typecasting.
|
|
11
|
+
def read_attribute_before_type_cast(name)
|
|
12
|
+
attributes.before_type_cast(name.to_sym)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Returns a hash of attributes before typecasting.
|
|
16
|
+
def attributes_before_type_cast
|
|
17
|
+
attributes.keys.inject({}) do |result, key|
|
|
18
|
+
result[key] = attributes.before_type_cast(key)
|
|
19
|
+
result
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
def attribute_before_type_cast(attribute_name)
|
|
25
|
+
read_attribute_before_type_cast(attribute_name)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module MongoModel
|
|
2
|
+
module AttributeMethods
|
|
3
|
+
module Dirty
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
include ActiveModel::Dirty
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
after_save { changed_attributes.clear }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Returns the attributes as they were before any changes were made to the document.
|
|
13
|
+
def original_attributes
|
|
14
|
+
attributes.merge(changed_attributes)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Wrap write_attribute to remember original attribute value.
|
|
18
|
+
def write_attribute(attr, value)
|
|
19
|
+
attr = attr.to_sym
|
|
20
|
+
|
|
21
|
+
# The attribute already has an unsaved change.
|
|
22
|
+
if changed_attributes.include?(attr)
|
|
23
|
+
old = changed_attributes[attr]
|
|
24
|
+
changed_attributes.delete(attr) if value == old
|
|
25
|
+
else
|
|
26
|
+
old = clone_attribute_value(attr)
|
|
27
|
+
changed_attributes[attr] = old unless value == old
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Carry on.
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
module MongoModel
|
|
2
|
+
module AttributeMethods
|
|
3
|
+
module Protected
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
module ClassMethods
|
|
7
|
+
def property(name, *args, &block)#:nodoc:
|
|
8
|
+
property = super(name, *args, &block)
|
|
9
|
+
|
|
10
|
+
attr_protected(name) if property.options[:protected]
|
|
11
|
+
attr_accessible(name) if property.options[:accessible]
|
|
12
|
+
|
|
13
|
+
property
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Attributes named in this macro are protected from mass-assignment,
|
|
17
|
+
# such as <tt>new(attributes)</tt>,
|
|
18
|
+
# <tt>update_attributes(attributes)</tt>, or
|
|
19
|
+
# <tt>attributes=(attributes)</tt>.
|
|
20
|
+
#
|
|
21
|
+
# Mass-assignment to these attributes will simply be ignored, to assign
|
|
22
|
+
# to them you can use direct writer methods. This is meant to protect
|
|
23
|
+
# sensitive attributes from being overwritten by malicious users
|
|
24
|
+
# tampering with URLs or forms.
|
|
25
|
+
#
|
|
26
|
+
# class Customer < Recliner::Document
|
|
27
|
+
# attr_protected :credit_rating
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# customer = Customer.new("name" => David, "credit_rating" => "Excellent")
|
|
31
|
+
# customer.credit_rating # => nil
|
|
32
|
+
# customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" }
|
|
33
|
+
# customer.credit_rating # => nil
|
|
34
|
+
#
|
|
35
|
+
# customer.credit_rating = "Average"
|
|
36
|
+
# customer.credit_rating # => "Average"
|
|
37
|
+
#
|
|
38
|
+
# To start from an all-closed default and enable attributes as needed,
|
|
39
|
+
# have a look at +attr_accessible+.
|
|
40
|
+
#
|
|
41
|
+
# If the access logic of your application is richer you can use <tt>Hash#except</tt>
|
|
42
|
+
# or <tt>Hash#slice</tt> to sanitize the hash of parameters before they are
|
|
43
|
+
# passed to Recliner.
|
|
44
|
+
#
|
|
45
|
+
# For example, it could be the case that the list of protected attributes
|
|
46
|
+
# for a given model depends on the role of the user:
|
|
47
|
+
#
|
|
48
|
+
# # Assumes plan_id is not protected because it depends on the role.
|
|
49
|
+
# params[:account] = params[:account].except(:plan_id) unless admin?
|
|
50
|
+
# @account.update_attributes(params[:account])
|
|
51
|
+
#
|
|
52
|
+
# Note that +attr_protected+ is still applied to the received hash. Thus,
|
|
53
|
+
# with this technique you can at most _extend_ the list of protected
|
|
54
|
+
# attributes for a particular mass-assignment call.
|
|
55
|
+
def attr_protected(*attrs)
|
|
56
|
+
write_inheritable_attribute(:attr_protected, attrs.map { |a| a.to_s } + protected_attributes)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Specifies a white list of model attributes that can be set via
|
|
60
|
+
# mass-assignment, such as <tt>new(attributes)</tt>,
|
|
61
|
+
# <tt>update_attributes(attributes)</tt>, or
|
|
62
|
+
# <tt>attributes=(attributes)</tt>
|
|
63
|
+
#
|
|
64
|
+
# This is the opposite of the +attr_protected+ macro: Mass-assignment
|
|
65
|
+
# will only set attributes in this list, to assign to the rest of
|
|
66
|
+
# attributes you can use direct writer methods. This is meant to protect
|
|
67
|
+
# sensitive attributes from being overwritten by malicious users
|
|
68
|
+
# tampering with URLs or forms. If you'd rather start from an all-open
|
|
69
|
+
# default and restrict attributes as needed, have a look at
|
|
70
|
+
# +attr_protected+.
|
|
71
|
+
#
|
|
72
|
+
# class Customer < Recliner::Document
|
|
73
|
+
# attr_accessible :name, :nickname
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
# customer = Customer.new(:name => "David", :nickname => "Dave", :credit_rating => "Excellent")
|
|
77
|
+
# customer.credit_rating # => nil
|
|
78
|
+
# customer.attributes = { :name => "Jolly fellow", :credit_rating => "Superb" }
|
|
79
|
+
# customer.credit_rating # => nil
|
|
80
|
+
#
|
|
81
|
+
# customer.credit_rating = "Average"
|
|
82
|
+
# customer.credit_rating # => "Average"
|
|
83
|
+
#
|
|
84
|
+
# If the access logic of your application is richer you can use <tt>Hash#except</tt>
|
|
85
|
+
# or <tt>Hash#slice</tt> to sanitize the hash of parameters before they are
|
|
86
|
+
# passed to Recliner.
|
|
87
|
+
#
|
|
88
|
+
# For example, it could be the case that the list of accessible attributes
|
|
89
|
+
# for a given model depends on the role of the user:
|
|
90
|
+
#
|
|
91
|
+
# # Assumes plan_id is accessible because it depends on the role.
|
|
92
|
+
# params[:account] = params[:account].except(:plan_id) unless admin?
|
|
93
|
+
# @account.update_attributes(params[:account])
|
|
94
|
+
#
|
|
95
|
+
# Note that +attr_accessible+ is still applied to the received hash. Thus,
|
|
96
|
+
# with this technique you can at most _narrow_ the list of accessible
|
|
97
|
+
# attributes for a particular mass-assignment call.
|
|
98
|
+
def attr_accessible(*attrs)
|
|
99
|
+
write_inheritable_attribute(:attr_accessible, attrs.map { |a| a.to_s } + accessible_attributes)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns an array of all the attributes that have been protected from mass-assignment.
|
|
103
|
+
def protected_attributes
|
|
104
|
+
read_inheritable_attribute(:attr_protected) || []
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Returns an array of all the attributes that have been made accessible to mass-assignment.
|
|
108
|
+
def accessible_attributes
|
|
109
|
+
read_inheritable_attribute(:attr_accessible) || []
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def attributes=(attrs)#:nodoc:
|
|
114
|
+
super(remove_protected_attributes(attrs))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
def remove_protected_attributes(attrs)
|
|
119
|
+
if self.class.accessible_attributes.empty?
|
|
120
|
+
attrs.reject { |k, v| self.class.protected_attributes.include?(k.to_s) }
|
|
121
|
+
else
|
|
122
|
+
attrs.reject { |k, v| !self.class.accessible_attributes.include?(k.to_s) }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module MongoModel
|
|
2
|
+
module AttributeMethods
|
|
3
|
+
module Query
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
attribute_method_suffix "?"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Returns true if the attribute is not blank (i.e. it has some value). Otherwise returns false.
|
|
11
|
+
def query_attribute(name)
|
|
12
|
+
attributes.has?(name.to_sym)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
# Handle *? for method_missing.
|
|
17
|
+
def attribute?(attribute_name)
|
|
18
|
+
query_attribute(attribute_name)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module MongoModel
|
|
2
|
+
module AttributeMethods
|
|
3
|
+
module Read
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
attribute_method_suffix ""
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Returns the value of the attribute identified by +name+ after it has been typecast (for example,
|
|
11
|
+
# "2004-12-12" in a date property is cast to a date object, like Date.new(2004, 12, 12)).
|
|
12
|
+
def read_attribute(name)
|
|
13
|
+
attributes[name.to_sym]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns the value of the attribute identified by <tt>name</tt> after it has been typecast (for example,
|
|
17
|
+
# "2004-12-12" in a date property is cast to a date object, like Date.new(2004, 12, 12)).
|
|
18
|
+
# (Alias for read_attribute).
|
|
19
|
+
def [](name)
|
|
20
|
+
read_attribute(name)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
def attribute(attribute_name)
|
|
25
|
+
read_attribute(attribute_name)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module MongoModel
|
|
2
|
+
module AttributeMethods
|
|
3
|
+
module Write
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
attribute_method_suffix "="
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Updates the attribute identified by <tt>name</tt> with the specified +value+.
|
|
11
|
+
# Values are typecast to the appropriate type determined by the property.
|
|
12
|
+
def write_attribute(name, value)
|
|
13
|
+
attributes[name.to_sym] = value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Updates the attribute identified by <tt>name</tt> with the specified +value+.
|
|
17
|
+
# (Alias for the protected write_attribute method).
|
|
18
|
+
def []=(name, value)
|
|
19
|
+
write_attribute(name, value)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
# Handle *= for method_missing.
|
|
24
|
+
def attribute=(attribute_name, value)
|
|
25
|
+
write_attribute(attribute_name, value)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require 'active_support/core_ext/module/aliasing'
|
|
2
|
+
require 'active_support/core_ext/class/removal'
|
|
3
|
+
|
|
4
|
+
module MongoModel
|
|
5
|
+
module Attributes
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
alias_method_chain :initialize, :attributes
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize_with_attributes(attrs={})
|
|
13
|
+
initialize_without_attributes
|
|
14
|
+
|
|
15
|
+
self.attributes = attrs
|
|
16
|
+
yield self if block_given?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def attributes
|
|
20
|
+
@attributes ||= Attributes::Store.new(self)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def attributes=(attrs)
|
|
24
|
+
attrs.each do |attr, value|
|
|
25
|
+
if respond_to?("#{attr}=")
|
|
26
|
+
send("#{attr}=", value)
|
|
27
|
+
else
|
|
28
|
+
write_attribute(attr, value)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def freeze
|
|
34
|
+
attributes.freeze; self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def frozen?
|
|
38
|
+
attributes.frozen?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns duplicated record with unfreezed attributes.
|
|
42
|
+
def dup
|
|
43
|
+
obj = super
|
|
44
|
+
obj.instance_variable_set('@attributes', instance_variable_get('@attributes').dup)
|
|
45
|
+
obj
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_mongo
|
|
49
|
+
attributes.to_mongo
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def embedded_documents
|
|
53
|
+
docs = []
|
|
54
|
+
|
|
55
|
+
docs.concat attributes.values.select { |attr| attr.is_a?(EmbeddedDocument) }
|
|
56
|
+
|
|
57
|
+
attributes.values.select { |attr| attr.is_a?(Collection) }.each do |collection|
|
|
58
|
+
docs.concat collection.embedded_documents
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
docs
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
module ClassMethods
|
|
65
|
+
def from_mongo(hash)
|
|
66
|
+
doc = class_for_type(hash['_type']).new
|
|
67
|
+
doc.attributes.from_mongo!(hash)
|
|
68
|
+
doc
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
def class_for_type(type)
|
|
73
|
+
type = type.constantize
|
|
74
|
+
|
|
75
|
+
if (subclasses + [name]).include?(type.to_s)
|
|
76
|
+
type
|
|
77
|
+
else
|
|
78
|
+
raise DocumentNotFound, "Document not of the correct type (got #{type.to_s})"
|
|
79
|
+
end
|
|
80
|
+
rescue NameError
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
require 'active_support/core_ext/module/aliasing'
|
|
2
|
+
|
|
3
|
+
module MongoModel
|
|
4
|
+
# Callbacks are hooks into the lifecycle of a MongoModel object that allow you to trigger logic
|
|
5
|
+
# before or after an alteration of the object state. This can be used to make sure that associated and
|
|
6
|
+
# dependent objects are deleted when +destroy+ is called (by overwriting +before_destroy+) or to massage attributes
|
|
7
|
+
# before they're validated (by overwriting +before_validation+). As an example of the callbacks initiated, consider
|
|
8
|
+
# the <tt>Document#save</tt> call for a new document:
|
|
9
|
+
#
|
|
10
|
+
# * (-) <tt>save</tt>
|
|
11
|
+
# * (-) <tt>valid</tt>
|
|
12
|
+
# * (1) <tt>before_validation</tt>
|
|
13
|
+
# * (-) <tt>validate</tt>
|
|
14
|
+
# * (-) <tt>validate_on_create</tt>
|
|
15
|
+
# * (2) <tt>after_validation</tt>
|
|
16
|
+
# * (3) <tt>before_save</tt>
|
|
17
|
+
# * (4) <tt>before_create</tt>
|
|
18
|
+
# * (-) <tt>create</tt>
|
|
19
|
+
# * (5) <tt>after_create</tt>
|
|
20
|
+
# * (6) <tt>after_save</tt>
|
|
21
|
+
#
|
|
22
|
+
# That's a total of eight callbacks, which gives you immense power to react and prepare for each state in the
|
|
23
|
+
# MongoModel lifecycle. The sequence for calling <tt>Document#save</tt> for an existing record is similar, except that each
|
|
24
|
+
# <tt>_on_create</tt> callback is replaced by the corresponding <tt>_on_update</tt> callback.
|
|
25
|
+
#
|
|
26
|
+
# Examples:
|
|
27
|
+
# class CreditCard < MongoModel::Document
|
|
28
|
+
# # Strip everything but digits, so the user can specify "555 234 34" or
|
|
29
|
+
# # "5552-3434" or both will mean "55523434"
|
|
30
|
+
# def before_validation_on_create
|
|
31
|
+
# self.number = number.gsub(/[^0-9]/, "") if number?
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# class Subscription < MongoModel::Document
|
|
36
|
+
# before_create :record_signup
|
|
37
|
+
#
|
|
38
|
+
# private
|
|
39
|
+
# def record_signup
|
|
40
|
+
# self.signed_up_on = Date.today
|
|
41
|
+
# end
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# == Inheritable callback queues
|
|
45
|
+
#
|
|
46
|
+
# Besides the overwritable callback methods, it's also possible to register callbacks through the use of the callback macros.
|
|
47
|
+
# Their main advantage is that the macros add behavior into a callback queue that is kept intact down through an inheritance
|
|
48
|
+
# hierarchy. Example:
|
|
49
|
+
#
|
|
50
|
+
# class Topic < MongoModel::Document
|
|
51
|
+
# before_destroy :destroy_author
|
|
52
|
+
# end
|
|
53
|
+
#
|
|
54
|
+
# class Reply < Topic
|
|
55
|
+
# before_destroy :destroy_readers
|
|
56
|
+
# end
|
|
57
|
+
#
|
|
58
|
+
# Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is run, both +destroy_author+ and
|
|
59
|
+
# +destroy_readers+ are called. Contrast this to the situation where we've implemented the save behavior through overwriteable
|
|
60
|
+
# methods:
|
|
61
|
+
#
|
|
62
|
+
# class Topic < MongoModel::Document
|
|
63
|
+
# def before_destroy() destroy_author end
|
|
64
|
+
# end
|
|
65
|
+
#
|
|
66
|
+
# class Reply < Topic
|
|
67
|
+
# def before_destroy() destroy_readers end
|
|
68
|
+
# end
|
|
69
|
+
#
|
|
70
|
+
# In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+. So, use the callback macros when
|
|
71
|
+
# you want to ensure that a certain callback is called for the entire hierarchy, and use the regular overwriteable methods
|
|
72
|
+
# when you want to leave it up to each descendant to decide whether they want to call +super+ and trigger the inherited callbacks.
|
|
73
|
+
#
|
|
74
|
+
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the callbacks before specifying the
|
|
75
|
+
# associations. Otherwise, you might trigger the loading of a child before the parent has registered the callbacks and they won't
|
|
76
|
+
# be inherited.
|
|
77
|
+
#
|
|
78
|
+
# == Types of callbacks
|
|
79
|
+
#
|
|
80
|
+
# There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects,
|
|
81
|
+
# inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects are the
|
|
82
|
+
# recommended approaches, inline methods using a proc are sometimes appropriate (such as for creating mix-ins), and inline
|
|
83
|
+
# eval methods are deprecated.
|
|
84
|
+
#
|
|
85
|
+
# The method reference callbacks work by specifying a protected or private method available in the object, like this:
|
|
86
|
+
#
|
|
87
|
+
# class Topic < MongoModel::Document
|
|
88
|
+
# before_destroy :delete_parents
|
|
89
|
+
#
|
|
90
|
+
# private
|
|
91
|
+
# def delete_parents
|
|
92
|
+
# self.class.delete_all "parent_id = #{id}"
|
|
93
|
+
# end
|
|
94
|
+
# end
|
|
95
|
+
#
|
|
96
|
+
# The callback objects have methods named after the callback called with the record as the only parameter, such as:
|
|
97
|
+
#
|
|
98
|
+
# class BankAccount < MongoModel::Document
|
|
99
|
+
# before_save EncryptionWrapper.new
|
|
100
|
+
# after_save EncryptionWrapper.new
|
|
101
|
+
# after_initialize EncryptionWrapper.new
|
|
102
|
+
# end
|
|
103
|
+
#
|
|
104
|
+
# class EncryptionWrapper
|
|
105
|
+
# def before_save(doc)
|
|
106
|
+
# doc.credit_card_number = encrypt(doc.credit_card_number)
|
|
107
|
+
# end
|
|
108
|
+
#
|
|
109
|
+
# def after_save(doc)
|
|
110
|
+
# doc.credit_card_number = decrypt(doc.credit_card_number)
|
|
111
|
+
# end
|
|
112
|
+
#
|
|
113
|
+
# alias_method :after_load, :after_save
|
|
114
|
+
#
|
|
115
|
+
# private
|
|
116
|
+
# def encrypt(value)
|
|
117
|
+
# # Secrecy is committed
|
|
118
|
+
# end
|
|
119
|
+
#
|
|
120
|
+
# def decrypt(value)
|
|
121
|
+
# # Secrecy is unveiled
|
|
122
|
+
# end
|
|
123
|
+
# end
|
|
124
|
+
#
|
|
125
|
+
# So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
|
|
126
|
+
# a method by the name of the callback messaged. You can make these callbacks more flexible by passing in other
|
|
127
|
+
# initialization data such as the name of the attribute to work with:
|
|
128
|
+
#
|
|
129
|
+
# class BankAccount < MongoModel::Document
|
|
130
|
+
# before_save EncryptionWrapper.new("credit_card_number")
|
|
131
|
+
# after_save EncryptionWrapper.new("credit_card_number")
|
|
132
|
+
# after_initialize EncryptionWrapper.new("credit_card_number")
|
|
133
|
+
# end
|
|
134
|
+
#
|
|
135
|
+
# class EncryptionWrapper
|
|
136
|
+
# def initialize(attribute)
|
|
137
|
+
# @attribute = attribute
|
|
138
|
+
# end
|
|
139
|
+
#
|
|
140
|
+
# def before_save(doc)
|
|
141
|
+
# doc.send("#{@attribute}=", encrypt(doc.send("#{@attribute}")))
|
|
142
|
+
# end
|
|
143
|
+
#
|
|
144
|
+
# def after_save(record)
|
|
145
|
+
# doc.send("#{@attribute}=", decrypt(doc.send("#{@attribute}")))
|
|
146
|
+
# end
|
|
147
|
+
#
|
|
148
|
+
# alias_method :after_load, :after_save
|
|
149
|
+
#
|
|
150
|
+
# private
|
|
151
|
+
# def encrypt(value)
|
|
152
|
+
# # Secrecy is committed
|
|
153
|
+
# end
|
|
154
|
+
#
|
|
155
|
+
# def decrypt(value)
|
|
156
|
+
# # Secrecy is unveiled
|
|
157
|
+
# end
|
|
158
|
+
# end
|
|
159
|
+
#
|
|
160
|
+
# The callback macros usually accept a symbol for the method they're supposed to run, but you can also pass a "method string",
|
|
161
|
+
# which will then be evaluated within the binding of the callback. Example:
|
|
162
|
+
#
|
|
163
|
+
# class Topic < MongoModel::Document
|
|
164
|
+
# before_destroy 'self.class.delete_all "parent_id = #{id}"'
|
|
165
|
+
# end
|
|
166
|
+
#
|
|
167
|
+
# Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback is triggered. Also note that these
|
|
168
|
+
# inline callbacks can be stacked just like the regular ones:
|
|
169
|
+
#
|
|
170
|
+
# class Topic < MongoModel::Document
|
|
171
|
+
# before_destroy 'self.class.delete_all "parent_id = #{id}"',
|
|
172
|
+
# 'puts "Evaluated after parents are destroyed"'
|
|
173
|
+
# end
|
|
174
|
+
#
|
|
175
|
+
# == The +after_find+ and +after_initialize+ exceptions
|
|
176
|
+
#
|
|
177
|
+
# Because +after_find+ and +after_initialize+ are called for each object found and instantiated by find, we've had
|
|
178
|
+
# to implement a simple performance constraint (50% more speed on a simple test case). Unlike all the other callbacks, +after_find+ and
|
|
179
|
+
# +after_initialize+ will only be run if an explicit implementation is defined (<tt>def after_find</tt>). In that case, all of the
|
|
180
|
+
# callback types will be called.
|
|
181
|
+
#
|
|
182
|
+
# == <tt>before_validation*</tt> returning statements
|
|
183
|
+
#
|
|
184
|
+
# If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be aborted and <tt>Document#save</tt> will return +false+.
|
|
185
|
+
# If Document#save! is called it will raise a MongoModel::DocumentInvalid exception.
|
|
186
|
+
# Nothing will be appended to the errors object.
|
|
187
|
+
#
|
|
188
|
+
# == Canceling callbacks
|
|
189
|
+
#
|
|
190
|
+
# If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are cancelled. If an <tt>after_*</tt> callback returns
|
|
191
|
+
# +false+, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
|
|
192
|
+
# defined as methods on the model, which are called last.
|
|
193
|
+
module Callbacks
|
|
194
|
+
extend ActiveSupport::Concern
|
|
195
|
+
|
|
196
|
+
include ActiveSupport::Callbacks
|
|
197
|
+
|
|
198
|
+
CALLBACKS = [
|
|
199
|
+
:after_initialize, :after_find, :before_validation, :after_validation,
|
|
200
|
+
:before_save, :around_save, :after_save, :before_create, :around_create,
|
|
201
|
+
:after_create, :before_update, :around_update, :after_update,
|
|
202
|
+
:before_destroy, :around_destroy, :after_destroy
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
included do
|
|
206
|
+
[:initialize, :valid?].each do |method|
|
|
207
|
+
alias_method_chain method, :callbacks
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
define_callbacks :initialize, :find, :save, :create, :update, :destroy,
|
|
211
|
+
:validation, :terminator => "result == false", :scope => [:kind, :name]
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
module ClassMethods
|
|
215
|
+
def after_initialize(*args, &block)
|
|
216
|
+
options = args.extract_options!
|
|
217
|
+
options[:prepend] = true
|
|
218
|
+
set_callback(:initialize, :after, *(args << options), &block)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def after_find(*args, &block)
|
|
222
|
+
options = args.extract_options!
|
|
223
|
+
options[:prepend] = true
|
|
224
|
+
set_callback(:find, :after, *(args << options), &block)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
[:save, :create, :update, :destroy].each do |callback|
|
|
228
|
+
module_eval <<-CALLBACKS, __FILE__, __LINE__
|
|
229
|
+
def before_#{callback}(*args, &block)
|
|
230
|
+
set_callback(:#{callback}, :before, *args, &block)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def around_#{callback}(*args, &block)
|
|
234
|
+
set_callback(:#{callback}, :around, *args, &block)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def after_#{callback}(*args, &block)
|
|
238
|
+
options = args.extract_options!
|
|
239
|
+
options[:prepend] = true
|
|
240
|
+
options[:if] = Array(options[:if]) << "!halted && value != false"
|
|
241
|
+
set_callback(:#{callback}, :after, *(args << options), &block)
|
|
242
|
+
end
|
|
243
|
+
CALLBACKS
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def before_validation(*args, &block)
|
|
247
|
+
options = args.extract_options!
|
|
248
|
+
if options[:on]
|
|
249
|
+
options[:if] = Array(options[:if])
|
|
250
|
+
options[:if] << "@_on_validate == :#{options[:on]}"
|
|
251
|
+
end
|
|
252
|
+
set_callback(:validation, :before, *(args << options), &block)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def after_validation(*args, &block)
|
|
256
|
+
options = args.extract_options!
|
|
257
|
+
options[:if] = Array(options[:if])
|
|
258
|
+
options[:if] << "!halted"
|
|
259
|
+
options[:if] << "@_on_validate == :#{options[:on]}" if options[:on]
|
|
260
|
+
options[:prepend] = true
|
|
261
|
+
set_callback(:validation, :after, *(args << options), &block)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def initialize_with_callbacks(*args, &block) #:nodoc:
|
|
266
|
+
initialize_without_callbacks(*args, &block)
|
|
267
|
+
run_callbacks_with_embedded(:initialize)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def valid_with_callbacks? #:nodoc:
|
|
271
|
+
@_on_validate = new_record? ? :create : :update
|
|
272
|
+
run_callbacks(:validation) do
|
|
273
|
+
valid_without_callbacks?
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def run_callbacks_with_embedded(kind, *args, &block)
|
|
278
|
+
if block_given?
|
|
279
|
+
embedded_callbacks = nest_embedded_callbacks(kind, *args, &block)
|
|
280
|
+
run_callbacks(kind, *args, &embedded_callbacks)
|
|
281
|
+
else
|
|
282
|
+
run_callbacks(kind, *args)
|
|
283
|
+
embedded_documents.each { |doc| doc.run_callbacks(kind, *args) } unless kind == :initialize
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
private
|
|
288
|
+
def nest_embedded_callbacks(kind, *args, &block)
|
|
289
|
+
embedded_documents.inject(block) do |block, doc|
|
|
290
|
+
Proc.new { doc.run_callbacks(kind, *args, &block) }
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|