mongo_mapper-unstable 2009.10.16 → 2009.10.31

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/.gitignore +3 -1
  2. data/README.rdoc +3 -0
  3. data/Rakefile +31 -65
  4. data/VERSION +1 -1
  5. data/lib/mongo_mapper/associations/base.rb +31 -4
  6. data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +0 -2
  7. data/lib/mongo_mapper/associations/many_documents_proxy.rb +21 -15
  8. data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +2 -2
  9. data/lib/mongo_mapper/associations/many_embedded_proxy.rb +21 -36
  10. data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +1 -1
  11. data/lib/mongo_mapper/associations/proxy.rb +1 -0
  12. data/lib/mongo_mapper/associations.rb +114 -17
  13. data/lib/mongo_mapper/callbacks.rb +18 -0
  14. data/lib/mongo_mapper/document.rb +230 -95
  15. data/lib/mongo_mapper/dynamic_finder.rb +1 -1
  16. data/lib/mongo_mapper/embedded_document.rb +7 -3
  17. data/lib/mongo_mapper/finder_options.rb +88 -56
  18. data/lib/mongo_mapper/pagination.rb +2 -0
  19. data/lib/mongo_mapper/serialization.rb +2 -3
  20. data/lib/mongo_mapper/serializers/json_serializer.rb +1 -1
  21. data/lib/mongo_mapper/support.rb +9 -0
  22. data/lib/mongo_mapper/validations.rb +14 -42
  23. data/lib/mongo_mapper.rb +15 -13
  24. data/mongo_mapper.gemspec +13 -13
  25. data/specs.watchr +2 -2
  26. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +5 -5
  27. data/test/functional/associations/test_belongs_to_proxy.rb +28 -30
  28. data/test/functional/associations/test_many_documents_as_proxy.rb +4 -4
  29. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +27 -3
  30. data/test/functional/associations/test_many_embedded_proxy.rb +58 -38
  31. data/test/functional/associations/test_many_polymorphic_proxy.rb +49 -7
  32. data/test/functional/associations/test_many_proxy.rb +65 -15
  33. data/test/functional/test_associations.rb +3 -3
  34. data/test/functional/test_binary.rb +1 -1
  35. data/test/functional/test_callbacks.rb +1 -1
  36. data/test/functional/test_dirty.rb +3 -3
  37. data/test/functional/test_document.rb +178 -57
  38. data/test/functional/test_embedded_document.rb +1 -1
  39. data/test/functional/test_pagination.rb +18 -18
  40. data/test/functional/test_rails_compatibility.rb +1 -1
  41. data/test/functional/test_validations.rb +80 -27
  42. data/test/models.rb +93 -17
  43. data/test/support/{test_timing.rb → timing.rb} +1 -1
  44. data/test/test_helper.rb +8 -11
  45. data/test/unit/test_association_base.rb +23 -1
  46. data/test/unit/test_document.rb +29 -12
  47. data/test/unit/test_embedded_document.rb +13 -4
  48. data/test/unit/test_finder_options.rb +74 -58
  49. data/test/unit/test_mongomapper.rb +2 -2
  50. data/test/unit/test_pagination.rb +4 -0
  51. metadata +7 -7
data/.gitignore CHANGED
@@ -5,4 +5,6 @@ rdoc
5
5
  pkg
6
6
  *~
7
7
  *.gem
8
- tmp
8
+ tmp
9
+ .yardoc
10
+ doc/*
data/README.rdoc CHANGED
@@ -35,7 +35,10 @@ You can also just declare the source:
35
35
 
36
36
  == Documentation
37
37
 
38
+ Documentation is lacking right now because if you can't look through the code right now and feel comfortable, this is probably too young for you to use. Wait til it stabilizes a bit more.
39
+
38
40
  http://rdoc.info/projects/jnunemaker/mongomapper
41
+ http://groups.google.com/group/mongomapper
39
42
 
40
43
  == More Info
41
44
 
data/Rakefile CHANGED
@@ -1,89 +1,55 @@
1
1
  require 'rubygems'
2
2
  require 'rake'
3
-
4
- begin
5
- require 'jeweler'
6
- Jeweler::Tasks.new do |gem|
7
- gem.name = "mongo_mapper"
8
- gem.summary = %Q{Awesome gem for modeling your domain and storing it in mongo}
9
- gem.email = "nunemaker@gmail.com"
10
- gem.homepage = "http://github.com/jnunemaker/mongomapper"
11
- gem.authors = ["John Nunemaker"]
12
- gem.rubyforge_project = "mongomapper"
13
-
14
- gem.add_dependency('activesupport', '>= 2.3')
15
- gem.add_dependency('mongo', '0.15.1')
16
- gem.add_dependency('jnunemaker-validatable', '1.7.4')
17
-
18
- gem.add_development_dependency('jnunemaker-matchy', '0.4.0')
19
- gem.add_development_dependency('shoulda', '2.10.2')
20
- gem.add_development_dependency('timecop', '0.3.1')
21
- gem.add_development_dependency('mocha', '0.9.4')
22
- end
3
+ require 'jeweler'
4
+ require 'yard'
5
+ require 'yard/rake/yardoc_task'
6
+
7
+ Jeweler::Tasks.new do |gem|
8
+ gem.name = "mongo_mapper"
9
+ gem.summary = %Q{Awesome gem for modeling your domain and storing it in mongo}
10
+ gem.email = "nunemaker@gmail.com"
11
+ gem.homepage = "http://github.com/jnunemaker/mongomapper"
12
+ gem.authors = ["John Nunemaker"]
23
13
 
24
- Jeweler::RubyforgeTasks.new do |rubyforge|
25
- rubyforge.doc_task = "rdoc"
26
- end
27
- rescue LoadError
28
- puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
14
+ gem.add_dependency('activesupport', '>= 2.3')
15
+ gem.add_dependency('mongo', '0.16')
16
+ gem.add_dependency('jnunemaker-validatable', '1.8.1')
17
+
18
+ gem.add_development_dependency('jnunemaker-matchy', '0.4.0')
19
+ gem.add_development_dependency('shoulda', '2.10.2')
20
+ gem.add_development_dependency('timecop', '0.3.1')
21
+ gem.add_development_dependency('mocha', '0.9.4')
29
22
  end
30
23
 
24
+ Jeweler::GemcutterTasks.new
25
+
31
26
  require 'rake/testtask'
32
27
  Rake::TestTask.new(:test) do |test|
33
- test.libs << 'lib' << 'test'
28
+ test.libs << 'test'
29
+ test.ruby_opts << '-rubygems'
34
30
  test.pattern = 'test/**/test_*.rb'
35
31
  test.verbose = true
36
32
  end
37
33
 
38
34
  namespace :test do
39
35
  Rake::TestTask.new(:units) do |test|
40
- test.libs << 'lib' << 'test'
36
+ test.libs << 'test'
37
+ test.ruby_opts << '-rubygems'
41
38
  test.pattern = 'test/unit/**/test_*.rb'
42
39
  test.verbose = true
43
40
  end
44
41
 
45
42
  Rake::TestTask.new(:functionals) do |test|
46
- test.libs << 'lib' << 'test'
47
- test.pattern = 'test/functional/**/test_*.rb'
48
- test.verbose = true
49
- end
50
- end
51
-
52
- begin
53
- require 'rcov/rcovtask'
54
- Rcov::RcovTask.new do |test|
55
43
  test.libs << 'test'
56
- test.pattern = 'test/**/test_*.rb'
44
+ test.ruby_opts << '-rubygems'
45
+ test.pattern = 'test/functional/**/test_*.rb'
57
46
  test.verbose = true
58
47
  end
59
- rescue LoadError
60
- task :rcov do
61
- abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
62
- end
63
48
  end
64
49
 
65
- begin
66
- require 'cucumber/rake/task'
67
- Cucumber::Rake::Task.new(:features)
68
- rescue LoadError
69
- task :features do
70
- abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
71
- end
72
- end
50
+ task :default => :test
51
+ task :test => :check_dependencies
73
52
 
74
- task :default => :test
75
-
76
- require 'rake/rdoctask'
77
- Rake::RDocTask.new do |rdoc|
78
- if File.exist?('VERSION.yml')
79
- config = YAML.load(File.read('VERSION.yml'))
80
- version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
81
- else
82
- version = ""
83
- end
84
-
85
- rdoc.rdoc_dir = 'rdoc'
86
- rdoc.title = "MongoMapper #{version}"
87
- rdoc.rdoc_files.include('README*')
88
- rdoc.rdoc_files.include('lib/**/*.rb')
89
- end
53
+ YARD::Rake::YardocTask.new(:doc) do |t|
54
+ t.options = ["--legacy"]
55
+ end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2009.10.16
1
+ 2009.10.31
@@ -1,10 +1,27 @@
1
1
  module MongoMapper
2
2
  module Associations
3
+ # Base class for keeping track of associations.
4
+ #
5
+ # @private
3
6
  class Base
4
- attr_reader :type, :name, :options
7
+ attr_reader :type, :name, :options, :finder_options
5
8
 
6
- def initialize(type, name, options = {})
7
- @type, @name, @options = type, name, options
9
+ # Options that should not be considered MongoDB query options/criteria
10
+ AssociationOptions = [:as, :class, :class_name, :dependent, :extend, :foreign_key, :polymorphic]
11
+
12
+ def initialize(type, name, options={}, &extension)
13
+ @type, @name, @options, @finder_options = type, name, {}, {}
14
+ options.symbolize_keys!
15
+
16
+ options[:extend] = modulized_extensions(extension, options[:extend])
17
+
18
+ options.each_pair do |key, value|
19
+ if AssociationOptions.include?(key)
20
+ @options[key] = value
21
+ else
22
+ @finder_options[key] = value
23
+ end
24
+ end
8
25
  end
9
26
 
10
27
  def class_name
@@ -20,7 +37,7 @@ module MongoMapper
20
37
  end
21
38
 
22
39
  def klass
23
- @klass ||= class_name.constantize
40
+ @klass ||= options[:class] ? options[:class] : class_name.constantize
24
41
  end
25
42
 
26
43
  def many?
@@ -78,6 +95,16 @@ module MongoMapper
78
95
  end
79
96
  end # end begin
80
97
  end # end proxy_class
98
+
99
+ private
100
+
101
+ # @param [Array<Module, Proc>] extensions a collection of Modules or
102
+ # Procs that extend the behaviour of this association.
103
+ def modulized_extensions(*extensions)
104
+ extensions.flatten.compact.map do |extension|
105
+ Proc === extension ? Module.new(&extension) : extension
106
+ end
107
+ end
81
108
  end
82
109
  end
83
110
  end
@@ -8,10 +8,8 @@ module MongoMapper
8
8
 
9
9
  def apply_scope(doc)
10
10
  ensure_owner_saved
11
-
12
11
  doc.send("#{as_type_name}=", @owner.class.name)
13
12
  doc.send("#{as_id_name}=", @owner.id)
14
-
15
13
  doc
16
14
  end
17
15
 
@@ -4,7 +4,7 @@ module MongoMapper
4
4
  delegate :klass, :to => :@association
5
5
  delegate :collection, :to => :klass
6
6
 
7
- include MongoMapper::Finders
7
+ include ::MongoMapper::Finders
8
8
 
9
9
  def find(*args)
10
10
  options = args.extract_options!
@@ -16,19 +16,19 @@ module MongoMapper
16
16
  end
17
17
 
18
18
  def all(options={})
19
- find(:all, scoped_options(options))
19
+ klass.all(scoped_options(options))
20
20
  end
21
21
 
22
22
  def first(options={})
23
- find(:first, scoped_options(options))
23
+ klass.first(scoped_options(options))
24
24
  end
25
25
 
26
26
  def last(options={})
27
- find(:last, scoped_options(options))
27
+ klass.last(scoped_options(options))
28
28
  end
29
29
 
30
- def count(conditions={})
31
- klass.count(conditions.deep_merge(scoped_conditions))
30
+ def count(options={})
31
+ klass.count(scoped_options(options))
32
32
  end
33
33
 
34
34
  def replace(docs)
@@ -56,21 +56,27 @@ module MongoMapper
56
56
  apply_scope(doc).save
57
57
  doc
58
58
  end
59
+
60
+ def create!(attrs={})
61
+ doc = klass.new(attrs)
62
+ apply_scope(doc).save!
63
+ doc
64
+ end
59
65
 
60
- def destroy_all(conditions={})
61
- all(:conditions => conditions).map(&:destroy)
66
+ def destroy_all(options={})
67
+ all(options).map(&:destroy)
62
68
  reset
63
69
  end
64
70
 
65
- def delete_all(conditions={})
66
- klass.delete_all(conditions.deep_merge(scoped_conditions))
71
+ def delete_all(options={})
72
+ klass.delete_all(options.merge(scoped_conditions))
67
73
  reset
68
74
  end
69
75
 
70
76
  def nullify
71
- criteria = FinderOptions.to_mongo_criteria(scoped_conditions)
77
+ criteria = FinderOptions.new(klass, scoped_conditions).criteria
72
78
  all(criteria).each do |doc|
73
- doc.update_attributes self.foreign_key => nil
79
+ doc.update_attributes(self.foreign_key => nil)
74
80
  end
75
81
  reset
76
82
  end
@@ -89,13 +95,13 @@ module MongoMapper
89
95
  def scoped_conditions
90
96
  {self.foreign_key => @owner.id}
91
97
  end
92
-
98
+
93
99
  def scoped_options(options)
94
- options.deep_merge({:conditions => scoped_conditions})
100
+ @association.finder_options.merge(options).merge(scoped_conditions)
95
101
  end
96
102
 
97
103
  def find_target
98
- find(:all)
104
+ all
99
105
  end
100
106
 
101
107
  def ensure_owner_saved
@@ -1,6 +1,6 @@
1
1
  module MongoMapper
2
2
  module Associations
3
- class ManyEmbeddedPolymorphicProxy < Proxy
3
+ class ManyEmbeddedPolymorphicProxy < Proxy
4
4
  def replace(v)
5
5
  @_values = v.map do |doc_or_hash|
6
6
  if doc_or_hash.kind_of?(EmbeddedDocument)
@@ -30,4 +30,4 @@ module MongoMapper
30
30
  end
31
31
  end
32
32
  end
33
- end
33
+ end
@@ -5,34 +5,23 @@ module MongoMapper
5
5
  @_values = v.map { |e| e.kind_of?(EmbeddedDocument) ? e.attributes : e }
6
6
  reset
7
7
  end
8
-
9
- def build(opts={})
10
- owner = @owner
11
- child = @association.klass.new(opts)
12
- assign_parent_reference(child)
13
- child._root_document = owner
14
- self << child
15
- child
8
+
9
+ def build(attributes={})
10
+ doc = @association.klass.new(attributes)
11
+ assign_root_document(doc)
12
+ self << doc
13
+ doc
16
14
  end
17
-
18
- def find(opts)
19
- case opts
20
- when :all
21
- self
22
- when String
23
- if load_target
24
- child = @target.detect {|item| item.id == opts}
25
- assign_parent_reference(child)
26
- child
27
- end
28
- end
15
+
16
+ def find(id)
17
+ load_target
18
+ @target.detect { |item| item.id == id }
29
19
  end
30
20
 
31
21
  def <<(*docs)
32
22
  if load_target
33
- root = @owner._root_document || @owner
34
23
  docs.each do |doc|
35
- doc._root_document = root
24
+ assign_root_document(doc)
36
25
  @target << doc
37
26
  end
38
27
  end
@@ -40,28 +29,24 @@ module MongoMapper
40
29
  alias_method :push, :<<
41
30
  alias_method :concat, :<<
42
31
 
43
- protected
32
+ private
44
33
  def find_target
45
34
  (@_values || []).map do |e|
46
35
  child = @association.klass.new(e)
47
- assign_parent_reference(child)
36
+ assign_root_document(child)
48
37
  child
49
38
  end
50
39
  end
51
-
52
- private
53
-
54
- def assign_parent_reference(child)
55
- return unless child && @owner
56
- return if @owner.class.name.blank?
57
- owner = @owner
58
- child.class_eval do
59
- define_method(owner.class.name.underscore) do
60
- owner
61
- end
40
+
41
+ def root_document
42
+ @owner._root_document || @owner
43
+ end
44
+
45
+ def assign_root_document(*docs)
46
+ docs.each do |doc|
47
+ doc._root_document = root_document
62
48
  end
63
49
  end
64
-
65
50
  end
66
51
  end
67
52
  end
@@ -1,6 +1,6 @@
1
1
  module MongoMapper
2
2
  module Associations
3
- class ManyPolymorphicProxy < ManyDocumentsProxy
3
+ class ManyPolymorphicProxy < ManyDocumentsProxy
4
4
  private
5
5
  def apply_scope(doc)
6
6
  doc.send("#{@association.type_key_name}=", doc.class.name)
@@ -6,6 +6,7 @@ module MongoMapper
6
6
  def initialize(owner, association)
7
7
  @owner = owner
8
8
  @association = association
9
+ @association.options[:extend].each { |ext| proxy_extend(ext) }
9
10
  reset
10
11
  end
11
12
 
@@ -1,13 +1,121 @@
1
1
  module MongoMapper
2
2
  module Associations
3
3
  module ClassMethods
4
- def belongs_to(association_id, options = {})
5
- create_association(:belongs_to, association_id, options)
4
+ ##
5
+ # This macro allows you define a "belongs-to" relationship between one
6
+ # document and some other document.
7
+ #
8
+ # == Requirements
9
+ #
10
+ # Usage of this macro requires that your document define a key that can
11
+ # be used to store the ID of the target document that is the parent of
12
+ # this document.
13
+ #
14
+ # == Conventions
15
+ #
16
+ # The following is a list of the conventions used by MongoMapper in
17
+ # defining a belongs-to relationship. Each can likely be overridden via
18
+ # the +options+ parameter.
19
+ #
20
+ # * The name of your belongs-to association is the lowercase, singular
21
+ # name of the target class
22
+ # * A key with the name of your association exists with an "_id" suffix
23
+ # to store the ID of the target of this relationship
24
+ #
25
+ # @param [Symbol] association_id The name of this association
26
+ # @param [Hash] options Optional parameters that define the
27
+ # characteristics of this relationship. These are often used to
28
+ # override MongoMapper conventions.
29
+ # @option options [Boolean] :polymorphic (false) Set this option to
30
+ # <code>true</code> to define a relationship that can be between this
31
+ # document and any other type of document. Note that you *must* also
32
+ # have a key on your document to store the type of document in this
33
+ # relationship.
34
+ # @option options [String] :class_name If your relationship doesn't use
35
+ # the name of some class, you *must* use this option to indicate the
36
+ # target class for this relationship.
37
+ # @option options [Symbol] :foreign_key Use this option to specify a
38
+ # non-conventional key that stores the ID of the parent in this
39
+ # relationship
40
+ #
41
+ # @example Conventional, and simple, usage of <code>belongs_to</code>
42
+ # class Novel
43
+ # include MongoMapper::Document
44
+ #
45
+ # key :author_id, String # our "foreign key"
46
+ #
47
+ # belongs_to :author
48
+ # end
49
+ #
50
+ # @example Using :foreign_key and :class_name
51
+ # class Pet
52
+ # include MongoMapper::Document
53
+ #
54
+ # key :person_id, String
55
+ #
56
+ # belongs_to :owner,
57
+ # :foreign_key => :person_id,
58
+ # :class_name => "Person"
59
+ # end
60
+ #
61
+ # @example Defining a polymorphic belongs-to relationship
62
+ # class Vehicle
63
+ # include MongoMapper::Document
64
+ #
65
+ # key :owner_id, String
66
+ # key :owner_type, String
67
+ #
68
+ # belongs_to :owner,
69
+ # :polymorphic => true
70
+ # end
71
+ #
72
+ # @example Non-standard polymorphic belongs-to relationship
73
+ # class Vehicle
74
+ # include MongoMapper::Document
75
+ #
76
+ # key :person_id, String
77
+ # key :person_type, String
78
+ #
79
+ # belongs_to :owner,
80
+ # :polymorphic => true,
81
+ # :foreign_key => "person_id",
82
+ # :type_key_name => "person_type"
83
+ # end
84
+ def belongs_to(association_id, options={}, &extension)
85
+ create_association(:belongs_to, association_id, options, &extension)
6
86
  self
7
87
  end
8
88
 
9
- def many(association_id, options = {})
10
- create_association(:many, association_id, options)
89
+ ##
90
+ # This macro allows you to define a "has-many" relationship between a
91
+ # document, and numerous child documents.
92
+ #
93
+ # == Conventions
94
+ #
95
+ # The following is a list of the conventions used by MongoMapper in
96
+ # defining this relationship. Each can likely be overridden via the
97
+ # +options+ parameter.
98
+ #
99
+ # * The name of your association is the lowercase, *plural* name of the
100
+ # target class
101
+ # * Your target class must have a "foreign key" bearing the name of this
102
+ # class suffixed by "_id"
103
+ #
104
+ # @param [Symbol] association_id The name of this association
105
+ # @param [Hash] options Optional parameters that define the
106
+ # characteristics of this relationship. These are often used to
107
+ # override MongoMapper conventions.
108
+ # @option options [String] :class_name If your relationship doesn't use
109
+ # the name of some class, you *must* use this option to indicate the
110
+ # target class for this relationship.
111
+ # @option options [Symbol] :foreign_key Use this option to specify a
112
+ # non-conventional key that stores the ID of the parent in this
113
+ # relationship
114
+ # @option options [#to_s] :as Used when the target relationship is
115
+ # polymorphic (i.e. the +belongs_to+ has set <tt>:polymorphic</tt> to
116
+ # +true+). See examples for usage.
117
+ def many(association_id, options={}, &extension)
118
+ create_association(:many, association_id, options, &extension)
11
119
  self
12
120
  end
13
121
 
@@ -18,8 +126,8 @@ module MongoMapper
18
126
  end
19
127
 
20
128
  private
21
- def create_association(type, name, options)
22
- association = Associations::Base.new(type, name, options)
129
+ def create_association(type, name, options, &extension)
130
+ association = Associations::Base.new(type, name, options, &extension)
23
131
  associations[association.name] = association
24
132
  define_association_methods(association)
25
133
  define_dependent_callback(association)
@@ -41,8 +149,6 @@ module MongoMapper
41
149
  if association.options[:dependent]
42
150
  if association.many?
43
151
  define_dependent_callback_for_many(association)
44
- elsif association.belongs_to?
45
- define_dependent_callback_for_belongs_to(association)
46
152
  end
47
153
  end
48
154
  end
@@ -61,15 +167,6 @@ module MongoMapper
61
167
  end
62
168
  end
63
169
  end
64
-
65
- def define_dependent_callback_for_belongs_to(association)
66
- after_destroy do |doc|
67
- case association.options[:dependent]
68
- when :destroy
69
- doc.get_proxy(association).destroy
70
- end
71
- end
72
- end
73
170
  end
74
171
 
75
172
  module InstanceMethods
@@ -1,4 +1,17 @@
1
1
  module MongoMapper
2
+ # This module is mixed into the Document module to provide call-backs before
3
+ # and after the following events:
4
+ #
5
+ # * save
6
+ # * create
7
+ # * update
8
+ # * validation
9
+ # ** every validation
10
+ # ** validation when created
11
+ # ** validation when updated
12
+ # * destruction
13
+ #
14
+ # @see ActiveSupport::Callbacks
2
15
  module Callbacks
3
16
  def self.included(model) #:nodoc:
4
17
  model.class_eval do
@@ -42,6 +55,11 @@ module MongoMapper
42
55
  return result
43
56
  end
44
57
 
58
+ # Here we override the +destroy+ method to allow for the +before_destroy+
59
+ # and +after_destroy+ call-backs. Note that the +destroy+ call is aborted
60
+ # if the +before_destroy+ call-back returns +false+.
61
+ #
62
+ # @return the result of calling +destroy+ on the document
45
63
  def destroy #:nodoc:
46
64
  return false if callback(:before_destroy) == false
47
65
  result = super