humanoid 1.2.7

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 (210) hide show
  1. data/.gitignore +6 -0
  2. data/.watchr +29 -0
  3. data/HISTORY +342 -0
  4. data/MIT_LICENSE +20 -0
  5. data/README.rdoc +56 -0
  6. data/Rakefile +53 -0
  7. data/VERSION +1 -0
  8. data/caliper.yml +4 -0
  9. data/humanoid.gemspec +374 -0
  10. data/lib/humanoid.rb +111 -0
  11. data/lib/humanoid/associations.rb +258 -0
  12. data/lib/humanoid/associations/belongs_to.rb +64 -0
  13. data/lib/humanoid/associations/belongs_to_related.rb +62 -0
  14. data/lib/humanoid/associations/has_many.rb +180 -0
  15. data/lib/humanoid/associations/has_many_related.rb +109 -0
  16. data/lib/humanoid/associations/has_one.rb +95 -0
  17. data/lib/humanoid/associations/has_one_related.rb +81 -0
  18. data/lib/humanoid/associations/options.rb +57 -0
  19. data/lib/humanoid/associations/proxy.rb +31 -0
  20. data/lib/humanoid/attributes.rb +184 -0
  21. data/lib/humanoid/callbacks.rb +23 -0
  22. data/lib/humanoid/collection.rb +118 -0
  23. data/lib/humanoid/collections/cyclic_iterator.rb +34 -0
  24. data/lib/humanoid/collections/master.rb +28 -0
  25. data/lib/humanoid/collections/mimic.rb +46 -0
  26. data/lib/humanoid/collections/operations.rb +41 -0
  27. data/lib/humanoid/collections/slaves.rb +44 -0
  28. data/lib/humanoid/commands.rb +182 -0
  29. data/lib/humanoid/commands/create.rb +21 -0
  30. data/lib/humanoid/commands/delete.rb +16 -0
  31. data/lib/humanoid/commands/delete_all.rb +23 -0
  32. data/lib/humanoid/commands/deletion.rb +18 -0
  33. data/lib/humanoid/commands/destroy.rb +19 -0
  34. data/lib/humanoid/commands/destroy_all.rb +23 -0
  35. data/lib/humanoid/commands/save.rb +27 -0
  36. data/lib/humanoid/components.rb +24 -0
  37. data/lib/humanoid/config.rb +84 -0
  38. data/lib/humanoid/contexts.rb +25 -0
  39. data/lib/humanoid/contexts/enumerable.rb +117 -0
  40. data/lib/humanoid/contexts/ids.rb +25 -0
  41. data/lib/humanoid/contexts/mongo.rb +224 -0
  42. data/lib/humanoid/contexts/paging.rb +42 -0
  43. data/lib/humanoid/criteria.rb +259 -0
  44. data/lib/humanoid/criterion/complex.rb +21 -0
  45. data/lib/humanoid/criterion/exclusion.rb +65 -0
  46. data/lib/humanoid/criterion/inclusion.rb +91 -0
  47. data/lib/humanoid/criterion/optional.rb +128 -0
  48. data/lib/humanoid/cursor.rb +82 -0
  49. data/lib/humanoid/document.rb +300 -0
  50. data/lib/humanoid/enslavement.rb +38 -0
  51. data/lib/humanoid/errors.rb +77 -0
  52. data/lib/humanoid/extensions.rb +84 -0
  53. data/lib/humanoid/extensions/array/accessors.rb +17 -0
  54. data/lib/humanoid/extensions/array/aliasing.rb +4 -0
  55. data/lib/humanoid/extensions/array/assimilation.rb +26 -0
  56. data/lib/humanoid/extensions/array/conversions.rb +29 -0
  57. data/lib/humanoid/extensions/array/parentization.rb +13 -0
  58. data/lib/humanoid/extensions/boolean/conversions.rb +16 -0
  59. data/lib/humanoid/extensions/date/conversions.rb +15 -0
  60. data/lib/humanoid/extensions/datetime/conversions.rb +17 -0
  61. data/lib/humanoid/extensions/float/conversions.rb +16 -0
  62. data/lib/humanoid/extensions/hash/accessors.rb +38 -0
  63. data/lib/humanoid/extensions/hash/assimilation.rb +30 -0
  64. data/lib/humanoid/extensions/hash/conversions.rb +15 -0
  65. data/lib/humanoid/extensions/hash/criteria_helpers.rb +20 -0
  66. data/lib/humanoid/extensions/hash/scoping.rb +12 -0
  67. data/lib/humanoid/extensions/integer/conversions.rb +16 -0
  68. data/lib/humanoid/extensions/nil/assimilation.rb +13 -0
  69. data/lib/humanoid/extensions/object/conversions.rb +33 -0
  70. data/lib/humanoid/extensions/proc/scoping.rb +12 -0
  71. data/lib/humanoid/extensions/string/conversions.rb +15 -0
  72. data/lib/humanoid/extensions/string/inflections.rb +97 -0
  73. data/lib/humanoid/extensions/symbol/inflections.rb +36 -0
  74. data/lib/humanoid/extensions/time/conversions.rb +18 -0
  75. data/lib/humanoid/factory.rb +19 -0
  76. data/lib/humanoid/field.rb +39 -0
  77. data/lib/humanoid/fields.rb +62 -0
  78. data/lib/humanoid/finders.rb +224 -0
  79. data/lib/humanoid/identity.rb +39 -0
  80. data/lib/humanoid/indexes.rb +30 -0
  81. data/lib/humanoid/matchers.rb +36 -0
  82. data/lib/humanoid/matchers/all.rb +11 -0
  83. data/lib/humanoid/matchers/default.rb +26 -0
  84. data/lib/humanoid/matchers/exists.rb +13 -0
  85. data/lib/humanoid/matchers/gt.rb +11 -0
  86. data/lib/humanoid/matchers/gte.rb +11 -0
  87. data/lib/humanoid/matchers/in.rb +11 -0
  88. data/lib/humanoid/matchers/lt.rb +11 -0
  89. data/lib/humanoid/matchers/lte.rb +11 -0
  90. data/lib/humanoid/matchers/ne.rb +11 -0
  91. data/lib/humanoid/matchers/nin.rb +11 -0
  92. data/lib/humanoid/matchers/size.rb +11 -0
  93. data/lib/humanoid/memoization.rb +27 -0
  94. data/lib/humanoid/named_scope.rb +40 -0
  95. data/lib/humanoid/scope.rb +75 -0
  96. data/lib/humanoid/timestamps.rb +30 -0
  97. data/lib/humanoid/versioning.rb +28 -0
  98. data/perf/benchmark.rb +77 -0
  99. data/spec/integration/humanoid/associations_spec.rb +301 -0
  100. data/spec/integration/humanoid/attributes_spec.rb +22 -0
  101. data/spec/integration/humanoid/commands_spec.rb +216 -0
  102. data/spec/integration/humanoid/contexts/enumerable_spec.rb +33 -0
  103. data/spec/integration/humanoid/criteria_spec.rb +224 -0
  104. data/spec/integration/humanoid/document_spec.rb +587 -0
  105. data/spec/integration/humanoid/extensions_spec.rb +26 -0
  106. data/spec/integration/humanoid/finders_spec.rb +119 -0
  107. data/spec/integration/humanoid/inheritance_spec.rb +137 -0
  108. data/spec/integration/humanoid/named_scope_spec.rb +46 -0
  109. data/spec/models/address.rb +39 -0
  110. data/spec/models/animal.rb +6 -0
  111. data/spec/models/comment.rb +8 -0
  112. data/spec/models/country_code.rb +6 -0
  113. data/spec/models/employer.rb +5 -0
  114. data/spec/models/game.rb +6 -0
  115. data/spec/models/inheritance.rb +56 -0
  116. data/spec/models/location.rb +5 -0
  117. data/spec/models/mixed_drink.rb +4 -0
  118. data/spec/models/name.rb +13 -0
  119. data/spec/models/namespacing.rb +11 -0
  120. data/spec/models/patient.rb +4 -0
  121. data/spec/models/person.rb +98 -0
  122. data/spec/models/pet.rb +7 -0
  123. data/spec/models/pet_owner.rb +6 -0
  124. data/spec/models/phone.rb +7 -0
  125. data/spec/models/post.rb +15 -0
  126. data/spec/models/translation.rb +5 -0
  127. data/spec/models/vet_visit.rb +5 -0
  128. data/spec/spec.opts +3 -0
  129. data/spec/spec_helper.rb +31 -0
  130. data/spec/unit/mongoid/associations/belongs_to_related_spec.rb +141 -0
  131. data/spec/unit/mongoid/associations/belongs_to_spec.rb +193 -0
  132. data/spec/unit/mongoid/associations/has_many_related_spec.rb +387 -0
  133. data/spec/unit/mongoid/associations/has_many_spec.rb +471 -0
  134. data/spec/unit/mongoid/associations/has_one_related_spec.rb +179 -0
  135. data/spec/unit/mongoid/associations/has_one_spec.rb +282 -0
  136. data/spec/unit/mongoid/associations/options_spec.rb +191 -0
  137. data/spec/unit/mongoid/associations_spec.rb +545 -0
  138. data/spec/unit/mongoid/attributes_spec.rb +484 -0
  139. data/spec/unit/mongoid/callbacks_spec.rb +55 -0
  140. data/spec/unit/mongoid/collection_spec.rb +171 -0
  141. data/spec/unit/mongoid/collections/cyclic_iterator_spec.rb +75 -0
  142. data/spec/unit/mongoid/collections/master_spec.rb +41 -0
  143. data/spec/unit/mongoid/collections/mimic_spec.rb +43 -0
  144. data/spec/unit/mongoid/collections/slaves_spec.rb +81 -0
  145. data/spec/unit/mongoid/commands/create_spec.rb +30 -0
  146. data/spec/unit/mongoid/commands/delete_all_spec.rb +58 -0
  147. data/spec/unit/mongoid/commands/delete_spec.rb +35 -0
  148. data/spec/unit/mongoid/commands/destroy_all_spec.rb +23 -0
  149. data/spec/unit/mongoid/commands/destroy_spec.rb +44 -0
  150. data/spec/unit/mongoid/commands/save_spec.rb +105 -0
  151. data/spec/unit/mongoid/commands_spec.rb +282 -0
  152. data/spec/unit/mongoid/config_spec.rb +165 -0
  153. data/spec/unit/mongoid/contexts/enumerable_spec.rb +374 -0
  154. data/spec/unit/mongoid/contexts/mongo_spec.rb +505 -0
  155. data/spec/unit/mongoid/contexts_spec.rb +25 -0
  156. data/spec/unit/mongoid/criteria_spec.rb +769 -0
  157. data/spec/unit/mongoid/criterion/complex_spec.rb +19 -0
  158. data/spec/unit/mongoid/criterion/exclusion_spec.rb +91 -0
  159. data/spec/unit/mongoid/criterion/inclusion_spec.rb +211 -0
  160. data/spec/unit/mongoid/criterion/optional_spec.rb +329 -0
  161. data/spec/unit/mongoid/cursor_spec.rb +74 -0
  162. data/spec/unit/mongoid/document_spec.rb +986 -0
  163. data/spec/unit/mongoid/enslavement_spec.rb +63 -0
  164. data/spec/unit/mongoid/errors_spec.rb +103 -0
  165. data/spec/unit/mongoid/extensions/array/accessors_spec.rb +50 -0
  166. data/spec/unit/mongoid/extensions/array/assimilation_spec.rb +24 -0
  167. data/spec/unit/mongoid/extensions/array/conversions_spec.rb +35 -0
  168. data/spec/unit/mongoid/extensions/array/parentization_spec.rb +20 -0
  169. data/spec/unit/mongoid/extensions/boolean/conversions_spec.rb +49 -0
  170. data/spec/unit/mongoid/extensions/date/conversions_spec.rb +102 -0
  171. data/spec/unit/mongoid/extensions/datetime/conversions_spec.rb +70 -0
  172. data/spec/unit/mongoid/extensions/float/conversions_spec.rb +61 -0
  173. data/spec/unit/mongoid/extensions/hash/accessors_spec.rb +184 -0
  174. data/spec/unit/mongoid/extensions/hash/assimilation_spec.rb +46 -0
  175. data/spec/unit/mongoid/extensions/hash/conversions_spec.rb +21 -0
  176. data/spec/unit/mongoid/extensions/hash/criteria_helpers_spec.rb +17 -0
  177. data/spec/unit/mongoid/extensions/hash/scoping_spec.rb +14 -0
  178. data/spec/unit/mongoid/extensions/integer/conversions_spec.rb +61 -0
  179. data/spec/unit/mongoid/extensions/nil/assimilation_spec.rb +24 -0
  180. data/spec/unit/mongoid/extensions/object/conversions_spec.rb +43 -0
  181. data/spec/unit/mongoid/extensions/proc/scoping_spec.rb +34 -0
  182. data/spec/unit/mongoid/extensions/string/conversions_spec.rb +17 -0
  183. data/spec/unit/mongoid/extensions/string/inflections_spec.rb +208 -0
  184. data/spec/unit/mongoid/extensions/symbol/inflections_spec.rb +91 -0
  185. data/spec/unit/mongoid/extensions/time/conversions_spec.rb +70 -0
  186. data/spec/unit/mongoid/factory_spec.rb +31 -0
  187. data/spec/unit/mongoid/field_spec.rb +81 -0
  188. data/spec/unit/mongoid/fields_spec.rb +158 -0
  189. data/spec/unit/mongoid/finders_spec.rb +368 -0
  190. data/spec/unit/mongoid/identity_spec.rb +88 -0
  191. data/spec/unit/mongoid/indexes_spec.rb +93 -0
  192. data/spec/unit/mongoid/matchers/all_spec.rb +27 -0
  193. data/spec/unit/mongoid/matchers/default_spec.rb +27 -0
  194. data/spec/unit/mongoid/matchers/exists_spec.rb +56 -0
  195. data/spec/unit/mongoid/matchers/gt_spec.rb +39 -0
  196. data/spec/unit/mongoid/matchers/gte_spec.rb +49 -0
  197. data/spec/unit/mongoid/matchers/in_spec.rb +27 -0
  198. data/spec/unit/mongoid/matchers/lt_spec.rb +39 -0
  199. data/spec/unit/mongoid/matchers/lte_spec.rb +49 -0
  200. data/spec/unit/mongoid/matchers/ne_spec.rb +27 -0
  201. data/spec/unit/mongoid/matchers/nin_spec.rb +27 -0
  202. data/spec/unit/mongoid/matchers/size_spec.rb +27 -0
  203. data/spec/unit/mongoid/matchers_spec.rb +329 -0
  204. data/spec/unit/mongoid/memoization_spec.rb +75 -0
  205. data/spec/unit/mongoid/named_scope_spec.rb +123 -0
  206. data/spec/unit/mongoid/scope_spec.rb +240 -0
  207. data/spec/unit/mongoid/timestamps_spec.rb +25 -0
  208. data/spec/unit/mongoid/versioning_spec.rb +41 -0
  209. data/spec/unit/mongoid_spec.rb +37 -0
  210. metadata +431 -0
@@ -0,0 +1,128 @@
1
+ # encoding: utf-8
2
+ module Humanoid #:nodoc:
3
+ module Criterion #:nodoc:
4
+ module Optional
5
+ # Tells the criteria that the cursor that gets returned needs to be
6
+ # cached. This is so multiple iterations don't hit the database multiple
7
+ # times, however this is not advisable when working with large data sets
8
+ # as the entire results will get stored in memory.
9
+ #
10
+ # Example:
11
+ #
12
+ # <tt>criteria.cache</tt>
13
+ def cache
14
+ @options.merge!(:cache => true); self
15
+ end
16
+
17
+ # Will return true if the cache option has been set.
18
+ #
19
+ # Example:
20
+ #
21
+ # <tt>criteria.cached?</tt>
22
+ def cached?
23
+ @cached ||= @options.delete(:cache)
24
+ @cached == true
25
+ end
26
+
27
+ # Flags the criteria to execute against a read-only slave in the pool
28
+ # instead of master.
29
+ #
30
+ # Example:
31
+ #
32
+ # <tt>criteria.enslave</tt>
33
+ def enslave
34
+ @options.merge!(:enslave => true); self
35
+ end
36
+
37
+ # Adds a criterion to the +Criteria+ that specifies additional options
38
+ # to be passed to the Ruby driver, in the exact format for the driver.
39
+ #
40
+ # Options:
41
+ #
42
+ # extras: A +Hash+ that gets set to the driver options.
43
+ #
44
+ # Example:
45
+ #
46
+ # <tt>criteria.extras(:limit => 20, :skip => 40)</tt>
47
+ #
48
+ # Returns: <tt>self</tt>
49
+ def extras(extras)
50
+ @options.merge!(extras); filter_options; self
51
+ end
52
+
53
+ # Adds a criterion to the +Criteria+ that specifies an id that must be matched.
54
+ #
55
+ # Options:
56
+ #
57
+ # object_id: A +String+ representation of a <tt>Mongo::ObjectID</tt>
58
+ #
59
+ # Example:
60
+ #
61
+ # <tt>criteria.id("4ab2bc4b8ad548971900005c")</tt>
62
+ #
63
+ # Returns: <tt>self</tt>
64
+ def id(*args)
65
+ (args.flatten.size > 1) ? self.in(:_id => args.flatten) : (@selector[:_id] = args.first)
66
+ self
67
+ end
68
+
69
+ # Adds a criterion to the +Criteria+ that specifies the maximum number of
70
+ # results to return. This is mostly used in conjunction with <tt>skip()</tt>
71
+ # to handle paginated results.
72
+ #
73
+ # Options:
74
+ #
75
+ # value: An +Integer+ specifying the max number of results. Defaults to 20.
76
+ #
77
+ # Example:
78
+ #
79
+ # <tt>criteria.limit(100)</tt>
80
+ #
81
+ # Returns: <tt>self</tt>
82
+ def limit(value = 20)
83
+ @options[:limit] = value; self
84
+ end
85
+
86
+ # Returns the offset option. If a per_page option is in the list then it
87
+ # will replace it with a skip parameter and return the same value. Defaults
88
+ # to 20 if nothing was provided.
89
+ def offset(*args)
90
+ args.size > 0 ? skip(args.first) : @options[:skip]
91
+ end
92
+
93
+ # Adds a criterion to the +Criteria+ that specifies the sort order of
94
+ # the returned documents in the database. Similar to a SQL "ORDER BY".
95
+ #
96
+ # Options:
97
+ #
98
+ # params: An +Array+ of [field, direction] sorting pairs.
99
+ #
100
+ # Example:
101
+ #
102
+ # <tt>criteria.order_by([[:field1, :asc], [:field2, :desc]])</tt>
103
+ #
104
+ # Returns: <tt>self</tt>
105
+ def order_by(params = [])
106
+ @options[:sort] = params; self
107
+ end
108
+
109
+ # Adds a criterion to the +Criteria+ that specifies how many results to skip
110
+ # when returning Documents. This is mostly used in conjunction with
111
+ # <tt>limit()</tt> to handle paginated results, and is similar to the
112
+ # traditional "offset" parameter.
113
+ #
114
+ # Options:
115
+ #
116
+ # value: An +Integer+ specifying the number of results to skip. Defaults to 0.
117
+ #
118
+ # Example:
119
+ #
120
+ # <tt>criteria.skip(20)</tt>
121
+ #
122
+ # Returns: <tt>self</tt>
123
+ def skip(value = 0)
124
+ @options[:skip] = value; self
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,82 @@
1
+ # encoding: utf-8
2
+ module Humanoid #:nodoc
3
+ class Cursor
4
+ include Enumerable
5
+ # Operations on the Mongo::Cursor object that will not get overriden by the
6
+ # Humanoid::Cursor are defined here.
7
+ OPERATIONS = [
8
+ :admin,
9
+ :close,
10
+ :closed?,
11
+ :count,
12
+ :explain,
13
+ :fields,
14
+ :full_collection_name,
15
+ :hint,
16
+ :limit,
17
+ :order,
18
+ :query_options_hash,
19
+ :query_opts,
20
+ :selector,
21
+ :skip,
22
+ :snapshot,
23
+ :sort,
24
+ :timeout
25
+ ]
26
+
27
+ attr_reader :collection
28
+
29
+ # The operations above will all delegate to the proxied Mongo::Cursor.
30
+ #
31
+ # Example:
32
+ #
33
+ # <tt>cursor.close</tt>
34
+ OPERATIONS.each do |name|
35
+ define_method(name) { |*args| @cursor.send(name, *args) }
36
+ end
37
+
38
+ # Iterate over each document in the cursor and yield to it.
39
+ #
40
+ # Example:
41
+ #
42
+ # <tt>cursor.each { |doc| p doc.title }</tt>
43
+ def each
44
+ @cursor.each do |document|
45
+ yield Humanoid::Factory.build(@klass, document)
46
+ end
47
+ end
48
+
49
+ # Create the new +Humanoid::Cursor+.
50
+ #
51
+ # Options:
52
+ #
53
+ # collection: The Humanoid::Collection instance.
54
+ # cursor: The Mongo::Cursor to be proxied.
55
+ #
56
+ # Example:
57
+ #
58
+ # <tt>Humanoid::Cursor.new(Person, cursor)</tt>
59
+ def initialize(klass, collection, cursor)
60
+ @klass, @collection, @cursor = klass, collection, cursor
61
+ end
62
+
63
+ # Return the next document in the cursor. Will instantiate a new Humanoid
64
+ # document with the attributes.
65
+ #
66
+ # Example:
67
+ #
68
+ # <tt>cursor.next_document</tt>
69
+ def next_document
70
+ Humanoid::Factory.build(@klass, @cursor.next_document)
71
+ end
72
+
73
+ # Returns an array of all the documents in the cursor.
74
+ #
75
+ # Example:
76
+ #
77
+ # <tt>cursor.to_a</tt>
78
+ def to_a
79
+ @cursor.to_a.collect { |attrs| Humanoid::Factory.build(@klass, attrs) }
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,300 @@
1
+ # encoding: utf-8
2
+ module Humanoid #:nodoc:
3
+ module Document
4
+ def self.included(base)
5
+ base.class_eval do
6
+ include Components
7
+ include InstanceMethods
8
+ extend ClassMethods
9
+
10
+ cattr_accessor :_collection, :collection_name, :embedded, :primary_key, :hereditary
11
+
12
+ self.embedded = false
13
+ self.hereditary = false
14
+ self.collection_name = self.name.collectionize
15
+
16
+ attr_accessor :association_name, :_parent
17
+ attr_reader :new_record
18
+
19
+ delegate :collection, :db, :embedded, :primary_key, :to => "self.class"
20
+ end
21
+ end
22
+
23
+ module ClassMethods
24
+ # Return the database associated with this class.
25
+ def db
26
+ collection.db
27
+ end
28
+
29
+ # Returns the collection associated with this +Document+. If the
30
+ # document is embedded, there will be no collection associated
31
+ # with it.
32
+ #
33
+ # Returns: <tt>Mongo::Collection</tt>
34
+ def collection
35
+ raise Errors::InvalidCollection.new(self) if embedded
36
+ self._collection ||= Humanoid::Collection.new(self, self.collection_name)
37
+ add_indexes; self._collection
38
+ end
39
+
40
+ # Perform default behavior but mark the hierarchy as being hereditary.
41
+ def inherited(subclass)
42
+ super(subclass)
43
+ self.hereditary = true
44
+ end
45
+
46
+ # Returns a human readable version of the class.
47
+ #
48
+ # Example:
49
+ #
50
+ # <tt>MixedDrink.human_name # returns "Mixed Drink"</tt>
51
+ def human_name
52
+ name.labelize
53
+ end
54
+
55
+ # Instantiate a new object, only when loaded from the database or when
56
+ # the attributes have already been typecast.
57
+ #
58
+ # Example:
59
+ #
60
+ # <tt>Person.instantiate(:title => "Sir", :age => 30)</tt>
61
+ def instantiate(attrs = nil, allocating = false)
62
+ attributes = attrs || {}
63
+ if attributes["_id"] || allocating
64
+ document = allocate
65
+ document.instance_variable_set(:@attributes, attributes)
66
+ return document
67
+ else
68
+ return new(attrs)
69
+ end
70
+ end
71
+
72
+ # Defines the field that will be used for the id of this +Document+. This
73
+ # set the id of this +Document+ before save to a parameterized version of
74
+ # the field that was supplied. This is good for use for readable URLS in
75
+ # web applications.
76
+ #
77
+ # Example:
78
+ #
79
+ # class Person
80
+ # include Humanoid::Document
81
+ # field :first_name
82
+ # field :last_name
83
+ # key :first_name, :last_name
84
+ # end
85
+ def key(*fields)
86
+ self.primary_key = fields
87
+ before_save :identify
88
+ end
89
+
90
+ # Macro for setting the collection name to store in.
91
+ #
92
+ # Example:
93
+ #
94
+ # <tt>Person.store_in :populdation</tt>
95
+ def store_in(name)
96
+ self.collection_name = name.to_s
97
+ self._collection = Humanoid::Collection.new(self, name.to_s)
98
+ end
99
+
100
+ # Returns all types to query for when using this class as the base.
101
+ def _types
102
+ @_type ||= (self.subclasses + [ self.name ])
103
+ end
104
+
105
+ end
106
+
107
+ module InstanceMethods
108
+ # Performs equality checking on the attributes. For now we chack against
109
+ # all attributes excluding timestamps on the object.
110
+ def ==(other)
111
+ return false unless other.is_a?(Document)
112
+ attributes.except(:modified_at).except(:created_at) ==
113
+ other.attributes.except(:modified_at).except(:created_at)
114
+ end
115
+
116
+ # Delegates to ==
117
+ def eql?(comparison_object)
118
+ self == (comparison_object)
119
+ end
120
+
121
+ # Delegates to id in order to allow two records of the same type and id to work with something like:
122
+ # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
123
+ def hash
124
+ id.hash
125
+ end
126
+
127
+ # Introduces a child object into the +Document+ object graph. This will
128
+ # set up the relationships between the parent and child and update the
129
+ # attributes of the parent +Document+.
130
+ #
131
+ # Options:
132
+ #
133
+ # parent: The +Document+ to assimilate with.
134
+ # options: The association +Options+ for the child.
135
+ #
136
+ # Example:
137
+ #
138
+ # <tt>name.assimilate(person, options)</tt>
139
+ def assimilate(parent, options)
140
+ parentize(parent, options.name); notify; self
141
+ end
142
+
143
+ # Return the attributes hash with indifferent access.
144
+ def attributes
145
+ @attributes.with_indifferent_access
146
+ end
147
+
148
+ # Clone the current +Document+. This will return all attributes with the
149
+ # exception of the document's id and versions.
150
+ def clone
151
+ self.class.instantiate(@attributes.except("_id").except("versions").dup, true)
152
+ end
153
+
154
+ # Generate an id for this +Document+.
155
+ def identify
156
+ Identity.create(self)
157
+ end
158
+
159
+ # Instantiate a new +Document+, setting the Document's attributes if
160
+ # given. If no attributes are provided, they will be initialized with
161
+ # an empty +Hash+.
162
+ #
163
+ # If a primary key is defined, the document's id will be set to that key,
164
+ # otherwise it will be set to a fresh +Mongo::ObjectID+ string.
165
+ #
166
+ # Options:
167
+ #
168
+ # attrs: The attributes +Hash+ to set up the document with.
169
+ def initialize(attrs = nil)
170
+ @attributes = {}
171
+ process(attrs)
172
+ @attributes = attributes_with_defaults(@attributes)
173
+ @new_record = true if id.nil?
174
+ document = yield self if block_given?
175
+ identify
176
+ end
177
+
178
+ # Returns the class name plus its attributes.
179
+ def inspect
180
+ attrs = fields.map { |name, field| "#{name}: #{@attributes[name].inspect}" } * ", "
181
+ "#<#{self.class.name} _id: #{id}, #{attrs}>"
182
+ end
183
+
184
+ # Returns true is the +Document+ has not been persisted to the database,
185
+ # false if it has. This is determined by the variable @new_record
186
+ # and NOT if the object has an id.
187
+ def new_record?
188
+ @new_record == true
189
+ end
190
+
191
+ # Sets the new_record boolean - used after document is saved.
192
+ def new_record=(saved)
193
+ @new_record = saved
194
+ end
195
+
196
+ # Set the changed state of the +Document+ then notify observers that it has changed.
197
+ #
198
+ # Example:
199
+ #
200
+ # <tt>person.notify</tt>
201
+ def notify
202
+ changed(true)
203
+ notify_observers(self)
204
+ end
205
+
206
+ # Sets up a child/parent association. This is used for newly created
207
+ # objects so they can be properly added to the graph and have the parent
208
+ # observers set up properly.
209
+ #
210
+ # Options:
211
+ #
212
+ # abject: The parent object that needs to be set for the child.
213
+ # association_name: The name of the association for the child.
214
+ #
215
+ # Example:
216
+ #
217
+ # <tt>address.parentize(person, :addresses)</tt>
218
+ def parentize(object, association_name)
219
+ self._parent = object
220
+ self.association_name = association_name.to_s
221
+ add_observer(object)
222
+ end
223
+
224
+ # Return the attributes hash.
225
+ def raw_attributes
226
+ @attributes
227
+ end
228
+
229
+ # Reloads the +Document+ attributes from the database.
230
+ def reload
231
+ @attributes = collection.find_one(:_id => id)
232
+ self
233
+ end
234
+
235
+ # Remove a child document from this parent +Document+. Will reset the
236
+ # memoized association and notify the parent of the change.
237
+ def remove(child)
238
+ name = child.association_name
239
+ reset(name) { @attributes.remove(name, child.raw_attributes) }
240
+ notify
241
+ end
242
+
243
+ # Return the root +Document+ in the object graph. If the current +Document+
244
+ # is the root object in the graph it will return self.
245
+ def _root
246
+ object = self
247
+ while (object._parent) do object = object._parent; end
248
+ object || self
249
+ end
250
+
251
+ # Return an array with this +Document+ only in it.
252
+ def to_a
253
+ [ self ]
254
+ end
255
+
256
+ # Return this document as a JSON string. Nothing special is required here
257
+ # since Humanoid bubbles up all the child associations to the parent
258
+ # attribute +Hash+ using observers throughout the +Document+ lifecycle.
259
+ #
260
+ # Example:
261
+ #
262
+ # <tt>person.to_json</tt>
263
+ def to_json(options = nil)
264
+ attributes.to_json(options)
265
+ end
266
+
267
+ # Returns the id of the Document, used in Rails compatibility.
268
+ def to_param
269
+ id
270
+ end
271
+
272
+ # Observe a notify call from a child +Document+. This will either update
273
+ # existing attributes on the +Document+ or clear them out for the child if
274
+ # the clear boolean is provided.
275
+ #
276
+ # Options:
277
+ #
278
+ # child: The child +Document+ that sent the notification.
279
+ # clear: Will clear out the child's attributes if set to true.
280
+ #
281
+ # This will also cause the observing +Document+ to notify it's parent if
282
+ # there is any.
283
+ def update(child, clear = false)
284
+ name = child.association_name
285
+ attrs = child.instance_variable_get(:@attributes)
286
+ clear ? @attributes.delete(name) : @attributes.insert(name, attrs)
287
+ notify
288
+ end
289
+
290
+ protected
291
+ # apply default values to attributes - calling procs as required
292
+ def attributes_with_defaults(attributes = {})
293
+ default_values = defaults.merge(attributes)
294
+ default_values.each_pair do |key, val|
295
+ default_values[key] = val.call if val.respond_to?(:call)
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end