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,91 @@
1
+ require 'active_support/core_ext/array/extract_options'
2
+
3
+ module MongoModel
4
+ module DocumentExtensions
5
+ module Indexes
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ index :_type
10
+ end
11
+
12
+ module ClassMethods
13
+ def property(name, *args, &block) #:nodoc:
14
+ property = super
15
+ index(name) if property.options[:index]
16
+ property
17
+ end
18
+
19
+ def index(*args)
20
+ index = Index.new(*args)
21
+ indexes << index
22
+ @_indexes_initialized = false
23
+ index
24
+ end
25
+
26
+ def indexes
27
+ read_inheritable_attribute(:indexes) || write_inheritable_attribute(:indexes, [])
28
+ end
29
+
30
+ def indexes_initialized?
31
+ @_indexes_initialized == true
32
+ end
33
+
34
+ def ensure_indexes!
35
+ indexes.each do |index|
36
+ collection.create_index(*index.to_args)
37
+ end
38
+
39
+ @_indexes_initialized = true
40
+ end
41
+
42
+ private
43
+ def _find(*)
44
+ ensure_indexes! unless indexes_initialized?
45
+ super
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ class Index
52
+ def initialize(*keys)
53
+ options = keys.extract_options!
54
+ @unique = options.delete(:unique)
55
+
56
+ keys.each do |key|
57
+ self.keys[key.to_sym] = :ascending
58
+ end
59
+
60
+ options.each do |key, order|
61
+ self.keys[key.to_sym] = order
62
+ end
63
+ end
64
+
65
+ def keys
66
+ @keys ||= OrderedHash.new
67
+ end
68
+
69
+ def unique?
70
+ @unique
71
+ end
72
+
73
+ def to_args
74
+ args = []
75
+
76
+ if keys.size == 1 && keys.all? { |k, o| o == :ascending }
77
+ args << keys.keys.first
78
+ else
79
+ args << keys.map { |k, o| [k, o == :ascending ? 1 : -1] }.sort_by { |k| k.first.to_s }
80
+ end
81
+
82
+ args << true if unique?
83
+
84
+ args
85
+ end
86
+
87
+ def ==(other)
88
+ other.is_a?(Index) && to_args == other.to_args
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,48 @@
1
+ module MongoModel
2
+ module DocumentExtensions
3
+ module OptimisticLocking
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def locking_enabled?
8
+ properties.include?(:_lock_version)
9
+ end
10
+
11
+ def lock_optimistically=(value)
12
+ if value == true
13
+ property :_lock_version, Integer, :default => 0, :internal => true, :protected => true
14
+ before_save :increment_lock_version, :if => :locking_enabled?
15
+ else
16
+ properties.delete(:_lock_version)
17
+ end
18
+ end
19
+ end
20
+
21
+ def locking_enabled?
22
+ self.class.locking_enabled?
23
+ end
24
+
25
+ private
26
+ def increment_lock_version
27
+ self._lock_version += 1
28
+ end
29
+
30
+ def save_to_collection
31
+ if locking_enabled? && _lock_version > 1
32
+ begin
33
+ collection.update({ '_id' => id, '_lock_version' => _lock_version-1 }, to_mongo)
34
+ success = database.last_status['updatedExisting']
35
+
36
+ self._lock_version -= 1 unless success
37
+
38
+ success
39
+ rescue Mongo::OperationFailure => e
40
+ false
41
+ end
42
+ else
43
+ super
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,143 @@
1
+ module MongoModel
2
+ module DocumentExtensions
3
+ module Persistence
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ undef_method :id if method_defined?(:id)
8
+ property :id, String, :as => '_id', :default => lambda { ::Mongo::ObjectID.new.to_s }
9
+
10
+ class_inheritable_writer :collection_name
11
+ end
12
+
13
+ def save
14
+ create_or_update
15
+ end
16
+
17
+ def save!
18
+ create_or_update || raise(DocumentNotSaved)
19
+ end
20
+
21
+ def delete
22
+ self.class.delete(id)
23
+ set_destroyed(true)
24
+ freeze
25
+ end
26
+
27
+ def destroy
28
+ delete
29
+ end
30
+
31
+ # Updates all the attributes from the passed-in Hash and saves the document.
32
+ # If the object is invalid, the saving will fail and false will be returned.
33
+ def update_attributes(attributes)
34
+ self.attributes = attributes
35
+ save
36
+ end
37
+
38
+ # Updates a single attribute and saves the document without going through the normal validation procedure.
39
+ # This is especially useful for boolean flags on existing documents.
40
+ def update_attribute(name, value)
41
+ send("#{name}=", value)
42
+ save(false)
43
+ end
44
+
45
+ def collection
46
+ self.class.collection
47
+ end
48
+
49
+ def database
50
+ self.class.database
51
+ end
52
+
53
+ module ClassMethods
54
+ def create(attributes={}, &block)
55
+ if attributes.is_a?(Array)
56
+ attributes.map { |attrs| create(attrs, &block) }
57
+ else
58
+ instance = new(attributes, &block)
59
+ instance.save
60
+ instance
61
+ end
62
+ end
63
+
64
+ def delete(id_or_conditions)
65
+ collection.remove(MongoOptions.new(self, :conditions => id_to_conditions(id_or_conditions)).selector)
66
+ end
67
+
68
+ def destroy(id_or_conditions)
69
+ find(:all, :conditions => id_to_conditions(id_or_conditions)).each { |instance| instance.destroy }
70
+ end
71
+
72
+ def from_mongo(document)
73
+ instance = super
74
+ instance.send(:instantiate, document)
75
+ instance
76
+ end
77
+
78
+ def collection_name
79
+ if superclass.abstract_class?
80
+ read_inheritable_attribute(:collection_name) || name.tableize.gsub(/\//, '.')
81
+ else
82
+ superclass.collection_name
83
+ end
84
+ end
85
+
86
+ def collection
87
+ @_collection ||= database.collection(collection_name)
88
+ end
89
+
90
+ def database
91
+ MongoModel.database
92
+ end
93
+
94
+ def save_safely?
95
+ @save_safely
96
+ end
97
+
98
+ def save_safely=(val)
99
+ @save_safely = val
100
+ end
101
+
102
+ private
103
+ def id_to_conditions(id_or_conditions)
104
+ case id_or_conditions
105
+ when String
106
+ { :id => id_or_conditions }
107
+ when Array
108
+ { :id.in => id_or_conditions }
109
+ else
110
+ id_or_conditions
111
+ end
112
+ end
113
+ end
114
+
115
+ private
116
+ def create_or_update
117
+ result = new_record? ? create : update
118
+ result != false
119
+ end
120
+
121
+ def create
122
+ save_to_collection
123
+ end
124
+
125
+ def update
126
+ save_to_collection
127
+ end
128
+
129
+ def save_to_collection
130
+ collection.save(to_mongo, :safe => self.class.save_safely?)
131
+ set_new_record(false)
132
+ true
133
+ rescue Mongo::OperationFailure => e
134
+ false
135
+ end
136
+
137
+ def instantiate(document)
138
+ attributes.from_mongo!(document)
139
+ set_new_record(false)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,161 @@
1
+ require 'active_support/core_ext/module/aliasing'
2
+ require 'active_support/core_ext/module/delegation'
3
+ require 'active_support/core_ext/object/metaclass'
4
+
5
+ module MongoModel
6
+ module DocumentExtensions
7
+ module Scopes
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ class << self
12
+ [:find, :count].each do |method|
13
+ alias_method_chain method, :scope
14
+ end
15
+ end
16
+
17
+ named_scope :scoped, lambda { |scope| scope }
18
+ end
19
+
20
+ module ClassMethods
21
+ protected
22
+ #
23
+ def named_scope(name, options={})
24
+ named_scopes[name] = options
25
+
26
+ metaclass.instance_eval do
27
+ define_method(name) do |*args|
28
+ scope(name).apply(*args)
29
+ end
30
+ end
31
+ end
32
+
33
+ #
34
+ def default_scope(options={})
35
+ if options.empty?
36
+ read_inheritable_attribute(:default_scope) || write_inheritable_attribute(:default_scope, Scope.new(self))
37
+ else
38
+ write_inheritable_attribute(:default_scope, default_scope.merge(:find => options))
39
+ end
40
+ end
41
+
42
+ #
43
+ def with_scope(options={}, &block)
44
+ push_scope(current_scope.merge(options), &block)
45
+ end
46
+
47
+ #
48
+ def with_exclusive_scope(options={}, &block)
49
+ push_scope(Scope.new(self, options), &block)
50
+ end
51
+
52
+ private
53
+ def push_scope(scope, &block)
54
+ scopes << scope
55
+ yield
56
+ ensure
57
+ scopes.pop
58
+ end
59
+
60
+ def find_with_scope(*args)
61
+ options = args.extract_options!
62
+ options = current_scope.options_for(:find).deep_merge(options)
63
+
64
+ find_without_scope(*(args << options))
65
+ end
66
+
67
+ def count_with_scope(conditions={})
68
+ scope_conditions = current_scope.options_for(:find)[:conditions] || {}
69
+
70
+ count_without_scope(scope_conditions.deep_merge(conditions))
71
+ end
72
+
73
+ def named_scopes
74
+ read_inheritable_attribute(:named_scopes) || write_inheritable_attribute(:named_scopes, {})
75
+ end
76
+
77
+ def scope(name)
78
+ Scope.new(self, named_scopes[name]) if named_scopes[name]
79
+ end
80
+
81
+ def scopes
82
+ Thread.current[:"#{self}_scopes"] ||= [ default_scope.dup ]
83
+ end
84
+
85
+ def current_scope
86
+ scopes.last
87
+ end
88
+ end
89
+ end
90
+
91
+ class Scope
92
+ attr_reader :model, :options
93
+
94
+ delegate :with_scope, :with_exclusive_scope, :scope, :to => :model
95
+ delegate :inspect, :to => :proxy_found
96
+
97
+ def initialize(model, options={})
98
+ if options.is_a?(Proc) || options.has_key?(:find) || options.has_key?(:create)
99
+ @options = options
100
+ else
101
+ @exclusive = options.delete(:exclusive)
102
+ @options = { :find => options }
103
+ end
104
+
105
+ @model = model
106
+ end
107
+
108
+ def merge(scope)
109
+ raise ArgumentError, "Scope must be applied before it can be merged" unless options.is_a?(Hash)
110
+ options_to_merge = scope.is_a?(self.class) ? scope.options : scope
111
+ self.class.new(model, options.deep_merge(options_to_merge))
112
+ end
113
+
114
+ def merge!(scope)
115
+ raise ArgumentError, "Scope must be applied before it can be merged" unless options.is_a?(Hash)
116
+ options_to_merge = scope.is_a?(self.class) ? scope.options : scope
117
+ options.deep_merge!(options_to_merge)
118
+ self
119
+ end
120
+
121
+ def reload
122
+ @found = nil
123
+ self
124
+ end
125
+
126
+ def apply(*args)
127
+ if options.is_a?(Hash)
128
+ reload
129
+ else
130
+ self.class.new(model, options.call(*args))
131
+ end
132
+ end
133
+
134
+ def options_for(action=:find)
135
+ options[action] || {}
136
+ end
137
+
138
+ def exclusive?
139
+ @exclusive == true
140
+ end
141
+
142
+ protected
143
+ def proxy_found
144
+ @found ||= find(:all)
145
+ end
146
+
147
+ private
148
+ def method_missing(method, *args, &block)
149
+ if scope = scope(method)
150
+ scope.exclusive? ? scope : merge(scope.apply(*args))
151
+ else
152
+ send(scope_type, options) { model.send(method, *args, &block) }
153
+ end
154
+ end
155
+
156
+ def scope_type
157
+ exclusive? ? :with_exclusive_scope : :with_scope
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,68 @@
1
+ module MongoModel
2
+ module DocumentExtensions
3
+ module Validations
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ alias_method_chain :save, :validation
8
+ alias_method_chain :save!, :validation
9
+ end
10
+
11
+ module ClassMethods
12
+ def property(name, *args, &block) #:nodoc:
13
+ property = super
14
+
15
+ validates_uniqueness_of(name) if property.options[:unique]
16
+
17
+ property
18
+ end
19
+
20
+ # Creates an object just like Document.create but calls save! instead of save
21
+ # so an exception is raised if the document is invalid.
22
+ def create!(attributes={}, &block)
23
+ if attributes.is_a?(Array)
24
+ attributes.map { |attrs| create!(attrs, &block) }
25
+ else
26
+ object = new(attributes, &block)
27
+ object.save!
28
+ object
29
+ end
30
+ end
31
+ end
32
+
33
+ # The validation process on save can be skipped by passing false. The regular Document#save method is
34
+ # replaced with this when the validations module is mixed in, which it is by default.
35
+ def save_with_validation(perform_validation = true)
36
+ if perform_validation && valid? || !perform_validation
37
+ begin
38
+ save_without_validation
39
+ rescue DocumentNotSaved
40
+ valid?
41
+ false
42
+ end
43
+ else
44
+ false
45
+ end
46
+ end
47
+
48
+ # Attempts to save the document just like Document#save but will raise a DocumentInvalid exception
49
+ # instead of returning false if the document is not valid.
50
+ def save_with_validation!
51
+ if valid?
52
+ begin
53
+ save_without_validation!
54
+ rescue DocumentNotSaved => e
55
+ raise valid? ? e : DocumentInvalid.new(self)
56
+ end
57
+ else
58
+ raise DocumentInvalid.new(self)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ Dir[File.dirname(__FILE__) + "/validations/*.rb"].sort.each do |path|
66
+ filename = File.basename(path)
67
+ require "mongomodel/document/validations/#{filename}"
68
+ end