perpetuity-mongodb 1.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1eb06e99c8f903ef80d322b5a5efafed98bdfe58
4
+ data.tar.gz: 0688a5ef4c25609887ef02748ba5fea2cf4d435e
5
+ SHA512:
6
+ metadata.gz: e9a4df359600fa4bd7a5434b91adffe4cd492605769239990d5652cd14ae25288779df219eb04cf31a97262036186f262c65eb77003e34fdaf3912d920bb8853
7
+ data.tar.gz: 3167ead40e5411aea1b1df00340dba7ab8c7a062a51a9264f9b6c01096305cf29c42699f7c2fddc2b73516d825c649701a1e5cfbf57f8856632087a011846322
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in perpetuity-mongodb.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Jamie Gaskins
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,36 @@
1
+ # Perpetuity::MongoDB
2
+
3
+ This is the MongoDB adapter for Perpetuity.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'perpetuity-mongodb'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install perpetuity-mongodb
18
+
19
+ ## Usage
20
+
21
+ Put this at the top of your application (or in a Rails initializer):
22
+
23
+ ```ruby
24
+ require 'perpetuity/mongodb' # Unnecessary if using Rails
25
+ Perpetuity.data_source :mongodb, 'my_perpetuity_database'
26
+ ```
27
+
28
+ For information on using Perpetuity to persist your Ruby objects, see the [main Perpetuity repo](https://github.com/jgaskins/perpetuity).
29
+
30
+ ## Contributing
31
+
32
+ 1. Fork it
33
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
34
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
35
+ 4. Push to the branch (`git push origin my-new-feature`)
36
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,231 @@
1
+ require 'perpetuity'
2
+ require 'moped'
3
+ require 'perpetuity/mongodb/query'
4
+ require 'perpetuity/mongodb/nil_query'
5
+ require 'perpetuity/mongodb/index'
6
+ require 'perpetuity/mongodb/serializer'
7
+ require 'set'
8
+ require 'perpetuity/exceptions/duplicate_key_error'
9
+ require 'perpetuity/attribute'
10
+
11
+ module Perpetuity
12
+ class MongoDB
13
+ attr_accessor :host, :port, :db, :pool_size, :username, :password
14
+
15
+ def initialize options
16
+ @host = options.fetch(:host, 'localhost')
17
+ @port = options.fetch(:port, 27017)
18
+ @db = options.fetch(:db)
19
+ @pool_size = options.fetch(:pool_size, 5)
20
+ @username = options[:username]
21
+ @password = options[:password]
22
+ @session = nil
23
+ @indexes = Hash.new { |hash, key| hash[key] = active_indexes(key) }
24
+ @connected = false
25
+ end
26
+
27
+ def session
28
+ @session ||= Moped::Session.new(["#{host}:#{port}"]).with(safe: true)
29
+ end
30
+
31
+ def connect
32
+ session.login(@username, @password) if @username and @password
33
+ @connected = true
34
+ session
35
+ end
36
+
37
+ def connected?
38
+ !!@connected
39
+ end
40
+
41
+ def database
42
+ session.use db
43
+ connect unless connected?
44
+ session
45
+ end
46
+
47
+ def collection klass
48
+ database[klass.to_s]
49
+ end
50
+
51
+ def insert klass, objects, _
52
+ if objects.is_a? Array
53
+ objects.each do |object|
54
+ object[:_id] = object.delete('id') || Moped::BSON::ObjectId.new
55
+ end
56
+
57
+ collection(klass).insert objects
58
+ objects.map { |object| object[:_id] }
59
+ else
60
+ insert(klass, [objects], _).first
61
+ end
62
+
63
+ rescue Moped::Errors::OperationFailure => e
64
+ if e.message =~ /duplicate key/
65
+ e.message =~ /\$(\w+)_\d.*dup key: { : (.*) }/
66
+ key = $1
67
+ value = $2.gsub("\\\"", "\"")
68
+ raise DuplicateKeyError, "Tried to insert #{klass} with duplicate unique index: #{key} => #{value}"
69
+ end
70
+ end
71
+
72
+ def count klass, criteria=nil_query, &block
73
+ q = block_given? ? query(&block).to_db : criteria.to_db
74
+ collection(klass).find(q).count
75
+ end
76
+
77
+ def delete_all klass
78
+ collection(klass.to_s).find.remove_all
79
+ end
80
+
81
+ def first klass
82
+ document = collection(klass.to_s).find.limit(1).first
83
+ document[:id] = document.delete("_id")
84
+
85
+ document
86
+ end
87
+
88
+ def retrieve klass, criteria, options = {}
89
+ # MongoDB uses '_id' as its ID field.
90
+ criteria = to_bson_id(criteria.to_db)
91
+
92
+ skipped = options.fetch(:skip) { 0 }
93
+
94
+ query = collection(klass.to_s)
95
+ .find(criteria)
96
+ .skip(skipped)
97
+ .limit(options[:limit])
98
+
99
+ sort(query, options).map do |document|
100
+ document[:id] = document.delete("_id")
101
+ document
102
+ end
103
+ end
104
+
105
+ def increment klass, id, attribute, count=1
106
+ find(klass, id).update '$inc' => { attribute => count }
107
+ end
108
+
109
+ def find klass, id
110
+ collection(klass).find(to_bson_id(_id: id))
111
+ end
112
+
113
+ def to_bson_id criteria
114
+ criteria = criteria.dup
115
+
116
+ # Check for both string and symbol ID in criteria
117
+ if criteria.has_key?('id')
118
+ criteria['_id'] = Moped::BSON::ObjectId(criteria['id']) rescue criteria['id']
119
+ criteria.delete 'id'
120
+ end
121
+
122
+ if criteria.has_key?(:id)
123
+ criteria[:_id] = Moped::BSON::ObjectId(criteria[:id]) rescue criteria[:id]
124
+ criteria.delete :id
125
+ end
126
+
127
+ criteria
128
+ end
129
+
130
+ def sort query, options
131
+ return query unless options[:attribute] &&
132
+ options[:direction]
133
+
134
+ sort_orders = { ascending: 1, descending: -1 }
135
+ sort_field = options[:attribute]
136
+ sort_direction = options[:direction]
137
+ sort_criteria = { sort_field => sort_orders[sort_direction] }
138
+ query.sort(sort_criteria)
139
+ end
140
+
141
+ def all klass
142
+ retrieve klass, nil_query, {}
143
+ end
144
+
145
+ def delete id, klass
146
+ collection(klass.to_s).find("_id" => id).remove
147
+ end
148
+
149
+ def update klass, id, new_data
150
+ find(klass, id).update('$set' => new_data)
151
+ end
152
+
153
+ def can_serialize? value
154
+ serializable_types.include? value.class
155
+ end
156
+
157
+ def drop_collection to_be_dropped
158
+ collection(to_be_dropped.to_s).drop
159
+ end
160
+
161
+ def query &block
162
+ Query.new(&block)
163
+ end
164
+
165
+ def nil_query
166
+ NilQuery.new
167
+ end
168
+
169
+ def negate_query &block
170
+ Query.new(&block).negate
171
+ end
172
+
173
+ def index klass, attribute, options={}
174
+ @indexes[klass] ||= Set.new
175
+
176
+ index = Index.new(klass, attribute, options)
177
+ @indexes[klass] << index
178
+ index
179
+ end
180
+
181
+ def indexes klass
182
+ @indexes[klass]
183
+ end
184
+
185
+ def active_indexes klass
186
+ collection(klass).indexes.map do |index|
187
+ key = index['key'].keys.first
188
+ direction = index['key'][key]
189
+ unique = index['unique']
190
+ Index.new(klass, Attribute.new(key), order: Index::KEY_ORDERS[direction], unique: unique)
191
+ end.to_set
192
+ end
193
+
194
+ def activate_index! index
195
+ attribute = index.attribute.to_s
196
+ order = index.order == :ascending ? 1 : -1
197
+ unique = index.unique?
198
+
199
+ collection(index.collection).indexes.create({attribute => order}, unique: unique)
200
+ index.activate!
201
+ end
202
+
203
+ def remove_index index
204
+ coll = collection(index.collection)
205
+ db_indexes = coll.indexes.select do |db_index|
206
+ db_index['name'] =~ /\A#{index.attribute}/
207
+ end.map { |idx| idx['key'] }
208
+
209
+ if db_indexes.any?
210
+ collection(index.collection).indexes.drop db_indexes.first
211
+ end
212
+ end
213
+
214
+ def serialize object, mapper
215
+ Serializer.new(mapper).serialize object
216
+ end
217
+
218
+ def serialize_changed_attributes object, original, mapper
219
+ Serializer.new(mapper).serialize_changes object, original
220
+ end
221
+
222
+ def unserialize data, mapper
223
+ Serializer.new(mapper).unserialize data
224
+ end
225
+
226
+ private
227
+ def serializable_types
228
+ @serializable_types ||= [NilClass, TrueClass, FalseClass, Fixnum, Float, String, Array, Hash, Time]
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,52 @@
1
+ module Perpetuity
2
+ class MongoDB
3
+ class Index
4
+ KEY_ORDERS = { 1 => :ascending, -1 => :descending }
5
+ attr_reader :collection, :attribute
6
+
7
+ def initialize klass, attribute, options={}
8
+ @collection = klass
9
+ @attribute = attribute
10
+ @unique = options.fetch(:unique) { false }
11
+ @order = options.fetch(:order) { :ascending }
12
+ @activated = false
13
+ end
14
+
15
+ def active?
16
+ @activated
17
+ end
18
+
19
+ def inactive?
20
+ !active?
21
+ end
22
+
23
+ def activate!
24
+ @activated = true
25
+ end
26
+
27
+ def unique?
28
+ @unique
29
+ end
30
+
31
+ def order
32
+ @order
33
+ end
34
+
35
+ def == other
36
+ hash == other.hash
37
+ end
38
+
39
+ def eql? other
40
+ self == other
41
+ end
42
+
43
+ def attribute_name
44
+ attribute.respond_to?(:name) ? attribute.name : attribute
45
+ end
46
+
47
+ def hash
48
+ "#{collection}/#{attribute_name}:#{unique?}:#{order}".hash
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,11 @@
1
+ module Perpetuity
2
+ class NilQuery
3
+ def self.new
4
+ @instance ||= allocate
5
+ end
6
+
7
+ def to_db
8
+ {}
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,33 @@
1
+ require 'perpetuity/mongodb/query_attribute'
2
+ require 'perpetuity/mongodb/nil_query'
3
+
4
+ module Perpetuity
5
+ class MongoDB
6
+ class Query
7
+ attr_reader :query
8
+ def initialize &block
9
+ if block_given?
10
+ @query = block.call(self)
11
+ else
12
+ @query = NilQuery.new
13
+ end
14
+ end
15
+
16
+ def to_db
17
+ @query.to_db
18
+ end
19
+
20
+ def negate
21
+ @query.negate
22
+ end
23
+
24
+ def method_missing missing_method
25
+ QueryAttribute.new missing_method
26
+ end
27
+
28
+ def == other
29
+ query == other.query
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,66 @@
1
+ require 'perpetuity/mongodb/query_expression'
2
+
3
+ module Perpetuity
4
+ class MongoDB
5
+ class QueryAttribute
6
+ attr_reader :name
7
+
8
+ def initialize name
9
+ @name = name
10
+ end
11
+
12
+ def == value
13
+ QueryExpression.new self, :equals, value
14
+ end
15
+
16
+ def < value
17
+ QueryExpression.new self, :less_than, value
18
+ end
19
+
20
+ def >= value
21
+ QueryExpression.new self, :gte, value
22
+ end
23
+
24
+ def > value
25
+ QueryExpression.new self, :greater_than, value
26
+ end
27
+
28
+ def <= value
29
+ QueryExpression.new self, :lte, value
30
+ end
31
+
32
+ def != value
33
+ QueryExpression.new self, :not_equal, value
34
+ end
35
+ alias :not_equal? :'!='
36
+
37
+ def =~ regexp
38
+ QueryExpression.new self, :matches, regexp
39
+ end
40
+
41
+ def in collection
42
+ QueryExpression.new self, :in, collection
43
+ end
44
+
45
+ def to_sym
46
+ name
47
+ end
48
+
49
+ def to_db
50
+ ((self != false) & (self != nil)).to_db
51
+ end
52
+
53
+ def method_missing name
54
+ if name.to_s == 'id'
55
+ name = :"#{self.name}.__metadata__.#{name}"
56
+ elsif name.to_s == 'klass'
57
+ name = :"#{self.name}.__metadata__.class"
58
+ else
59
+ name = :"#{self.name}.#{name}"
60
+ end
61
+
62
+ self.class.new(name)
63
+ end
64
+ end
65
+ end
66
+ end