mongomodel 0.1

Sign up to get free protection for your applications and to get access to all the features.
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