mongodoc 0.2.1 → 0.2.2

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 (44) hide show
  1. data/README.textile +42 -12
  2. data/Rakefile +4 -4
  3. data/TODO +26 -0
  4. data/VERSION +1 -1
  5. data/examples/simple_document.rb +1 -1
  6. data/examples/simple_object.rb +0 -2
  7. data/features/mongodb.yml +6 -5
  8. data/features/removing_documents.feature +68 -0
  9. data/features/step_definitions/collection_steps.rb +3 -3
  10. data/features/step_definitions/document_steps.rb +2 -2
  11. data/features/step_definitions/removing_documents_steps.rb +14 -0
  12. data/features/support/support.rb +2 -2
  13. data/lib/mongodoc.rb +4 -7
  14. data/lib/mongodoc/associations/collection_proxy.rb +103 -0
  15. data/lib/mongodoc/associations/document_proxy.rb +53 -0
  16. data/lib/mongodoc/associations/hash_proxy.rb +96 -0
  17. data/lib/mongodoc/associations/proxy_base.rb +51 -0
  18. data/lib/mongodoc/attributes.rb +49 -17
  19. data/lib/mongodoc/collection.rb +15 -5
  20. data/lib/mongodoc/connection.rb +83 -20
  21. data/lib/mongodoc/criteria.rb +9 -4
  22. data/lib/mongodoc/cursor.rb +9 -3
  23. data/lib/mongodoc/document.rb +37 -24
  24. data/lib/mongodoc/validations/macros.rb +11 -0
  25. data/lib/mongodoc/validations/validates_embedded.rb +13 -0
  26. data/mongodb.example.yml +13 -5
  27. data/mongodoc.gemspec +33 -23
  28. data/spec/associations/collection_proxy_spec.rb +200 -0
  29. data/spec/associations/document_proxy_spec.rb +42 -0
  30. data/spec/associations/hash_proxy_spec.rb +163 -0
  31. data/spec/attributes_spec.rb +113 -47
  32. data/spec/bson_spec.rb +24 -24
  33. data/spec/collection_spec.rb +67 -86
  34. data/spec/connection_spec.rb +98 -150
  35. data/spec/criteria_spec.rb +4 -3
  36. data/spec/cursor_spec.rb +33 -27
  37. data/spec/document_spec.rb +173 -156
  38. data/spec/embedded_save_spec.rb +8 -3
  39. data/spec/new_record_spec.rb +33 -121
  40. metadata +80 -39
  41. data/lib/mongodoc/parent_proxy.rb +0 -44
  42. data/lib/mongodoc/proxy.rb +0 -83
  43. data/spec/parent_proxy_spec.rb +0 -44
  44. data/spec/proxy_spec.rb +0 -80
@@ -0,0 +1,96 @@
1
+ module MongoDoc
2
+ class InvalidEmbeddedHashKey < RuntimeError; end
3
+
4
+ module Associations
5
+ class HashProxy < ProxyBase
6
+ HASH_METHODS = (Hash.instance_methods - Object.instance_methods).map { |n| n.to_s }
7
+
8
+ MUST_DEFINE = %w[to_a inspect to_bson ==]
9
+
10
+ DO_NOT_DEFINE = %w[merge! replace store update]
11
+
12
+ (HASH_METHODS + MUST_DEFINE - DO_NOT_DEFINE).uniq.each do |method|
13
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
14
+ def #{method}(*args, &block) # def each(*args, &block)
15
+ hash.send(:#{method}, *args, &block) # hash.send(:each, *args, &block)
16
+ end # end
17
+ RUBY
18
+ end
19
+
20
+ attr_reader :hash
21
+
22
+ def _root=(root)
23
+ @_root = root
24
+ hash.each do |key, value|
25
+ value._root = root if is_document?(value)
26
+ end
27
+ end
28
+
29
+ def initialize(options)
30
+ super
31
+ @hash = {}
32
+ end
33
+
34
+ alias put []=
35
+ def []=(key, value)
36
+ raise InvalidEmbeddedHashKey.new("Key name [#{key}] must be a valid element name, see http://www.mongodb.org/display/DOCS/BSON#BSON-noteonelementname") unless valid_key?(key)
37
+ attach(value)
38
+ put(key, value)
39
+ end
40
+ alias store []=
41
+
42
+ def build(key, attrs)
43
+ item = assoc_class.new(attrs)
44
+ store(key, item)
45
+ end
46
+
47
+ # Lie about our class. Borrowed from Rake::FileList
48
+ # Note: Does not work for case equality (<tt>===</tt>)
49
+ def is_a?(klass)
50
+ klass == Hash || super(klass)
51
+ end
52
+ alias kind_of? is_a?
53
+
54
+ def merge!(other)
55
+ other.each_pair do |key, value|
56
+ self[key] = if block_given?
57
+ yield key, [key], value
58
+ else
59
+ value
60
+ end
61
+ end
62
+ end
63
+ alias update merge!
64
+
65
+ def replace(other)
66
+ clear
67
+ merge!(other)
68
+ end
69
+
70
+ def valid?
71
+ values.all? do |child|
72
+ if is_document?(child)
73
+ child.valid?
74
+ else
75
+ true
76
+ end
77
+ end
78
+ end
79
+
80
+ protected
81
+
82
+ def annotated_keys(src, attrs)
83
+ assoc_path = "#{assoc_name}.#{index(src)}"
84
+ annotated = {}
85
+ attrs.each do |(key, value)|
86
+ annotated["#{assoc_path}.#{key}"] = value
87
+ end
88
+ annotated
89
+ end
90
+
91
+ def valid_key?(key)
92
+ (String === key or Symbol === key) and key.to_s !~ /(_id|query|\$.*|.*\..*)/
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,51 @@
1
+ module MongoDoc
2
+ module Associations
3
+ class ProxyBase
4
+ undef_method :id, :to_bson
5
+
6
+ attr_reader :assoc_name, :assoc_class, :_parent, :_root
7
+
8
+ def _parent=(parent)
9
+ @_parent = parent
10
+ end
11
+
12
+ def _path_to_root(src, attrs)
13
+ _parent._path_to_root(src, annotated_keys(src, attrs))
14
+ end
15
+
16
+ def _root=(root)
17
+ @_root = root
18
+ end
19
+
20
+ def initialize(options)
21
+ @assoc_name = options[:assoc_name]
22
+ @assoc_class = options[:assoc_class]
23
+ @_root = options[:root]
24
+ @_parent = options[:parent]
25
+ end
26
+
27
+ def attach(item)
28
+ if is_document?(item)
29
+ item._parent = self
30
+ item._root = _root
31
+ _root.send(:register_save_observer, item)
32
+ end
33
+ item
34
+ end
35
+
36
+ protected
37
+
38
+ def annotated_keys(src, hash)
39
+ annotated = {}
40
+ hash.each do |(key, value)|
41
+ annotated["#{assoc_name}.#{key}"] = value
42
+ end
43
+ annotated
44
+ end
45
+
46
+ def is_document?(object)
47
+ object.respond_to?(:_parent)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,5 +1,7 @@
1
- require 'mongodoc/proxy'
2
- require 'mongodoc/parent_proxy'
1
+ require 'mongodoc/associations/proxy_base'
2
+ require 'mongodoc/associations/collection_proxy'
3
+ require 'mongodoc/associations/document_proxy'
4
+ require 'mongodoc/associations/hash_proxy'
3
5
 
4
6
  module MongoDoc
5
7
  module Attributes
@@ -34,11 +36,6 @@ module MongoDoc
34
36
  _parent._path_to_root(self, attrs)
35
37
  end
36
38
 
37
- def _selector_path_to_root(selector)
38
- return selector unless _parent
39
- _parent._selector_path_to_root(selector)
40
- end
41
-
42
39
  module ClassMethods
43
40
  def _attributes
44
41
  _keys + _associations
@@ -52,27 +49,32 @@ module MongoDoc
52
49
  end
53
50
 
54
51
  def has_one(*args)
52
+ options = args.extract_options!
53
+ assoc_class = if class_name = options.delete(:class_name)
54
+ type_name_with_module(class_name).constantize
55
+ end
56
+
55
57
  args.each do |name|
56
58
  _associations << name unless _associations.include?(name)
59
+
57
60
  attr_reader name
58
61
 
59
62
  define_method("#{name}=") do |value|
60
- if value
61
- raise NotADocumentError unless Document === value
62
- value._parent = ParentProxy.new(self, name)
63
- value._root = _root || self
64
- value._root.register_save_observer(value)
63
+ association = instance_variable_get("@#{name}")
64
+ unless association
65
+ association = Associations::DocumentProxy.new(:root => _root || self, :parent => self, :assoc_name => name, :assoc_class => assoc_class || self.class.class_from_name(name))
66
+ instance_variable_set("@#{name}", association)
65
67
  end
66
- instance_variable_set("@#{name}", value)
68
+ association.document = value
67
69
  end
68
70
 
69
- validates_associated name
71
+ validates_embedded name, :if => Proc.new { !send(name).nil? }
70
72
  end
71
73
  end
72
74
 
73
75
  def has_many(*args)
74
76
  options = args.extract_options!
75
- collection_class = if class_name = options.delete(:class_name)
77
+ assoc_class = if class_name = options.delete(:class_name)
76
78
  type_name_with_module(class_name).constantize
77
79
  end
78
80
 
@@ -82,13 +84,13 @@ module MongoDoc
82
84
  define_method("#{name}") do
83
85
  association = instance_variable_get("@#{name}")
84
86
  unless association
85
- association = Proxy.new(:root => _root || self, :parent => self, :assoc_name => name, :collection_class => collection_class || self.class.type_name_with_module(name.to_s.classify).constantize)
87
+ association = Associations::CollectionProxy.new(:root => _root || self, :parent => self, :assoc_name => name, :assoc_class => assoc_class || self.class.type_name_with_module(name.to_s.classify).constantize)
86
88
  instance_variable_set("@#{name}", association)
87
89
  end
88
90
  association
89
91
  end
90
92
 
91
- validates_associated name
93
+ validates_embedded name
92
94
 
93
95
  define_method("#{name}=") do |arrayish|
94
96
  proxy = send("#{name}")
@@ -100,6 +102,36 @@ module MongoDoc
100
102
  end
101
103
  end
102
104
 
105
+ def has_hash(*args)
106
+ options = args.extract_options!
107
+ assoc_class = if class_name = options.delete(:class_name)
108
+ type_name_with_module(class_name).constantize
109
+ end
110
+
111
+ args.each do |name|
112
+ _associations << name unless _associations.include?(name)
113
+
114
+ define_method("#{name}") do
115
+ association = instance_variable_get("@#{name}")
116
+ unless association
117
+ association = Associations::HashProxy.new(:root => _root || self, :parent => self, :assoc_name => name, :assoc_class => assoc_class || self.class.type_name_with_module(name.to_s.classify).constantize)
118
+ instance_variable_set("@#{name}", association)
119
+ end
120
+ association
121
+ end
122
+
123
+ validates_embedded name
124
+
125
+ define_method("#{name}=") do |hash|
126
+ send("#{name}").replace(hash)
127
+ end
128
+ end
129
+ end
130
+
131
+ def class_from_name(name)
132
+ type_name_with_module(name.to_s.classify).constantize rescue nil
133
+ end
134
+
103
135
  def type_name_with_module(type_name)
104
136
  (/^::/ =~ type_name) ? type_name : "#{parents}::#{type_name}"
105
137
  end
@@ -3,14 +3,15 @@ require 'mongodoc/cursor'
3
3
  module MongoDoc
4
4
  class Collection
5
5
  attr_accessor :_collection
6
- delegate :[], :clear, :count, :create_index, :db, :drop, :drop_index, :drop_indexes, :group, :hint, :index_information, :name, :options, :remove, :rename, :size, :to => :_collection
6
+
7
+ delegate :[], :clear, :count, :create_index, :db, :distinct, :drop, :drop_index, :drop_indexes, :group, :hint, :index_information, :map_reduce, :mapreduce, :name, :options, :pk_factory, :remove, :rename, :size, :to => :_collection
7
8
 
8
9
  def initialize(name)
9
10
  self._collection = self.class.mongo_collection(name)
10
11
  end
11
12
 
12
13
  def find(query = {}, options = {})
13
- cursor = MongoDoc::Cursor.new(_collection.find(query, options))
14
+ cursor = wrapped_cursor(query, options)
14
15
  if block_given?
15
16
  yield cursor
16
17
  cursor.close
@@ -34,12 +35,21 @@ module MongoDoc
34
35
 
35
36
  def update(spec, doc, options = {})
36
37
  _collection.update(spec, doc.to_bson, options)
37
- result = MongoDoc.database.command({'getlasterror' => 1})
38
- (result and result.has_key?('updatedExisting')) ? result['updatedExisting'] : false
38
+ (last_error || {})['updatedExisting'] || false
39
+ end
40
+
41
+ protected
42
+
43
+ def last_error
44
+ MongoDoc::Connection.database.command({'getlasterror' => 1})
45
+ end
46
+
47
+ def wrapped_cursor(query = {}, options = {})
48
+ MongoDoc::Cursor.new(self, _collection.find(query, options))
39
49
  end
40
50
 
41
51
  def self.mongo_collection(name)
42
- MongoDoc.database.collection(name)
52
+ MongoDoc::Connection.database.collection(name)
43
53
  end
44
54
  end
45
55
  end
@@ -1,25 +1,88 @@
1
1
  module MongoDoc
2
- mattr_accessor :config, :config_path, :connection, :database
3
- self.config_path = './mongodb.yml'
4
-
5
- def self.connect_to_database(name = nil, host = nil, port = nil, options = nil, force_connect = false)
6
- name ||= configuration['name']
7
- self.database = ((!force_connect && connection)|| connect(host, port, options)).db(name)
8
- raise NoDatabaseError unless database
9
- database
10
- end
2
+ class NoConnectionError < RuntimeError; end
3
+ class UnsupportedServerVersionError < RuntimeError; end
11
4
 
12
- def self.connect(*args)
13
- opts = args.extract_options!
14
- host = args[0] || configuration['host'] || configuration['host_pairs']
15
- port = args[1] || configuration['port']
16
- options = opts.empty? ? configuration['options'] || {} : opts
17
- self.connection = Mongo::Connection.new(host, port, options)
18
- raise NoConnectionError unless connection
19
- connection
20
- end
5
+ module Connection
6
+
7
+ extend self
8
+
9
+ attr_writer :config_path, :env, :host, :name, :options, :port, :strict
10
+
11
+ def config_path
12
+ @config_path || default_path
13
+ end
14
+
15
+ def configuration
16
+ @configuration ||= File.exists?(config_path) ? YAML.load_file(config_path)[env] : {}
17
+ end
18
+
19
+ def connection
20
+ @connection ||= connect
21
+ end
22
+
23
+ def database
24
+ @database ||= connection.db(name, :strict => strict)
25
+ end
26
+
27
+ def env
28
+ if rails?
29
+ Rails.env
30
+ else
31
+ @env ||= 'development'
32
+ end
33
+ end
34
+
35
+ def host
36
+ @host ||= configuration['host']
37
+ end
38
+
39
+ def name
40
+ @name ||= configuration['name'] || default_name
41
+ end
42
+
43
+ def options
44
+ @options ||= configuration['options'] || {}
45
+ end
46
+
47
+ def port
48
+ @port ||= configuration['port']
49
+ end
50
+
51
+ def strict
52
+ @strict ||= configuration['strict'] || false
53
+ end
54
+
55
+ private
56
+
57
+ def connect
58
+ connection = Mongo::Connection.new(host, port, options)
59
+ raise NoConnectionError unless connection
60
+ verify_server_version(connection)
61
+ connection
62
+ end
63
+
64
+ def default_name
65
+ if rails?
66
+ "#{Rails.root.basename}_#{Rails.env}"
67
+ else
68
+ "mongodoc"
69
+ end
70
+ end
71
+
72
+ def default_path
73
+ if rails?
74
+ Rails.root + 'config/mongodb.yml'
75
+ else
76
+ './mongodb.yml'
77
+ end
78
+ end
79
+
80
+ def rails?
81
+ Object.const_defined?("Rails")
82
+ end
21
83
 
22
- def self.configuration
23
- self.config ||= File.exists?(config_path || '') ? YAML.load_file(config_path) : {}
84
+ def verify_server_version(connection)
85
+ raise UnsupportedServerVersionError.new('MongoDoc requires at least mongoDB version 1.3.2') unless connection.server_version >= "1.3.2"
86
+ end
24
87
  end
25
88
  end
@@ -101,11 +101,12 @@ module MongoDoc #:nodoc:
101
101
  def each(&block)
102
102
  @collection ||= execute
103
103
  if block_given?
104
- @collection = collection.inject([]) do |container, item|
104
+ container = []
105
+ collection.each do |item|
105
106
  container << item
106
107
  yield item
107
- container
108
108
  end
109
+ @collection = container
109
110
  end
110
111
  self
111
112
  end
@@ -166,9 +167,10 @@ module MongoDoc #:nodoc:
166
167
  #
167
168
  # Returns <tt>self</tt>
168
169
  def criteria(criteria_conditions = {})
169
- criteria_conditions.inject(self) do |criteria, (key, value)|
170
- criteria.send(key, value)
170
+ criteria_conditions.each do |(key, value)|
171
+ send(key, value)
171
172
  end
173
+ self
172
174
  end
173
175
 
174
176
  # Adds a criterion to the +Criteria+ that specifies values that must all
@@ -242,6 +244,9 @@ module MongoDoc #:nodoc:
242
244
  #
243
245
  # Returns: <tt>self</tt>
244
246
  def id(id_or_object_id)
247
+ if id_or_object_id.kind_of?(String)
248
+ id_or_object_id = Mongo::ObjectID.from_string(id_or_object_id)
249
+ end
245
250
  selector[:_id] = id_or_object_id; self
246
251
  end
247
252