mongo_db 0.1.5 → 0.1.6

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.
@@ -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.