perpetuity-mongodb 1.0.0.beta

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