mongo_db 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,27 +1,33 @@
1
+ require 'set'
2
+ require 'date'
3
+
1
4
  module Mongo::Ext::Collection
2
- #
5
+ #
3
6
  # CRUD
4
- #
7
+ #
5
8
  def save_with_ext doc, opts = {}
6
9
  save_without_ext doc, reverse_merge_defaults(opts, :safe)
7
10
  end
8
-
9
- def insert_with_ext doc_or_docs, opts = {}
10
- insert_without_ext doc_or_docs, reverse_merge_defaults(opts, :safe)
11
+
12
+ def insert_with_ext args, opts = {}
13
+ result = insert_without_ext args, reverse_merge_defaults(opts, :safe)
14
+
15
+ # fix for mongodriver, it will return single result if we supply [doc] as args
16
+ (args.is_a?(Array) and !result.is_a?(Array)) ? [result] : result
11
17
  end
12
-
13
- def update_with_ext selector, document, opts = {}
18
+
19
+ def update_with_ext selector, doc, opts = {}
14
20
  selector = convert_underscore_to_dollar_in_selector selector
15
- document = convert_underscore_to_dollar_in_update document
16
-
17
- # because :multi works only with $ operators, we need to check it
18
- opts = if document.keys.any?{|k| k =~ /^\$/}
21
+ doc = convert_underscore_to_dollar_in_update doc
22
+
23
+ # because :multi works only with $ operators, we need to check if it's applicable
24
+ opts = if doc.keys.any?{|k| k =~ /^\$/}
19
25
  reverse_merge_defaults(opts, :safe, :multi)
20
26
  else
21
27
  reverse_merge_defaults(opts, :safe)
22
28
  end
23
-
24
- update_without_ext selector, document, opts
29
+
30
+ update_without_ext selector, doc, opts
25
31
  end
26
32
 
27
33
  def remove_with_ext selector = {}, opts = {}
@@ -32,64 +38,107 @@ module Mongo::Ext::Collection
32
38
  def destroy *args
33
39
  remove *args
34
40
  end
35
-
36
- #
41
+
42
+ #
37
43
  # Querying
38
- #
39
- def first spec_or_object_id = nil, opts = {}
40
- spec_or_object_id = convert_underscore_to_dollar_in_selector spec_or_object_id if spec_or_object_id.is_a? Hash
41
-
42
- o = find_one spec_or_object_id, opts
43
- o = ::Mongo::Ext::HashHelper.symbolize o if Mongo.defaults[:symbolize]
44
- o
44
+ #
45
+ def first selector = nil, opts = {}
46
+ selector = convert_underscore_to_dollar_in_selector selector if selector.is_a? Hash
47
+
48
+ h = find_one selector, opts
49
+ symbolize_doc h
45
50
  end
46
-
51
+
47
52
  def all *args, &block
48
53
  if block
49
54
  each *args, &block
50
55
  else
51
56
  list = []
52
- each(*args){|o| list << o}
57
+ each(*args){|doc| list << doc}
53
58
  list
54
59
  end
55
60
  end
56
-
61
+
57
62
  def each selector = {}, opts = {}, &block
58
63
  selector = convert_underscore_to_dollar_in_selector selector
59
-
64
+
60
65
  cursor = nil
61
66
  begin
62
67
  cursor = find selector, reverse_merge_defaults(opts, :batch_size)
63
- cursor.each do |o|
64
- o = ::Mongo::Ext::HashHelper.symbolize o if Mongo.defaults[:symbolize]
65
- block.call o
68
+ cursor.each do |doc|
69
+ doc = symbolize_doc doc
70
+ block.call doc
66
71
  end
67
72
  nil
68
73
  ensure
69
74
  cursor.close if cursor
70
75
  end
76
+ nil
71
77
  end
72
-
78
+
73
79
  protected
80
+ QUERY_KEYWORDS = [
81
+ :_lt, :_lte, :_gt, :_gte,
82
+ :_all, :_exists, :_mod, :_ne, :_in, :_nin,
83
+ :_nor, :_or, :_and,
84
+ :_size, :_type
85
+ ].to_set
86
+
87
+ UPDATE_KEYWORDS = [
88
+ :_inc, :_set, :_unset, :_push, :_pushAll, :_addToSet, :_pop, :_pull, :_pullAll, :_rename, :_bit
89
+ ].to_set
90
+
91
+ def reverse_merge_defaults opts, *keys
92
+ h = opts.clone
93
+ keys.each do |k|
94
+ h[k] = Mongo.defaults[k] if Mongo.defaults.include?(k) and !h.include?(k)
95
+ end
96
+ h
97
+ end
98
+
99
+ # symbolizing hashes
100
+ def symbolize_doc doc
101
+ return doc unless Mongo.defaults[:symbolize]
102
+
103
+ Mongo::Ext::Collection.convert_doc doc do |k, v, result|
104
+ k = k.to_sym if k.is_a? String
105
+ result[k] = v
106
+ end
107
+ end
108
+
109
+ # replaces :_lt to :$lt in query
74
110
  def convert_underscore_to_dollar_in_selector selector
75
- if Mongo.defaults[:convert_underscore_to_dollar]
76
- selector = ::Mongo::Ext::HashHelper.convert_underscore_to_dollar_in_selector selector
111
+ return selector unless Mongo.defaults[:convert_underscore_to_dollar]
112
+
113
+ Mongo::Ext::Collection.convert_doc selector do |k, v, result|
114
+ k = "$#{k.to_s[1..-1]}".to_sym if QUERY_KEYWORDS.include?(k)
115
+ result[k] = v
77
116
  end
78
- selector
79
117
  end
80
-
118
+
119
+ # replaces :_set to :$set in query
81
120
  def convert_underscore_to_dollar_in_update update
82
- if Mongo.defaults[:convert_underscore_to_dollar]
83
- update = ::Mongo::Ext::HashHelper.convert_underscore_to_dollar_in_selector update
121
+ return update unless Mongo.defaults[:convert_underscore_to_dollar]
122
+
123
+ Mongo::Ext::Collection.convert_doc update do |k, v, result|
124
+ k = "$#{k.to_s[1..-1]}".to_sym if UPDATE_KEYWORDS.include?(k)
125
+ result[k] = v
84
126
  end
85
- update
86
127
  end
87
-
88
- def reverse_merge_defaults opts, *keys
89
- h = opts.clone
90
- keys.each do |k|
91
- h[k] = Mongo.defaults[k] if Mongo.defaults.include?(k) and !h.include?(k)
128
+
129
+ # walks on hash and creates another (also works with nested & arrays)
130
+ def self.convert_doc doc, &block
131
+ if doc.is_a? Hash
132
+ result = {}
133
+ doc.each do |k, v|
134
+ v = convert_doc v, &block
135
+ block.call k, v, result
136
+ end
137
+ result
138
+ elsif doc.is_a? Array
139
+ doc.collect{|v| convert_doc v, &block}
140
+ else
141
+ doc
92
142
  end
93
- h
94
143
  end
95
144
  end
@@ -7,8 +7,7 @@ class Mongo::Ext; end
7
7
 
8
8
  %w(
9
9
  database
10
- collection
11
- hash_helper
10
+ collection
12
11
  ).each{|f| require "mongo_db/driver/core/#{f}"}
13
12
 
14
13
  # defaults
@@ -25,7 +24,7 @@ Mongo::DB.send :include, Mongo::Ext::DB
25
24
  # collection
26
25
  Mongo::Collection.class_eval do
27
26
  include Mongo::Ext::Collection
28
-
27
+
29
28
  %w(insert update remove save).each do |method|
30
29
  alias_method "#{method}_without_ext", method
31
30
  alias_method method, "#{method}_with_ext"
@@ -1,7 +1,7 @@
1
- module Mongo::Ext::Collection
2
- #
1
+ module Mongo::Ext::CollectionFinders
2
+ #
3
3
  # first_by_id, special case
4
- #
4
+ #
5
5
  def first_by_id id
6
6
  first _id: id
7
7
  end
@@ -11,31 +11,27 @@ module Mongo::Ext::Collection
11
11
  first_by_id(id) || raise(Mongo::NotFound, "document with id #{id} not found!")
12
12
  end
13
13
  alias_method :by_id!, :first_by_id!
14
-
15
- def where &block
16
- Mongo::Ext::Query.new self, &block
17
- end
18
-
14
+
19
15
  protected
20
- #
16
+ #
21
17
  # first_by_field, all_by_field
22
- #
18
+ #
23
19
  def method_missing clause, *a, &b
24
- if clause =~ /^([a-z]_by_[a-z_])|(by_[a-z_])/
25
- clause = clause.to_s
26
-
20
+ if clause =~ /^([a-z]_by_[a-z_])|(by_[a-z_])/
21
+ clause = clause.to_s
22
+
27
23
  bang = clause =~ /!$/
28
24
  clause = clause[0..-2] if bang
29
-
30
- finder, field = if clause =~ /^by_/
25
+
26
+ finder, field = if clause =~ /^by_/
31
27
  ['first', clause.sub(/by_/, '')]
32
- else
33
- clause.split(/_by_/, 2)
34
- end
28
+ else
29
+ clause.split(/_by_/, 2)
30
+ end
35
31
  finder = 'first' if finder == 'find'
36
-
32
+
37
33
  raise "You can't use bang version with :#{finder}!" if bang and finder != 'first'
38
-
34
+
39
35
  raise "invalid arguments for finder (#{a})!" unless a.size == 1
40
36
  field_value = a.first
41
37
 
@@ -3,5 +3,8 @@ require 'mongo_db/driver/core'
3
3
  class Mongo::NotFound < StandardError; end
4
4
 
5
5
  %w(
6
- collection
7
- ).each{|f| require "mongo_db/driver/more/#{f}"}
6
+ collection_finders
7
+ ).each{|f| require "mongo_db/driver/more/#{f}"}
8
+
9
+ Mongo::Collection.send :include, Mongo::Ext::CollectionFinders
10
+
@@ -1,31 +1,31 @@
1
1
  MONGO_TEST_DATABASE_NAME = 'default_test'
2
2
 
3
- rspec do
3
+ rspec do
4
4
  def mongo
5
5
  $mongo || raise('Mongo not defined (use :with_mongo helper)!')
6
6
  end
7
-
7
+
8
8
  def clear_mongo name = MONGO_TEST_DATABASE_NAME
9
9
  mongo.db.collection_names.each do |name|
10
10
  next if name =~ /^system\./
11
11
  mongo.db.collection(name).drop
12
12
  end
13
13
  end
14
-
14
+
15
15
  class << self
16
16
  def with_mongo
17
17
  before :all do
18
18
  require 'ostruct'
19
-
19
+
20
20
  $mongo = OpenStruct.new.tap do |m|
21
21
  m.connection = Mongo::Connection.new
22
22
  m.db = m.connection.db MONGO_TEST_DATABASE_NAME
23
23
  end
24
- end
24
+ end
25
25
  after(:all){$mongo = nil}
26
-
27
- before do
28
- clear_mongo
26
+
27
+ before do
28
+ clear_mongo
29
29
  end
30
30
  end
31
31
  end
@@ -0,0 +1,154 @@
1
+ module Mongo::Ext::ModelHelper
2
+ #
3
+ # CRUD
4
+ #
5
+ def save_with_model doc, opts = {}
6
+ if doc.is_a? Hash
7
+ save_without_model doc, opts
8
+ else
9
+ if id = doc.instance_variable_get(:@_id)
10
+ update({:_id => id}, doc, opts.merge(upsert: true))
11
+ else
12
+ insert doc, opts
13
+ end
14
+ end
15
+ end
16
+
17
+ def insert_with_model args, opts = {}
18
+ docs = args.is_a?(Array) ? args : [args]
19
+ result = _insert_with_model docs, opts
20
+ args.is_a?(Array) ? result : result.first
21
+ end
22
+
23
+ def update_with_model selector, doc, opts = {}
24
+ doc = Mongo::Ext::ModelHelper.convert_object_to_doc doc unless doc.is_a?(Hash)
25
+ update_without_model selector, doc, opts
26
+ end
27
+
28
+ def remove_with_model arg = {}, opts = {}
29
+ if arg.is_a? Hash
30
+ remove_without_model arg, opts
31
+ else
32
+ id = arg.instance_variable_get(:@_id) || "can't remove object without _id (#{arg})!"
33
+ remove_without_model({_id: id}, opts)
34
+ end
35
+ end
36
+
37
+
38
+ #
39
+ # Querying
40
+ #
41
+ def first *args, &block
42
+ doc = super *args, &block
43
+ Mongo::Ext::ModelHelper.convert_doc_to_object doc
44
+ end
45
+
46
+ def each *args, &block
47
+ super *args do |doc|
48
+ doc = Mongo::Ext::ModelHelper.convert_doc_to_object(doc)
49
+ block.call doc
50
+ end
51
+ nil
52
+ end
53
+
54
+ protected
55
+ def _insert_with_model docs, opts
56
+ hashes = docs.collect do |doc|
57
+ doc.is_a?(Hash) ? doc : Mongo::Ext::ModelHelper.convert_object_to_doc(doc)
58
+ end
59
+ result = insert_without_model hashes, opts
60
+ hashes.each_with_index do |h, i|
61
+ Mongo::Ext::ModelHelper.update_object_after_insertion docs[i], h
62
+ end
63
+ result
64
+ end
65
+
66
+
67
+ SIMPLE_TYPES = [
68
+ Fixnum, Float,
69
+ TrueClass, FalseClass,
70
+ String, Symbol,
71
+ Array, Hash, Set,
72
+ Data, DateTime,
73
+ NilClass, Time,
74
+ BSON::ObjectId
75
+ ].to_set
76
+
77
+ class << self
78
+ def update_object_after_insertion doc, hash
79
+ return if doc.is_a? Hash
80
+ if id = hash[:_id] || hash['_id']
81
+ doc.instance_variable_set :@_id, id
82
+ end
83
+ end
84
+
85
+ # converts object to hash (also works with nested & arrays)
86
+ def convert_object_to_doc obj
87
+ return obj.to_mongo if obj.respond_to? :to_mongo
88
+
89
+ if obj.is_a? Hash
90
+ doc = {}
91
+ obj.each do |k, v|
92
+ doc[k] = convert_object_to_doc v
93
+ end
94
+ doc
95
+ elsif obj.is_a? Array
96
+ obj.collect{|v| convert_object_to_doc v}
97
+ elsif SIMPLE_TYPES.include? obj.class
98
+ obj
99
+ else
100
+ doc = {}
101
+
102
+ # copying instance variables to hash
103
+ obj.instance_variables.each do |iv_name|
104
+ # skipping variables starting with _xx, usually they
105
+ # have specific meaning and used for example for cache
106
+ next if iv_name =~ /^@_/ and iv_name != :@_id
107
+
108
+ k = iv_name.to_s[1..-1]
109
+ k = k.to_sym if Mongo.defaults[:symbolize]
110
+ v = obj.instance_variable_get iv_name
111
+ doc[k] = convert_object_to_doc v
112
+ end
113
+
114
+ # setting class
115
+ class_name = '_class'
116
+ class_name = class_name.to_sym if Mongo.defaults[:symbolize]
117
+ doc[class_name] = obj.class.name
118
+
119
+ doc
120
+ end
121
+ end
122
+
123
+ def convert_doc_to_object doc
124
+ if doc.is_a? Hash
125
+ if class_name = doc[:_class] || doc['_class']
126
+ klass = constantize class_name
127
+ obj = klass.new
128
+ doc.each do |k, v|
129
+ next if k.to_sym == :_class
130
+
131
+ v = convert_doc_to_object v
132
+ obj.instance_variable_set "@#{k}", v
133
+ end
134
+ obj
135
+ else
136
+ doc
137
+ end
138
+ elsif doc.is_a? Array
139
+ doc.collect{|v| convert_doc_to_object v}
140
+ else
141
+ doc
142
+ end
143
+ end
144
+
145
+ def constantize class_name
146
+ @constantize_cache ||= {}
147
+ unless klass = @constantize_cache[class_name]
148
+ klass = eval class_name, TOPLEVEL_BINDING, __FILE__, __LINE__
149
+ @constantize_cache[class_name] = klass
150
+ end
151
+ klass
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,3 @@
1
+ class Mongo::Ext::ModelSerializer
2
+
3
+ end
@@ -1,4 +1,16 @@
1
1
  require 'mongo_db/driver'
2
2
 
3
3
  %w(
4
- ).each{|f| require "mongo_db/model/#{f}"}
4
+ model_serializer
5
+ model_helper
6
+ ).each{|f| require "mongo_db/model/#{f}"}
7
+
8
+ # collection
9
+ Mongo::Collection.class_eval do
10
+ include Mongo::Ext::ModelHelper
11
+
12
+ %w(insert update remove save).each do |method|
13
+ alias_method "#{method}_without_model", method
14
+ alias_method method, "#{method}_with_model"
15
+ end
16
+ end
@@ -4,7 +4,7 @@
4
4
  # unless defined_method? :subset
5
5
  # def subset *keys, &block
6
6
  # keys = keys.first if keys.first.is_a? Array
7
- # h = {}
7
+ # h = {}
8
8
  # if keys
9
9
  # self.each do |k, v|
10
10
  # h[k] = v if keys.include? k
@@ -17,14 +17,14 @@
17
17
  # h
18
18
  # end
19
19
  # end
20
- #
20
+ #
21
21
  # unless defined_method? :reverse_merge
22
22
  # def reverse_merge(other_hash)
23
23
  # other_hash.merge(self)
24
24
  # end
25
- #
25
+ #
26
26
  # def reverse_merge!(other_hash)
27
27
  # merge!(other_hash){|k,o,n| o }
28
28
  # end
29
- # end
29
+ # end
30
30
  # end
data/readme.md CHANGED
@@ -1,18 +1,20 @@
1
1
  Object Model & Ruby driver enhancements for MongoDB.
2
2
 
3
+ - Driver enchancements
4
+ - Persistence for pure Ruby objects
5
+ - Migrations (work in progress)
6
+ - Object Model (callbacks, validations, mass-assignment, finders, ...) (work in progress)
7
+
3
8
  # MongoDB driver enhancements
4
9
 
5
- MongoDB itself is very powerful, flexible and simple tool, but it's Ruby driver hides it behind complicated API.
6
- This enhancements alter the Ruby-driver's API to be more simple and intuitive.
10
+ MongoDB itself is very powerful, flexible and simple tool, but it's Ruby driver has a little complicated API.
11
+ This enhancements alter this API to be more simple and intuitive.
7
12
 
8
13
  - Makes API of mongo-ruby-driver friendly & handy.
9
14
  - No extra abstraction or complexities introduced, all things are exactly the same as in MongoDB.
10
- - Simple migrations support (work in progress).
11
-
12
- Note: By default it also adds a little magic and alters some default values of standard driver to be more useful, but You can omit it and require only the core stuff: use *requre 'mongo_db/driver/core'* instead of *requre 'mongo_db/driver'*
13
15
 
14
16
  ``` ruby
15
- require 'mongo_db/driver'
17
+ require 'mongo_db/driver/core'
16
18
 
17
19
  # changing some defaults
18
20
  Mongo.defaults.merge! symbolize: true, multi: true, safe: true
@@ -35,33 +37,92 @@ db.units.save tassadar
35
37
  # querying - first & all, there's also :each, the same as :all
36
38
  db.units.first name: 'Zeratul' # => zeratul
37
39
  db.units.all name: 'Zeratul' # => [zeratul]
38
- db.units.all name: 'Zeratul' do |hero|
39
- hero # => zeratul
40
+ db.units.all name: 'Zeratul' do |unit|
41
+ unit # => zeratul
40
42
  end
41
43
  ```
42
44
 
43
45
  Optionall stuff:
44
46
 
47
+ - Simple query enchancements
48
+
45
49
  ``` ruby
50
+ require 'mongo_db/driver/more'
51
+
46
52
  # simple finders (bang versions also availiable)
47
53
  db.units.by_name 'Zeratul' # => zeratul
48
54
  db.units.first_by_name 'Zeratul' # => zeratul
49
55
  db.units.all_by_name 'Zeratul' # => [zeratul]
50
56
 
51
57
  # query sugar, use {life: {_lt: 100}} instead of {life: {:$lt => 100}}
52
- # it will affect olny small set of keywords (:_lt, :_inc),
53
- # other underscored keys will be intact.
54
- Mongo.defaults.merge! convert_underscore_to_dollar: true
55
- db.units.all life: {_lt: 100} # => [tassadar]
56
-
57
- # it's also trivial to add support for {:life.lt => 100} notion, but
58
- # it uses ugly '=>' hash notation instead of ':' and it differs from
59
- # how it looks in native MongoDB JSON query.
58
+ Mongo.defaults.merge! convert_underscore_to_dollar: true
59
+ db.units.all 'stats.life' => {_lt: 100} # => [tassadar]
60
60
  ```
61
61
 
62
- More docs - there's no need for more docs, the whole point of this extension is to be small, intuitive, 100% compatible with official driver (at least should be), and require no extra knowledge.
62
+ More docs - there's no need for more docs, the whole point of this extension is to be small, intuitive, 100% compatible with the official driver (at least should be), and require no extra knowledge.
63
63
  So, please use standard Ruby driver documentation.
64
64
 
65
+ # Persistence for pure Ruby objects
66
+
67
+ Save any Ruby object to MongoDB, as if it's hash. Object can be any type, simple or composite with other objects / arrays / hashes inside.
68
+
69
+ Note: the :initialize method should allow to create object without arguments.
70
+
71
+ ``` ruby
72
+ # let's define the game unit
73
+ class Unit
74
+ attr_reader :name, :stats
75
+
76
+ # don't forget to allow creating object with no arguments
77
+ def initialize name = nil, stats = nil
78
+ @name, @stats = name, stats
79
+ end
80
+
81
+ class Stats
82
+ attr_accessor :attack, :life, :shield
83
+
84
+ def initialize attack = nil, life = nil, shield = nil
85
+ @attack, @life, @shield = attack, life, shield
86
+ end
87
+ end
88
+ end
89
+
90
+ # connecting to MongoDB
91
+ require 'mongo_db/model'
92
+ Mongo.defaults.merge! symbolize: true, multi: true, safe: true
93
+ connection = Mongo::Connection.new
94
+ db = connection.db 'default_test'
95
+
96
+ # create
97
+ zeratul = Unit.new('Zeratul', Unit::Stats.new(85, 300, 100))
98
+ tassadar = Unit.new('Tassadar', Unit::Stats.new(0, 80, 300))
99
+
100
+ db.units.save zeratul
101
+ db.units.save tassadar
102
+
103
+ # udate (we made error - mistakenly set Tassadar's attack as zero, let's fix it)
104
+ tassadar.stats.attack = 20
105
+ db.units.save tassadar
106
+
107
+ # querying first & all, there's also :each, the same as :all
108
+ db.units.first name: 'Zeratul' # => zeratul
109
+ db.units.all name: 'Zeratul' # => [zeratul]
110
+ db.units.all name: 'Zeratul' do |unit|
111
+ unit # => zeratul
112
+ end
113
+
114
+ # simple finders (bang versions also availiable)
115
+ db.units.by_name 'Zeratul' # => zeratul
116
+ db.units.first_by_name 'Zeratul' # => zeratul
117
+ db.units.all_by_name 'Zeratul' # => [zeratul]
118
+
119
+ # query sugar, use {life: {_lt: 100}} instead of {life: {:$lt => 100}}
120
+ Mongo.defaults.merge! convert_underscore_to_dollar: true
121
+ db.units.all('stats.life' => {_lt: 100}) # => [tassadar]
122
+ ```
123
+
124
+ # Migrations (work in progress)
125
+
65
126
  # Object Model (work in progress)
66
127
 
67
128
  Model designed after the excellent "Domain-Driven Design" book by Eric Evans.