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,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