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