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.
Files changed (108) hide show
  1. data/LICENSE +22 -0
  2. data/README.md +34 -0
  3. data/Rakefile +47 -0
  4. data/bin/console +45 -0
  5. data/lib/mongomodel.rb +92 -0
  6. data/lib/mongomodel/attributes/mongo.rb +40 -0
  7. data/lib/mongomodel/attributes/store.rb +30 -0
  8. data/lib/mongomodel/attributes/typecasting.rb +51 -0
  9. data/lib/mongomodel/concerns/abstract_class.rb +17 -0
  10. data/lib/mongomodel/concerns/activemodel.rb +11 -0
  11. data/lib/mongomodel/concerns/associations.rb +103 -0
  12. data/lib/mongomodel/concerns/associations/base/association.rb +33 -0
  13. data/lib/mongomodel/concerns/associations/base/definition.rb +56 -0
  14. data/lib/mongomodel/concerns/associations/base/proxy.rb +58 -0
  15. data/lib/mongomodel/concerns/associations/belongs_to.rb +68 -0
  16. data/lib/mongomodel/concerns/associations/has_many_by_foreign_key.rb +159 -0
  17. data/lib/mongomodel/concerns/associations/has_many_by_ids.rb +175 -0
  18. data/lib/mongomodel/concerns/attribute_methods.rb +55 -0
  19. data/lib/mongomodel/concerns/attribute_methods/before_type_cast.rb +29 -0
  20. data/lib/mongomodel/concerns/attribute_methods/dirty.rb +35 -0
  21. data/lib/mongomodel/concerns/attribute_methods/protected.rb +127 -0
  22. data/lib/mongomodel/concerns/attribute_methods/query.rb +22 -0
  23. data/lib/mongomodel/concerns/attribute_methods/read.rb +29 -0
  24. data/lib/mongomodel/concerns/attribute_methods/write.rb +29 -0
  25. data/lib/mongomodel/concerns/attributes.rb +85 -0
  26. data/lib/mongomodel/concerns/callbacks.rb +294 -0
  27. data/lib/mongomodel/concerns/logging.rb +15 -0
  28. data/lib/mongomodel/concerns/pretty_inspect.rb +29 -0
  29. data/lib/mongomodel/concerns/properties.rb +69 -0
  30. data/lib/mongomodel/concerns/record_status.rb +42 -0
  31. data/lib/mongomodel/concerns/timestamps.rb +32 -0
  32. data/lib/mongomodel/concerns/validations.rb +38 -0
  33. data/lib/mongomodel/concerns/validations/associated.rb +46 -0
  34. data/lib/mongomodel/document.rb +20 -0
  35. data/lib/mongomodel/document/callbacks.rb +46 -0
  36. data/lib/mongomodel/document/dynamic_finders.rb +88 -0
  37. data/lib/mongomodel/document/finders.rb +82 -0
  38. data/lib/mongomodel/document/indexes.rb +91 -0
  39. data/lib/mongomodel/document/optimistic_locking.rb +48 -0
  40. data/lib/mongomodel/document/persistence.rb +143 -0
  41. data/lib/mongomodel/document/scopes.rb +161 -0
  42. data/lib/mongomodel/document/validations.rb +68 -0
  43. data/lib/mongomodel/document/validations/uniqueness.rb +78 -0
  44. data/lib/mongomodel/embedded_document.rb +42 -0
  45. data/lib/mongomodel/locale/en.yml +55 -0
  46. data/lib/mongomodel/support/collection.rb +109 -0
  47. data/lib/mongomodel/support/configuration.rb +35 -0
  48. data/lib/mongomodel/support/core_extensions.rb +10 -0
  49. data/lib/mongomodel/support/exceptions.rb +25 -0
  50. data/lib/mongomodel/support/mongo_options.rb +177 -0
  51. data/lib/mongomodel/support/types.rb +35 -0
  52. data/lib/mongomodel/support/types/array.rb +11 -0
  53. data/lib/mongomodel/support/types/boolean.rb +25 -0
  54. data/lib/mongomodel/support/types/custom.rb +38 -0
  55. data/lib/mongomodel/support/types/date.rb +20 -0
  56. data/lib/mongomodel/support/types/float.rb +13 -0
  57. data/lib/mongomodel/support/types/hash.rb +18 -0
  58. data/lib/mongomodel/support/types/integer.rb +13 -0
  59. data/lib/mongomodel/support/types/object.rb +21 -0
  60. data/lib/mongomodel/support/types/string.rb +9 -0
  61. data/lib/mongomodel/support/types/symbol.rb +9 -0
  62. data/lib/mongomodel/support/types/time.rb +12 -0
  63. data/lib/mongomodel/version.rb +3 -0
  64. data/spec/mongomodel/attributes/store_spec.rb +273 -0
  65. data/spec/mongomodel/concerns/activemodel_spec.rb +61 -0
  66. data/spec/mongomodel/concerns/associations/belongs_to_spec.rb +153 -0
  67. data/spec/mongomodel/concerns/associations/has_many_by_foreign_key_spec.rb +165 -0
  68. data/spec/mongomodel/concerns/associations/has_many_by_ids_spec.rb +192 -0
  69. data/spec/mongomodel/concerns/attribute_methods/before_type_cast_spec.rb +46 -0
  70. data/spec/mongomodel/concerns/attribute_methods/dirty_spec.rb +131 -0
  71. data/spec/mongomodel/concerns/attribute_methods/protected_spec.rb +86 -0
  72. data/spec/mongomodel/concerns/attribute_methods/query_spec.rb +27 -0
  73. data/spec/mongomodel/concerns/attribute_methods/read_spec.rb +52 -0
  74. data/spec/mongomodel/concerns/attribute_methods/write_spec.rb +43 -0
  75. data/spec/mongomodel/concerns/attributes_spec.rb +152 -0
  76. data/spec/mongomodel/concerns/callbacks_spec.rb +90 -0
  77. data/spec/mongomodel/concerns/logging_spec.rb +20 -0
  78. data/spec/mongomodel/concerns/pretty_inspect_spec.rb +68 -0
  79. data/spec/mongomodel/concerns/properties_spec.rb +29 -0
  80. data/spec/mongomodel/concerns/timestamps_spec.rb +170 -0
  81. data/spec/mongomodel/concerns/validations_spec.rb +159 -0
  82. data/spec/mongomodel/document/callbacks_spec.rb +80 -0
  83. data/spec/mongomodel/document/dynamic_finders_spec.rb +183 -0
  84. data/spec/mongomodel/document/finders_spec.rb +231 -0
  85. data/spec/mongomodel/document/indexes_spec.rb +121 -0
  86. data/spec/mongomodel/document/optimistic_locking_spec.rb +57 -0
  87. data/spec/mongomodel/document/persistence_spec.rb +319 -0
  88. data/spec/mongomodel/document/scopes_spec.rb +204 -0
  89. data/spec/mongomodel/document/validations/uniqueness_spec.rb +217 -0
  90. data/spec/mongomodel/document/validations_spec.rb +132 -0
  91. data/spec/mongomodel/document_spec.rb +74 -0
  92. data/spec/mongomodel/embedded_document_spec.rb +66 -0
  93. data/spec/mongomodel/mongomodel_spec.rb +33 -0
  94. data/spec/mongomodel/support/collection_spec.rb +248 -0
  95. data/spec/mongomodel/support/mongo_options_spec.rb +295 -0
  96. data/spec/mongomodel/support/property_spec.rb +83 -0
  97. data/spec/spec.opts +6 -0
  98. data/spec/spec_helper.rb +21 -0
  99. data/spec/specdoc.opts +6 -0
  100. data/spec/support/callbacks.rb +44 -0
  101. data/spec/support/helpers/define_class.rb +24 -0
  102. data/spec/support/helpers/specs_for.rb +11 -0
  103. data/spec/support/matchers/be_a_subclass_of.rb +5 -0
  104. data/spec/support/matchers/respond_to_boolean.rb +17 -0
  105. data/spec/support/matchers/run_callbacks.rb +20 -0
  106. data/spec/support/models.rb +23 -0
  107. data/spec/support/time.rb +6 -0
  108. 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