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,15 @@
1
+ module MongoModel
2
+ module Logging
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def logger
7
+ MongoModel.logger
8
+ end
9
+ end
10
+
11
+ def logger
12
+ self.class.logger
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,29 @@
1
+ module MongoModel
2
+ module PrettyInspect
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # Returns a string like 'Post(title:String, body:String)'
7
+ def inspect
8
+ if [Document, EmbeddedDocument].include?(self)
9
+ super
10
+ else
11
+ attr_list = model_properties.map { |name, property| "#{name}: #{property.type.inspect}" } * ', '
12
+ "#{super}(#{attr_list})"
13
+ end
14
+ end
15
+ end
16
+
17
+ # Returns the contents of the document as a nicely formatted string.
18
+ def inspect
19
+ "#<#{self.class.name} #{attributes_for_inspect}>"
20
+ end
21
+
22
+ private
23
+ def attributes_for_inspect
24
+ attrs = self.class.model_properties.map { |name, property| "#{name}: #{send(name).inspect}" }
25
+ attrs.unshift "id: #{id}" if self.class.properties.include?(:id)
26
+ attrs * ', '
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,69 @@
1
+ require 'active_support/core_ext/class/inheritable_attributes'
2
+ require 'active_support/core_ext/module/delegation'
3
+ require 'active_support/core_ext/array/extract_options'
4
+ require 'active_support/core_ext/hash/except'
5
+
6
+ module MongoModel
7
+ module Properties
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ class_inheritable_accessor :properties
12
+ self.properties = ActiveSupport::OrderedHash.new
13
+ end
14
+
15
+ module ClassMethods
16
+ def property(name, type, options={})
17
+ properties[name.to_sym] = Property.new(name, type, options)
18
+ end
19
+
20
+ def model_properties
21
+ properties.reject { |k, p| p.internal? }
22
+ end
23
+ end
24
+
25
+ class Property
26
+ delegate :cast, :boolean, :to_mongo, :from_mongo, :to => :type_converter
27
+
28
+ attr_reader :name, :type, :options
29
+
30
+ def initialize(name, type, options={})
31
+ @name, @type, @options = name.to_sym, type, options
32
+ end
33
+
34
+ def as
35
+ options[:as] || name.to_s
36
+ end
37
+
38
+ def default(instance)
39
+ default = options[:default]
40
+
41
+ if default.respond_to?(:call)
42
+ case default.arity
43
+ when 0 then default.call
44
+ else default.call(instance)
45
+ end
46
+ else
47
+ default.duplicable? ? default.dup : default
48
+ end
49
+ end
50
+
51
+ def ==(other)
52
+ other.is_a?(self.class) && name == other.name && type == other.type && options == other.options
53
+ end
54
+
55
+ def embeddable?
56
+ type.ancestors.include?(EmbeddedDocument)
57
+ end
58
+
59
+ def internal?
60
+ as =~ /^_/ || options[:internal]
61
+ end
62
+
63
+ private
64
+ def type_converter
65
+ @type_converter ||= Types.converter_for(type)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,42 @@
1
+ require 'active_support/core_ext/module/aliasing'
2
+
3
+ module MongoModel
4
+ module RecordStatus
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ alias_method_chain :initialize, :record_status
9
+ end
10
+
11
+ def new_record?
12
+ @_new_record
13
+ end
14
+
15
+ def destroyed?
16
+ @_destroyed
17
+ end
18
+
19
+ def initialize_with_record_status(*args, &block)
20
+ set_new_record(true)
21
+ set_destroyed(false)
22
+
23
+ initialize_without_record_status(*args, &block)
24
+ end
25
+
26
+ protected
27
+ def set_new_record(value)
28
+ set_record_status(:new_record, value)
29
+ end
30
+
31
+ def set_destroyed(value)
32
+ set_record_status(:destroyed, value)
33
+ end
34
+
35
+ private
36
+ def set_record_status(type, value)
37
+ instance_variable_set("@_#{type}", value)
38
+ embedded_documents.each { |doc| doc.send(:set_record_status, type, value) }
39
+ value
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,32 @@
1
+ module MongoModel
2
+ # MongoModel automatically timestamps create and update operations if the document has properties
3
+ # named created_at/created_on or updated_at/updated_on.
4
+ module Timestamps #:nodoc:
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_save :set_update_timestamps
9
+ before_create :set_create_timestamps
10
+ end
11
+
12
+ module ClassMethods
13
+ # Defines timestamp properties created_at and updated_at.
14
+ # When the document is created or updated, these properties will be respectively updated.
15
+ def timestamps!
16
+ property :created_at, Time
17
+ property :updated_at, Time
18
+ end
19
+ end
20
+
21
+ private
22
+ def set_update_timestamps
23
+ write_attribute(:updated_at, Time.now) if properties.include?(:updated_at)
24
+ write_attribute(:updated_on, Time.now) if properties.include?(:updated_on)
25
+ end
26
+
27
+ def set_create_timestamps
28
+ write_attribute(:created_at, Time.now) if properties.include?(:created_at) && !query_attribute(:created_at)
29
+ write_attribute(:created_on, Time.now) if properties.include?(:created_on) && !query_attribute(:created_on)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,38 @@
1
+ module MongoModel
2
+ module Validations
3
+ extend ActiveSupport::Concern
4
+
5
+ include ActiveModel::Validations
6
+
7
+ module ClassMethods
8
+ def property(name, *args, &block) #:nodoc:
9
+ property = super
10
+
11
+ validates_associated(name) if property.embeddable?
12
+ validates_presence_of(name) if property.options[:required]
13
+ validates_format_of(name, property.options[:format]) if property.options[:format]
14
+
15
+ property
16
+ end
17
+
18
+ # Set the i18n scope to overwrite ActiveModel.
19
+ def i18n_scope #:nodoc:
20
+ :mongomodel
21
+ end
22
+ end
23
+
24
+ def valid?
25
+ errors.clear
26
+
27
+ @_on_validate = new_record? ? :create : :update
28
+ run_callbacks(:validate)
29
+
30
+ errors.empty?
31
+ end
32
+ end
33
+ end
34
+
35
+ Dir[File.dirname(__FILE__) + "/validations/*.rb"].sort.each do |path|
36
+ filename = File.basename(path)
37
+ require "mongomodel/concerns/validations/#{filename}"
38
+ end
@@ -0,0 +1,46 @@
1
+ module MongoModel
2
+ module Validations
3
+ module ClassMethods
4
+ # Validates whether the associated object or objects are all valid themselves. Works with any kind of association.
5
+ #
6
+ # class Book < MongoModel::Document
7
+ # has_many :pages
8
+ # belongs_to :library
9
+ #
10
+ # validates_associated :pages, :library
11
+ # end
12
+ #
13
+ # Warning: If, after the above definition, you then wrote:
14
+ #
15
+ # class Page < MongoModel::Document
16
+ # belongs_to :book
17
+ #
18
+ # validates_associated :book
19
+ # end
20
+ #
21
+ # this would specify a circular dependency and cause infinite recursion.
22
+ #
23
+ # NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association
24
+ # is both present and guaranteed to be valid, you also need to use +validates_presence_of+.
25
+ #
26
+ # Configuration options:
27
+ # * <tt>:message</tt> - A custom error message (default is: "is invalid")
28
+ # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
29
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
30
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
31
+ # method, proc or string should return or evaluate to a true or false value.
32
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
33
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
34
+ # method, proc or string should return or evaluate to a true or false value.
35
+ def validates_associated(*attr_names)
36
+ configuration = attr_names.extract_options!
37
+
38
+ validates_each(attr_names, configuration) do |record, attr_name, value|
39
+ unless (value.is_a?(Array) ? value : [value]).collect { |r| r.nil? || r.valid? }.all?
40
+ record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ require 'active_support/core_ext/hash/reverse_merge'
2
+ require 'active_support/core_ext/hash/deep_merge'
3
+ require 'active_support/core_ext/string/inflections'
4
+
5
+ module MongoModel
6
+ class Document < EmbeddedDocument
7
+ include DocumentExtensions::Persistence
8
+ include DocumentExtensions::OptimisticLocking
9
+
10
+ extend DocumentExtensions::Finders
11
+ extend DocumentExtensions::DynamicFinders
12
+ include DocumentExtensions::Indexes
13
+
14
+ include DocumentExtensions::Scopes
15
+ include DocumentExtensions::Validations
16
+ include DocumentExtensions::Callbacks
17
+
18
+ self.abstract_class = true
19
+ end
20
+ end
@@ -0,0 +1,46 @@
1
+ module MongoModel
2
+ module DocumentExtensions
3
+ module Callbacks
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ [:instantiate, :create_or_update, :create, :update, :destroy].each do |method|
8
+ alias_method_chain method, :callbacks
9
+ end
10
+ end
11
+
12
+ def instantiate_with_callbacks(*args) #:nodoc:
13
+ instantiate_without_callbacks(*args)
14
+ run_callbacks_with_embedded(:find)
15
+ end
16
+ private :instantiate_with_callbacks
17
+
18
+ def create_or_update_with_callbacks #:nodoc:
19
+ run_callbacks_with_embedded(:save) do
20
+ create_or_update_without_callbacks
21
+ end
22
+ end
23
+ private :create_or_update_with_callbacks
24
+
25
+ def create_with_callbacks #:nodoc:
26
+ run_callbacks_with_embedded(:create) do
27
+ create_without_callbacks
28
+ end
29
+ end
30
+ private :create_with_callbacks
31
+
32
+ def update_with_callbacks(*args) #:nodoc:
33
+ run_callbacks_with_embedded(:update) do
34
+ update_without_callbacks(*args)
35
+ end
36
+ end
37
+ private :update_with_callbacks
38
+
39
+ def destroy_with_callbacks #:nodoc:
40
+ run_callbacks_with_embedded(:destroy) do
41
+ destroy_without_callbacks
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,88 @@
1
+ module MongoModel
2
+ module DocumentExtensions
3
+ module DynamicFinders
4
+ def respond_to?(method_id, include_private = false)
5
+ if DynamicFinder.match(self, method_id)
6
+ true
7
+ else
8
+ super
9
+ end
10
+ end
11
+
12
+ def method_missing(method_id, *args, &block)
13
+ if finder = DynamicFinder.match(self, method_id)
14
+ finder.execute(*args)
15
+ else
16
+ super
17
+ end
18
+ end
19
+ end
20
+
21
+ class DynamicFinder
22
+ def initialize(model, attribute_names, finder=:first, bang=false)
23
+ @model, @attribute_names, @finder, @bang = model, attribute_names, finder, bang
24
+ end
25
+
26
+ def execute(*args)
27
+ options = args.extract_options!
28
+ conditions = build_conditions(args)
29
+
30
+ result = @model.send(instantiator? ? :first : @finder, options.deep_merge(:conditions => conditions))
31
+
32
+ if result.nil?
33
+ if bang?
34
+ raise DocumentNotFound, "Couldn't find #{@model.to_s} with #{conditions.inspect}"
35
+ elsif instantiator?
36
+ return @model.send(@finder, conditions)
37
+ end
38
+ end
39
+
40
+ result
41
+ end
42
+
43
+ def bang?
44
+ @bang
45
+ end
46
+
47
+ def instantiator?
48
+ @finder == :new || @finder == :create
49
+ end
50
+
51
+ def self.match(model, method)
52
+ finder = :first
53
+ bang = false
54
+
55
+ case method.to_s
56
+ when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
57
+ finder = :last if $1 == 'last_by'
58
+ finder = :all if $1 == 'all_by'
59
+ names = $2
60
+ when /^find_by_([_a-zA-Z]\w*)\!$/
61
+ bang = true
62
+ names = $1
63
+ when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
64
+ finder = ($1 == 'initialize' ? :new : :create)
65
+ names = $2
66
+ else
67
+ return nil
68
+ end
69
+
70
+ names = names.split('_and_')
71
+ if names.all? { |n| model.properties.include?(n.to_sym) }
72
+ new(model, names, finder, bang)
73
+ end
74
+ end
75
+
76
+ private
77
+ def build_conditions(args)
78
+ result = {}
79
+
80
+ @attribute_names.zip(args) do |attribute, value|
81
+ result[attribute.to_sym] = value
82
+ end
83
+
84
+ result
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,82 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+
3
+ module MongoModel
4
+ module DocumentExtensions
5
+ module Finders
6
+ def find(*args)
7
+ options = args.extract_options!
8
+
9
+ case args.first
10
+ when :first then find_first(options)
11
+ when :last then find_last(options)
12
+ when :all then find_all(options)
13
+ else find_by_ids(args, options)
14
+ end
15
+ end
16
+
17
+ def first(options={})
18
+ find(:first, options)
19
+ end
20
+
21
+ def last(options={})
22
+ find(:last, options)
23
+ end
24
+
25
+ def all(options={})
26
+ find(:all, options)
27
+ end
28
+
29
+ def count(conditions={})
30
+ _find(:conditions => conditions).count
31
+ end
32
+
33
+ def exists?(id_or_conditions)
34
+ case id_or_conditions
35
+ when String
36
+ exists?(:id => id_or_conditions)
37
+ else
38
+ count(id_or_conditions) > 0
39
+ end
40
+ end
41
+
42
+ private
43
+ def find_first(options={})
44
+ _find_and_instantiate(options.merge(:limit => 1)).first
45
+ end
46
+
47
+ def find_last(options={})
48
+ order = MongoOrder.parse(options[:order]) || :id.asc
49
+ _find_and_instantiate(options.merge(:order => order.reverse, :limit => 1)).first
50
+ end
51
+
52
+ def find_all(options={})
53
+ _find_and_instantiate(options)
54
+ end
55
+
56
+ def find_by_ids(ids, options={})
57
+ ids.flatten!
58
+
59
+ case ids.size
60
+ when 0
61
+ raise ArgumentError, "At least one id must be specified"
62
+ when 1
63
+ id = ids.first.to_s
64
+ _find_and_instantiate(options.deep_merge(:conditions => { :id => id })).first || raise(DocumentNotFound, "Couldn't find document with id: #{id}")
65
+ else
66
+ docs = _find_and_instantiate(options.deep_merge(:conditions => { :id.in => ids.map { |id| id.to_s } }))
67
+ raise DocumentNotFound if docs.size != ids.size
68
+ docs.sort_by { |doc| ids.index(doc.id) }
69
+ end
70
+ end
71
+
72
+ def _find(options={})
73
+ selector, options = MongoOptions.new(self, options).to_a
74
+ collection.find(selector, options)
75
+ end
76
+
77
+ def _find_and_instantiate(options={})
78
+ _find(options).to_a.map { |doc| from_mongo(doc) }
79
+ end
80
+ end
81
+ end
82
+ end