kongo 1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -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 kongo.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ The Azure License
2
+
3
+ Copyright (c) 2011 Kenneth Ballenegger
4
+
5
+ Attribute to Kenneth Ballenegger - http://kswizz.com/
6
+
7
+ You (the licensee) are hereby granted permission, free of charge, to deal in this software or source code (this "Software") without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sublicense this Software, subject to the following conditions:
8
+
9
+ You must give attribution to the party mentioned above, by name and by hyperlink, in the about box, credits document and/or documentation of any derivative work using a substantial portion of this Software.
10
+
11
+ You may not use the name of the copyright holder(s) to endorse or promote products derived from this Software without specific prior written permission.
12
+
13
+ THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THIS SOFTWARE OR THE USE OR OTHER DEALINGS IN THIS SOFTWARE.
14
+
15
+ http://license.azuretalon.com/
data/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # Kongo
2
+
3
+ Kongo is a lightweight and generic library for accessing data from Mongo.
4
+
5
+ ## Rationale
6
+
7
+ Kongo is not your typical ORM. Traditionally, according to MVC architecture best practices, you would create a model class to represent data from each collection in your application. However, while it is a good abstraction early on, as an application grows and scales, it is not uncommon to see models grow to thousands of lines of code, grouping together many different pieces of unrelated business logic. Not only that, but dependencies and tight coupling arises between the various related models.
8
+
9
+ Kongo takes a different approach. Kongo does not have models, it simply provides basic data-access functionality and object-oriented wrapping of the Mongo driver. It also provides support for extending collections and models with libraries. Kongo believes logic belongs in libraries, and related code belongs in the same file, not divided amongst multiple models and mixed with other logic.
10
+
11
+ ## Usage
12
+
13
+ Using Kongo is fairly straight-forward.
14
+
15
+ First, however, Kongo must know how to connect to Mongo (put this in a init / config type file):
16
+
17
+ ```ruby
18
+ db = Mongo::Connection.new(...)['dbname']
19
+ Kongo::Collection.fetch_collections_using do |collection_name|
20
+ # In larger applications you would insert logic here to figure out
21
+ # how to connect to Mongo, when you have multiple replicated or
22
+ # sharded clusters...
23
+ db[collection_name]
24
+ end
25
+ ```
26
+
27
+ Then, simply define your collections as you use them. (It's okay to do this in multiple contexts).
28
+
29
+ ```ruby
30
+ Posts = Kongo::Collection.new(:posts)
31
+ Comments = Kongo::Collection.new(:comments)
32
+ ```
33
+
34
+ You can use a collection to read data:
35
+
36
+ ```ruby
37
+ post = Posts.find_by_id(id)
38
+
39
+ # this returns a cursor
40
+ comments = Comments.find_many(post: post._id)
41
+
42
+ # get a json of the top ten comments
43
+ top = comments.sort(score: -1)
44
+ json = top.to_enum.take(10).map(:&to_hash).to_json
45
+ ```
46
+
47
+ All Kongo methods yield `Kongo::Model` objects whenever possible. These objects simply wrap around a `Hash` and provide some helper methods for dealing with the object.
48
+
49
+ Perhaps the most useful of these helpers is `update!`:
50
+
51
+ ```ruby
52
+ # Kongo encourages only performing atomic updates:
53
+
54
+ # simple setters change the data and record the appropriate delta
55
+ post.title = 'New title!'
56
+ post.date = Time.now.to_i
57
+ # more advanced deltas can be set explicitly. see mongo update syntax for documentation.
58
+ post.delta('$inc', edit_count: 1)
59
+
60
+ # the update! method commits deltas.
61
+ post.update!
62
+ ```
63
+
64
+ This is just the tip of the iceberg. See the documentation for the Kongo classes to see everything that Kongo can do.
65
+
66
+ ### Writing an extension
67
+
68
+ Imagine you have three models, `user`, `account`, and `transaction`. You want to make a library that will let you transfer money between users and their accounts. Typically you'd add code to your three existing model classes:
69
+
70
+ models/user.rb:
71
+
72
+ ```ruby
73
+ require 'models/account'
74
+
75
+ class User < Model
76
+
77
+ # a whole bunch of random crap
78
+ # ...
79
+
80
+ def earnings
81
+ Account::find_by_id(self['earnings_account'])
82
+ end
83
+ def spend
84
+ Account::find_by_id(sefl['spend_account'])
85
+ end
86
+ end
87
+ ```
88
+
89
+ models/account.rb:
90
+
91
+ ```ruby
92
+ require 'models/transaction'
93
+ class Account < Model
94
+ # more random crap
95
+ def deposit; ...; end
96
+
97
+ def transfer_to(other_account, amount)
98
+ if Transaction.create(self, other_account, amount})
99
+ self.update!('$inc', amount: (-1 * amount))
100
+ other_account('$inc', amount: amount)
101
+ end
102
+ end
103
+ ```
104
+
105
+ models/transaction.rb:
106
+
107
+ ```ruby
108
+ class Transaction < Model
109
+ def self.create(from, to, amount)
110
+ if from.balance >= amount
111
+ insert!({form: from['_id'], to: to['_id'], amount: amount})
112
+ true
113
+ else
114
+ false
115
+ end
116
+ end
117
+ end
118
+ ```
119
+
120
+ Now we have code related to the same thing in three different model files, and it's all mixed up with the other functionality of these models (such as analytics for transactions or authentication for the user model).
121
+
122
+ Instead, if we have the possibility of abstracting this into a library, we might have something much cleaner like a single `lib/finance.rb` file:
123
+
124
+ ```ruby
125
+ module Finance
126
+
127
+ # other finance functionality that does not belong directly to a
128
+ # model, eg something like inance::convert_currency
129
+ # our finance extensions to the models:
130
+
131
+ module Extensions
132
+ module User
133
+ def earnings
134
+ Account::find_by_id(self['earnings_account'])
135
+ end
136
+ def spend
137
+ Account::find_by_id(sefl['spend_account'])
138
+ end
139
+ end
140
+ Kongo::Model.add_extension(:users, User)
141
+
142
+ module Transactions
143
+ def create(from, to, amount)
144
+ if from.balance >= amount
145
+ insert!({form: from['_id'], to: to['_id'], amount: amount})
146
+ true
147
+ else
148
+ false
149
+ end
150
+ end
151
+ end
152
+ Kongo::Collection.add_extension(:transactions, Transactions)
153
+
154
+ TransactionsCollection = Kongo::Collection.new(:transactions)
155
+ module Account
156
+ def deposit; ...; end
157
+ def transfer_to(other_account, amount)
158
+ if TransactionsCollection.create(self, other_account, amount})
159
+ self.update!('$inc', amount: (-1 * amount))
160
+ other_account('$inc', amount: amount)
161
+ end
162
+ end
163
+ Kongo::Model.add_extension(:accounts, Account)
164
+
165
+ end
166
+
167
+ # The Account and Transaction "models" belong to the finance library,
168
+ # in that it is their primary function. So we provide constants for them:
169
+ Acounts = Kongo::Collection.new(:accounts)
170
+ Transactions = Kongo::Collection.new(:transactions)
171
+ end
172
+ ```
173
+
174
+ Using this extension is now straight-forward:
175
+
176
+ ```ruby
177
+ require 'lib/authentication' # imagine this lib provides user-related functionality
178
+ require 'lib/finance'
179
+ user = Authentication.current_user
180
+ kenneth = Authentication::Users.find_one(email: 'kenneth@ballenegger.com')
181
+ user.transfer_to(kenneth, 100)
182
+ ```
183
+
184
+ ## Installation
185
+
186
+ Add this line to your application's Gemfile:
187
+
188
+ gem 'kongo'
189
+
190
+ And then execute:
191
+
192
+ $ bundle
193
+
194
+ Or install it yourself as:
195
+
196
+ $ gem install kongo
197
+
198
+ ## Contributing
199
+
200
+ 1. Fork it
201
+ 2. Create your feature branch (```git checkout -b my-new-feature```)
202
+ 3. Commit your changes (```git commit -am 'Add some feature'```)
203
+ 4. Push to the branch (```git push origin my-new-feature```)
204
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/kongo.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'kongo/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'kongo'
8
+ gem.version = Kongo::VERSION
9
+ gem.authors = ['Kenneth Ballenegger']
10
+ gem.email = ['kenneth@ballenegger.com']
11
+ gem.description = %q{Kongo is a lightweight and generic library for accessing data from Mongo.}
12
+ gem.summary = %q{Kongo is a lightweight and generic library for accessing data from Mongo.}
13
+ gem.homepage = 'https://github.com/kballenegger/Kongo'
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ['lib']
19
+
20
+ gem.add_dependency('mongo', '>= 1.7.0')
21
+ end
data/lib/kongo.rb ADDED
@@ -0,0 +1,348 @@
1
+
2
+ require 'mongo'
3
+
4
+
5
+ module Kongo
6
+
7
+ class Collection
8
+
9
+ # Initialize with a collection name (as symbol), will use that collection
10
+ # name to generate the connection lazily when it is first used, thru the
11
+ # collection callback (*must* be defined).
12
+ #
13
+ def initialize(name)
14
+ @coll_name = name
15
+ @visible_ivars = []
16
+ @@extensions ||= {}
17
+
18
+ if @@extensions[name.to_sym]
19
+ @@extensions[name.to_sym].each do |const|
20
+ extend(const)
21
+ end
22
+ end
23
+ end
24
+
25
+ # `find_one` and `find_by_id` are the same method. When passed a native
26
+ # BSON::ObjectId, it will act as a find_by_id, otherwise, expects a regular
27
+ # query hash. Will return a Kongo::Model.
28
+ #
29
+ def find_one(*args)
30
+ (r = coll.find_one(*args)) ?
31
+ Model.new(r, coll) : r
32
+ end
33
+ alias :find_by_id :find_one
34
+
35
+ # `find`, aka. `find_many` returns a Kongo::Cursor wrapping the Mongo
36
+ # cursor.
37
+ #
38
+ def find(*args)
39
+ (c = coll.find(*args)).is_a?(::Mongo::Cursor) ?
40
+ Cursor.new(c, coll) : c
41
+ end
42
+ alias :find_many :find
43
+
44
+ # Count, just forwards to driver.
45
+ #
46
+ def count(*args)
47
+ coll.count(*args)
48
+ end
49
+
50
+ # Verify existence of record by id.
51
+ #
52
+ def has?(id)
53
+ count(:query => {_id: id}) == 1
54
+ end
55
+
56
+ # Insert a record, returns a Model object.
57
+ #
58
+ def insert!(hash)
59
+ coll.insert(hash)
60
+ Model.new(hash, coll)
61
+ end
62
+
63
+ # Collection#extend adds the option of extending with a symbol, which
64
+ # will automatically use that constant from Extensions::Collections,
65
+ # calls super in every other case.
66
+ #
67
+ def extend(arg)
68
+ super(arg.is_a?(Symbol) ? Extensions::Collections.const_get(arg) : arg)
69
+ end
70
+
71
+
72
+ # This method must be called statically on the ORM once before being used,
73
+ # so that the ORM knows how to connect to mongo and fetch collections.
74
+ #
75
+ # A simple usage example might look like this:
76
+ #
77
+ # class MongoCollectionFetcher
78
+ # def self.fetch(name)
79
+ # unless @mongo
80
+ # @mongo = Mongo::Connection.new
81
+ # end
82
+ # return @mongo['database'][name.to_s]
83
+ # end
84
+ # end
85
+ #
86
+ # Kongo::Collection.fetch_collections_using do |n|
87
+ # MongoCollectionFetcher.fetch(n)
88
+ # end
89
+ #
90
+ def self.fetch_collections_using(&block)
91
+ @@collection_fetcher = block
92
+ end
93
+
94
+
95
+ # This method returns the Mongo::Collection object for this collection.
96
+ #
97
+ def coll
98
+ return @coll if @coll
99
+
100
+ raise 'Kongo has not been initialized with a collection fetcher.' unless @@collection_fetcher
101
+ @coll = @@collection_fetcher.call(@coll_name)
102
+ @coll
103
+ end
104
+
105
+
106
+
107
+ # Inspecting a Mongo Model attempts to only show *useful* information,
108
+ # such as what its extensions are as well as certain ivars.
109
+ #
110
+ def inspect
111
+ proxy = Object.new
112
+ @visible_ivars.each do |ivar|
113
+ val = instance_variable_get(ivar)
114
+ proxy.instance_variable_set(ivar, val) unless val.nil?
115
+ end
116
+ string = proxy.inspect
117
+ ext_info = @extensions ? '(+ '+@extensions.join(', ')+')' : ''
118
+ string.gsub(/Object:0x[0-9a-f]+/, "Kongo::Collection#{ext_info}:0x#{object_id}")
119
+ end
120
+
121
+
122
+ # Declare extensions using this method:
123
+ #
124
+ # Kongo::Collection.add_extension(:collection_name, module)
125
+ #
126
+ def self.add_extension(collection_name, mod)
127
+ ((@@extensions ||= {})[collection_name.to_sym] ||= []) << mod
128
+ end
129
+
130
+ # This method just adds the extension to the list of extension, for the
131
+ # sake of inspect, and call super:
132
+ #
133
+ def extend(const)
134
+ (@extensions ||= []) << const.to_s
135
+ super
136
+ end
137
+
138
+ end
139
+
140
+
141
+ # Cursor is an object that wraps around a Mongo::Cursor, wrapping objects it
142
+ # returns in Kongo::Model objects.
143
+ #
144
+ class Cursor
145
+
146
+ # `initialize` is typically only used internally by Kongo::Collection when
147
+ # it returns cursors.
148
+ #
149
+ def initialize(cursor, coll)
150
+ @coll = coll
151
+ @cursor = cursor
152
+ end
153
+
154
+ # Any method is forwarded to its wrapped cursor.
155
+ #
156
+ def method_missing(*args, &block)
157
+ @cursor.send(*args, &block)
158
+ end
159
+
160
+ # `next` wraps responses in Kongo::Model.
161
+ #
162
+ def next
163
+ (e = @cursor.next).is_a?(Hash) ?
164
+ Model.new(e, @coll) : e
165
+ end
166
+
167
+ # `each` yields Kongo::Model objects.
168
+ #
169
+ def each
170
+ @cursor.each { |e| yield Model.new(e, @coll) }
171
+ end
172
+
173
+ # `to_enum` returns an Enumerator which yields the results of the cursor.
174
+ #
175
+ def to_enum
176
+ Enumerator.new do |yielder|
177
+ while @cursor.has_next?
178
+ yielder.yield(self.next)
179
+ end
180
+ end
181
+ end
182
+
183
+ # `to_a` returns an array of Kongo::Model.
184
+ #
185
+ def to_a
186
+ arr = []
187
+ each { |e| arr << e }
188
+ arr
189
+ end
190
+ end
191
+
192
+
193
+ # Kongo::Model is the most important class of Kongo, it wraps around hashes
194
+ # representing Mongo records, provides a collection of useful methods, and
195
+ # allows itself to be extended by third party libraries.
196
+ #
197
+ class Model
198
+
199
+ # Typically, you would not call Model#new directly, but rather get
200
+ # a model from a method on Collections, such as a finder or #insert.
201
+ #
202
+ def initialize(hash, coll)
203
+ @coll = coll
204
+ @hash = hash
205
+ @deltas = {}
206
+ @visible_ivars = [:@hash, :@deltas]
207
+ @@extensions ||= {}
208
+
209
+ if @@extensions[coll.name.to_sym]
210
+ @@extensions[coll.name.to_sym].each do |const|
211
+ extend(const)
212
+ end
213
+ end
214
+ end
215
+
216
+ attr_reader :hash
217
+ attr_reader :deltas
218
+
219
+ # Record fields can be accessed using [] syntax.
220
+ #
221
+ def [](k); @hash[k]; end
222
+
223
+ # This adds to the list of deltas, so that we may update with no
224
+ # arguments below
225
+ #
226
+ def []=(k,v)
227
+ @hash[k.to_s]=v
228
+
229
+ delta('$set', k => v)
230
+ end
231
+
232
+ # Add a delta
233
+ #
234
+ # delta '$inc',
235
+ # total: 3,
236
+ # unique: 1
237
+ #
238
+ def delta(type, fields = {})
239
+ fields.each do |k,v|
240
+ @deltas[type.to_s] ||= {}
241
+ @deltas[type.to_s][k.to_s] = v
242
+ end
243
+ self
244
+ end
245
+
246
+ # `unset` lets you remove a key from the hash, as well as adding it to
247
+ # the deltas so that the next update will unset that key in Mongo.
248
+ #
249
+ def unset(key)
250
+ @hash.delete(key.to_s)
251
+ delta('$unset', key => 1)
252
+ end
253
+
254
+ # This method_missing provides accessors for keys as proprieties of the
255
+ # model object, so you may do:
256
+ #
257
+ # object.key = :value
258
+ # object.key #=> :value
259
+ #
260
+ def method_missing(key, *args, &block)
261
+ key = key.to_s
262
+ if matches = /^(.+)=$/.match(key)
263
+ raise ArgumentError.new 'Unexpected argument count.' if args.count != 1
264
+ self[matches[1]] = args.first
265
+ else
266
+ raise ArgumentError.new 'Unexpected argument count.' if args.count != 0
267
+ return self[key]
268
+ end
269
+ end
270
+
271
+ # @deprecated
272
+ # Do not use saves, they're dirty.
273
+ #
274
+ def save!(options = {})
275
+ warn("#{Kernel.caller.first}: `save` is deprecated, use `update` instead.")
276
+ raise if @stale unless options[:ignore_stale] # TODO: custom exception
277
+ @coll.save(@hash, :safe => true)
278
+ end
279
+
280
+ # Issues an update on the database, for this record, with the provided
281
+ # deltas. WARNING: the record will become stale, and should no longer be
282
+ # saved after an update has been issued.
283
+ #
284
+ def update!(deltas = {})
285
+ return if @deltas.empty?
286
+
287
+ id = @hash['_id']
288
+ raise unless id # TODO: custom exception
289
+
290
+ if @deltas
291
+ deltas = @deltas.merge(deltas)
292
+ @deltas = {}
293
+ end
294
+
295
+ @stale = true
296
+
297
+ @coll.update({_id: id}, deltas, :safe => true)
298
+ end
299
+
300
+ # Deletes this record from the database.
301
+ #
302
+ def delete!
303
+ id = @hash['_id']
304
+ raise unless id # TODO: custom exception
305
+ @coll.remove({_id: id})
306
+ end
307
+
308
+ # Returns the hash of the record itself.
309
+ #
310
+ def to_hash
311
+ @hash
312
+ end
313
+
314
+ # Inspecting a Mongo Model attempts to only show *useful* information,
315
+ # such as what its extensions are as well as certain ivars.
316
+ #
317
+ def inspect
318
+ proxy = Object.new
319
+ @visible_ivars.each do |ivar|
320
+ val = instance_variable_get(ivar)
321
+ proxy.instance_variable_set(ivar, val) unless val.nil?
322
+ end
323
+ string = proxy.inspect
324
+ ext_info = @extensions ? '(+ '+@extensions.join(', ')+')' : ''
325
+ string.gsub(/Object:0x[0-9a-f]+/, "Kongo::Model#{ext_info}:0x#{object_id}")
326
+ end
327
+
328
+
329
+
330
+ # Declare extensions using this method:
331
+ #
332
+ # Kongo::Model.add_extension(:collection_name, module)
333
+ #
334
+ def self.add_extension(collection_name, mod)
335
+ ((@@extensions ||= {})[collection_name.to_sym] ||= []) << mod
336
+ end
337
+
338
+ # This method just adds the extension to the list of extension, for the
339
+ # sake of inspect, and call super:
340
+ #
341
+ def extend(const)
342
+ (@extensions ||= []) << const.to_s
343
+ super
344
+ end
345
+
346
+ end
347
+
348
+ end
@@ -0,0 +1,3 @@
1
+ module Kongo
2
+ VERSION = '1.0'
3
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kongo
3
+ version: !ruby/object:Gem::Version
4
+ version: '1.0'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kenneth Ballenegger
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mongo
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.7.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.7.0
30
+ description: Kongo is a lightweight and generic library for accessing data from Mongo.
31
+ email:
32
+ - kenneth@ballenegger.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - .gitignore
38
+ - Gemfile
39
+ - LICENSE
40
+ - README.md
41
+ - Rakefile
42
+ - kongo.gemspec
43
+ - lib/kongo.rb
44
+ - lib/kongo/version.rb
45
+ homepage: https://github.com/kballenegger/Kongo
46
+ licenses: []
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubyforge_project:
65
+ rubygems_version: 1.8.24
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: Kongo is a lightweight and generic library for accessing data from Mongo.
69
+ test_files: []
70
+ has_rdoc: