toughguy 0.1.0

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: fd8b128c7302c3b25ec0274ce6fea18599e8acd2
4
+ data.tar.gz: bb2a8e05fee133b6dd322c5fd9cdc3140857a7ed
5
+ SHA512:
6
+ metadata.gz: a578bbc736a87c373b440fd32750d03791e5fcad33b69f6331602d3a4cb1ac9c31473bea8f2d22dfdf7812a71dd4a29b7a280a531e52c138684ef4d9f8a6bf2d
7
+ data.tar.gz: cb5f2f74d06f4c4a157cee9aba307b2f6516855611f4541e0e95a1836bb210486bc73e4e7504cb9f0977497900781deb3d91cf391be2206d34e42347a08b77d8
@@ -0,0 +1,34 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+
12
+ ## Specific to RubyMotion:
13
+ .dat*
14
+ .repl_history
15
+ build/
16
+
17
+ ## Documentation cache and generated files:
18
+ /.yardoc/
19
+ /_yardoc/
20
+ /doc/
21
+ /rdoc/
22
+
23
+ ## Environment normalisation:
24
+ /.bundle/
25
+ /lib/bundler/man/
26
+
27
+ # for a library or gem, you might want to ignore these files since the code is
28
+ # intended to run in multiple environments; otherwise, check them in:
29
+ # Gemfile.lock
30
+ # .ruby-version
31
+ # .ruby-gemset
32
+
33
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
34
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem "mongo", "1.11.1"
5
+ gem "bson_ext", "1.11.1"
6
+ gem "plucky", "0.6.6"
7
+ gem "assistance", "0.1.5"
8
+ gem "rake"
9
+
10
+ group(:test) do
11
+ gem 'rspec'
12
+ end
@@ -0,0 +1,46 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ toughguy (0.1.0)
5
+ assistance (= 0.1.5)
6
+ bson_ext (= 1.11.1)
7
+ mongo (= 1.11.1)
8
+ plucky (= 0.6.6)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ assistance (0.1.5)
14
+ bson (1.11.1)
15
+ bson_ext (1.11.1)
16
+ bson (~> 1.11.1)
17
+ diff-lcs (1.2.5)
18
+ mongo (1.11.1)
19
+ bson (= 1.11.1)
20
+ plucky (0.6.6)
21
+ mongo (~> 1.5)
22
+ rake (10.4.2)
23
+ rspec (3.1.0)
24
+ rspec-core (~> 3.1.0)
25
+ rspec-expectations (~> 3.1.0)
26
+ rspec-mocks (~> 3.1.0)
27
+ rspec-core (3.1.7)
28
+ rspec-support (~> 3.1.0)
29
+ rspec-expectations (3.1.2)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.1.0)
32
+ rspec-mocks (3.1.3)
33
+ rspec-support (~> 3.1.0)
34
+ rspec-support (3.1.2)
35
+
36
+ PLATFORMS
37
+ ruby
38
+
39
+ DEPENDENCIES
40
+ assistance (= 0.1.5)
41
+ bson_ext (= 1.11.1)
42
+ mongo (= 1.11.1)
43
+ plucky (= 0.6.6)
44
+ rake
45
+ rspec
46
+ toughguy!
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Sharon Rosner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
File without changes
@@ -0,0 +1,4 @@
1
+ require 'rspec/core/rake_task'
2
+ RSpec::Core::RakeTask.new
3
+
4
+ task :default => :spec
@@ -0,0 +1,12 @@
1
+ require 'mongo'
2
+ require 'plucky'
3
+ require 'assistance'
4
+
5
+ require 'toughguy/ext'
6
+ require 'toughguy/model'
7
+ require 'toughguy/query'
8
+ require 'toughguy/singleton_model'
9
+ require 'toughguy/mapped_query'
10
+ require 'toughguy/mapped_model'
11
+
12
+
@@ -0,0 +1,75 @@
1
+ class Plucky::Normalizers::CriteriaHashValue
2
+ # add support for specifying ranges
3
+ def call(parent_key, key, value)
4
+ case value
5
+ when Array, Set
6
+ if object_id?(parent_key)
7
+ value = value.map { |v| to_object_id(v) }
8
+ end
9
+
10
+ if nesting_operator?(key)
11
+ value.map { |v| criteria_hash_class.new(v, options).to_hash }
12
+ elsif parent_key == key && !modifier?(key) && !value.empty?
13
+ # we're not nested and not the value for a symbol operator
14
+ {:$in => value.to_a}
15
+ else
16
+ # we are a value for a symbol operator or nested hash
17
+ value.to_a
18
+ end
19
+ when Time
20
+ value.utc
21
+ when String
22
+ if object_id?(key)
23
+ return to_object_id(value)
24
+ end
25
+ value
26
+ when Range
27
+ v1 = value.begin; v2 = value.end
28
+ if (parent_key == :_id) && v1.is_a?(Time) && v2.is_a?(Time)
29
+ v1 = BSON::ObjectId.from_time(v1)
30
+ v2 = BSON::ObjectId.from_time(v2)
31
+ end
32
+ {'$gte' => v1, (value.exclude_end? ? '$lt' : '$lte') => v2}
33
+ when Hash
34
+ value.each { |k, v| value[k] = call(key, k, v) }
35
+ value
36
+ when Regexp
37
+ Regexp.new(value)
38
+ else
39
+ value
40
+ end
41
+ end
42
+ end
43
+
44
+ class Symbol
45
+ # return descending order hash
46
+ def desc
47
+ [self, -1]
48
+ end
49
+ end
50
+
51
+ class Mongo::DB
52
+ def stats
53
+ command({dbstats: 1}).symbolize_keys_with_underscore
54
+ end
55
+ end
56
+
57
+ # Required for regexp to work in plucky 0.3.2. I don't really see the point in
58
+ # this. See: plucky-0.3.2/lib/plucky/criteria_hash.rb:16
59
+ class Regexp
60
+ def duplicable?
61
+ false
62
+ end
63
+ end
64
+
65
+ # Hash extensions
66
+ class Hash
67
+ def symbolize_keys
68
+ inject({}) {|m, kv| v = kv[1]; m[kv[0].to_sym] = v.is_a?(Hash) ? v.symbolize_keys : v; m}
69
+ end
70
+
71
+ def symbolize_keys_with_underscore
72
+ inject({}) {|m, kv| v = kv[1]; m[kv[0].underscore.to_sym] = v.is_a?(Hash) ? v.symbolize_keys : v; m}
73
+ end
74
+ end
75
+
@@ -0,0 +1,119 @@
1
+ require 'toughguy/mapped_query'
2
+
3
+ module ToughGuy
4
+ class MappedModel < Model
5
+ def delete
6
+ model.delete(_id: id)
7
+ model.remove_map_ref(self)
8
+ end
9
+
10
+ def set(hash)
11
+ collection.update({_id: id}, '$set' => hash)
12
+ @values.merge!(hash)
13
+ end
14
+
15
+ def exists?
16
+ model.q.filter(_id: id).count == 1
17
+ end
18
+
19
+ class << self
20
+ def map_key
21
+ @map_key
22
+ end
23
+
24
+ def set_map_key(k)
25
+ @map = {}
26
+ @map_key = k
27
+ end
28
+
29
+ def inherited(m)
30
+ super
31
+ # the map key is inherited, the map itself is not.
32
+ m.set_map_key(@map_key)
33
+ end
34
+
35
+ def map
36
+ @map
37
+ end
38
+
39
+ def instances
40
+ @map.values
41
+ end
42
+
43
+ def enumerate
44
+ @map.each
45
+ end
46
+
47
+ def load_map(opts = {})
48
+ query({}).sort(@map_key).each {}
49
+ end
50
+
51
+ def load_map_async(&block)
52
+ @async_start = Time.now
53
+ EM.next_tick {load_next_map_chunk(1, &block)}
54
+ end
55
+
56
+ def load_next_map_chunk(page, &block)
57
+ query.paginate(page: page, per_page: 100).each {}.tap do |q|
58
+ if page < q.total_pages
59
+ EM.timeout(0.1) {load_next_map_chunk(page + 1, &block)}
60
+ else
61
+ block.call if block
62
+ end
63
+ end
64
+ end
65
+
66
+ def all(load=false)
67
+ load_map if load
68
+ @map.values
69
+ end
70
+
71
+ def reset_map
72
+ @map = {}
73
+ end
74
+
75
+ def remove_map_ref(o)
76
+ @map.delete(o[@map_key])
77
+ end
78
+
79
+ def set_map_ref(o)
80
+ if @map[o[@map_key]]
81
+ raise "Map reference already set for #{self}[#{o[@map_key].inspect}]"
82
+ end
83
+ @map[o[@map_key]] = o
84
+ end
85
+
86
+ def [](k)
87
+ @map[k] || first(@map_key => k)
88
+ end
89
+
90
+ alias_method :find, :[]
91
+
92
+ def delete_all
93
+ super
94
+ reset_map
95
+ end
96
+
97
+ def destroy_all
98
+ instances.each {|o| o.destroy}
99
+ reset_map
100
+ end
101
+
102
+ def load(values)
103
+ new(values).tap {|o| o.after_load; set_map_ref(o)}
104
+ end
105
+
106
+ def create(values = {})
107
+ super.tap {|o| set_map_ref(o)}
108
+ end
109
+
110
+ def query(opts = {})
111
+ MappedQuery.new(self).where(opts)
112
+ end
113
+
114
+ alias_method :q, :query
115
+ alias_method :where, :query
116
+ alias_method :filter, :query
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,40 @@
1
+ module ToughGuy
2
+ class MappedQuery < Query
3
+ def initialize(klass, opts = {})
4
+ super(klass, opts)
5
+ @map = klass.map
6
+ @map_key = klass.map_key
7
+ @map_key_s = @map_key.to_s
8
+ end
9
+
10
+ def all(opts={})
11
+ a = []
12
+ each {|o| a << o}
13
+ a
14
+ end
15
+
16
+ alias_method :to_a, :all
17
+
18
+ def each(opts={}, &block)
19
+ find_each(opts).each do |d|
20
+ if o = @map[d[@map_key_s]]
21
+ block.call(o)
22
+ else
23
+ block.call(@klass.load(d))
24
+ end
25
+ end
26
+ self
27
+ end
28
+
29
+ def find_one(opts={})
30
+ if (d = orig_find_one(opts))
31
+ @map[d[@map_key_s]] || @klass.load(d)
32
+ else
33
+ nil
34
+ end
35
+ end
36
+
37
+ # override method aliasing in Plucky::Query
38
+ alias_method :first, :find_one
39
+ end
40
+ end
@@ -0,0 +1,339 @@
1
+ module ToughGuy
2
+ class Model
3
+ attr_accessor :values, :collection
4
+
5
+ def initialize(values = {}, collection = nil)
6
+ @values = values.symbolize_keys
7
+ @collection = collection
8
+ end
9
+
10
+ def after_load
11
+ end
12
+
13
+ def method_missing(m, *args)
14
+ if m.to_s =~ /^(.+)=$/
15
+ @values[$1.to_sym] = args[0]
16
+ else
17
+ @values[m]
18
+ end
19
+ end
20
+
21
+ def collection
22
+ @collection ||= model.collection
23
+ end
24
+
25
+ def database
26
+ @database ||= collection.db
27
+ end
28
+
29
+ alias_method :db, :database
30
+
31
+ def model
32
+ self.class
33
+ end
34
+
35
+ def [](k)
36
+ @values[k]
37
+ end
38
+
39
+ def []=(k, v)
40
+ @values[k] = v
41
+ end
42
+
43
+ def id
44
+ @values[:_id]
45
+ end
46
+
47
+ def new?
48
+ !@values[:_id]
49
+ end
50
+
51
+ def before_create
52
+ end
53
+
54
+ def after_create
55
+ end
56
+
57
+ def save
58
+ if new?
59
+ before_create
60
+ values[:_id] = collection.insert(values)
61
+ after_create
62
+ else
63
+ collection.update({_id: id}, values.exclude(:_id))
64
+ end
65
+ end
66
+
67
+ def update(hash)
68
+ collection.update({_id: id}, hash)
69
+ end
70
+
71
+ def set(hash)
72
+ update('$set' => hash)
73
+ @values.merge!(hash.symbolize_keys)
74
+ end
75
+
76
+ def refresh
77
+ @values = collection.find(_id: id).first.symbolize_keys
78
+ self
79
+ end
80
+
81
+ def delete
82
+ model.delete(_id: id)
83
+ end
84
+
85
+ def before_destroy
86
+ end
87
+
88
+ def after_destroy
89
+ end
90
+
91
+ def destroyed?
92
+ @destroyed
93
+ end
94
+
95
+ def destroy
96
+ before_destroy
97
+ @destroyed = true
98
+ delete
99
+ after_destroy
100
+ end
101
+
102
+ def to_s
103
+ "#<#{model.name}:#{id || 'unsaved'}>"
104
+ end
105
+
106
+ def inspect
107
+ v = values.exclude(:_id).map {|k, v| "#{k}: #{v.inspect}"}.join(', ')
108
+ "#<#{model.name}:#{id || 'unsaved'} #{v}>"
109
+ end
110
+
111
+ def ==(obj)
112
+ (obj.class == model) && (obj.values == @values)
113
+ end
114
+
115
+ def exists?
116
+ model.find(id) != nil
117
+ end
118
+
119
+ class << self
120
+ def add_index(spec, opts={})
121
+ @indexes ||= []
122
+ @indexes << [spec, opts]
123
+ end
124
+
125
+ def ensure_index(spec, opts={})
126
+ add_index(spec, opts)
127
+ collection.create_index(spec, opts)
128
+ end
129
+
130
+ def recreate_indexes
131
+ if @indexes
132
+ @indexes.each {|idx| ensure_index(*idx)}
133
+ end
134
+ end
135
+
136
+ def load(values)
137
+ new(values).tap {|o| o.after_load}
138
+ end
139
+
140
+ def create(values = {})
141
+ o = new(values)
142
+ o.save
143
+ o
144
+ end
145
+
146
+ def [](opts)
147
+ unless opts.is_a?(Hash)
148
+ opts = {_id: opts}
149
+ end
150
+ first(opts)
151
+ end
152
+
153
+ alias_method :find, :[]
154
+
155
+ def query(opts = {})
156
+ ToughGuy::Query.new(self).where(opts)
157
+ end
158
+
159
+ alias_method :q, :query
160
+ alias_method :where, :query
161
+ alias_method :filter, :query
162
+
163
+ def sort(*args)
164
+ query.sort(*args)
165
+ end
166
+
167
+ def limit(l, skip = 0)
168
+ query.limit(l).skip(0)
169
+ end
170
+
171
+ def paginate(opts)
172
+ query.paginate(opts)
173
+ end
174
+
175
+ def count(opts = {})
176
+ query(opts).count
177
+ end
178
+
179
+ def all(opts = {})
180
+ query(opts).to_a
181
+ end
182
+
183
+ def each(*args, &block)
184
+ query.each(*args, &block)
185
+ end
186
+
187
+ def map(key = nil, &block)
188
+ query.map(key, &block)
189
+ end
190
+
191
+ def sort(*args)
192
+ query.sort(*args)
193
+ end
194
+
195
+ def first(opts = {})
196
+ query(opts).first
197
+ end
198
+
199
+ def map_keys(hash)
200
+ hash.each do |key, short_key|
201
+ define_method(key) {@values[short_key]}
202
+ define_method(:"#{key}=") {|v| @values[short_key] = v}
203
+ end
204
+ end
205
+
206
+ def collection
207
+ @collection ||= database[collection_name]
208
+ end
209
+
210
+ def set_collection(c)
211
+ @collection = c
212
+ end
213
+
214
+ def to_table_name
215
+ parts = name.downcase.split('::')
216
+ parts.push(parts.pop.pluralize)
217
+ parts.join('.')
218
+ end
219
+
220
+ def collection_name
221
+ @collection_name ||= to_table_name
222
+ end
223
+
224
+ def set_collection_name(n)
225
+ @collection_name = n
226
+ end
227
+
228
+ def delete_all
229
+ collection.remove
230
+ end
231
+
232
+ def delete(selector)
233
+ q(selector).remove
234
+ end
235
+
236
+ def destroy_all
237
+ q.each {|d| d.destroy}
238
+ end
239
+
240
+ def drop
241
+ collection.drop
242
+ end
243
+
244
+ def update(*args)
245
+ collection.update(*args)
246
+ end
247
+
248
+ def stats
249
+ collection.stats
250
+ end
251
+
252
+ @database = nil
253
+
254
+ def database
255
+ @database
256
+ end
257
+
258
+ alias_method :db, :database
259
+
260
+ @@db_map = {}
261
+
262
+ def database=(db)
263
+ if db.is_a?(String)
264
+ db = (@@db_map[db] = connection[db])
265
+ end
266
+ @database = db
267
+ # update also for mapped model
268
+ if self == ToughGuy::Model
269
+ ToughGuy::MappedModel.database = db
270
+ ToughGuy::SingletonModel.database = db
271
+ end
272
+ end
273
+
274
+ def db_key(db)
275
+ db.is_a?(String) ? db : db.name
276
+ end
277
+
278
+ def db_class_name(db_key)
279
+ name = (db_key =~ /_(.+)/) && $1.gsub(/[^a-z0-9]/, '').capitalize
280
+ "P_#{name}"
281
+ end
282
+
283
+ def subclass_with_db(db, db_key)
284
+ c = Class.new(self).tap do |klass|
285
+ klass.database = db
286
+ klass.set_collection_name(collection_name)
287
+ if @indexes
288
+ @indexes.each {|idx| klass.ensure_index(*idx)}
289
+ end
290
+ end
291
+
292
+ # create proper class name
293
+ class_name = db_class_name(db_key)
294
+ if class_name
295
+ # remove class const before defining it in order to prevent warning
296
+ send(:remove_const, class_name) if const_defined?(class_name, false)
297
+ const_set(class_name, c)
298
+ end
299
+
300
+ c
301
+ end
302
+
303
+ def with_db(db)
304
+ key = db_key(db)
305
+ return self if key == db_key(self.db)
306
+ @subclass_map ||= {}
307
+ @subclass_map[key] ||= subclass_with_db(db, key)
308
+ end
309
+
310
+ def remove_class_with_db(db)
311
+ @subclass_map ||= {}
312
+ key = db_key(db)
313
+ class_name = db_class_name(key)
314
+ c = @subclass_map.delete(key)
315
+ if class_name
316
+ send(:remove_const, class_name) if const_defined?(class_name, false)
317
+ end
318
+ end
319
+
320
+ def inherited(subclass)
321
+ subclass.database = database
322
+ end
323
+
324
+ def mapped_db_classes
325
+ (@subclass_map || {}).values.unshift(self)
326
+ end
327
+
328
+ @@connection = nil
329
+
330
+ def connection
331
+ @@connection
332
+ end
333
+
334
+ def connection=(c)
335
+ @@connection = c
336
+ end
337
+ end
338
+ end
339
+ end
@@ -0,0 +1,196 @@
1
+ module ToughGuy
2
+ class Query < Plucky::Query
3
+ attr_accessor :klass
4
+
5
+ def initialize(klass, opts = {})
6
+ super(klass.collection, opts)
7
+ @klass = klass
8
+ end
9
+
10
+ # Allow self-changing methods: where!, sort! etc.
11
+ def method_missing(m, *args)
12
+ if m.to_s =~ /^(.+)!$/
13
+ send($1.to_sym, *args).tap {|q| @criteria, @options = q.criteria, q.options}
14
+ self
15
+ else
16
+ super(m, *args)
17
+ end
18
+ end
19
+
20
+ def explain(opts={})
21
+ find_each(opts).explain
22
+ end
23
+
24
+ alias_method :filter, :where
25
+
26
+ alias_method :orig_all, :all
27
+
28
+ def all(opts={})
29
+ if @klass
30
+ orig_all(opts).map {|d| @klass.load(d)}
31
+ else
32
+ orig_all(opts)
33
+ end
34
+ end
35
+
36
+ alias_method :to_a, :all
37
+
38
+ def each(opts={}, &block)
39
+ if @klass
40
+ find_each(opts).each {|d| block[@klass.load(d)]}
41
+ else
42
+ find_each(opts).each(&block)
43
+ end
44
+ self
45
+ end
46
+
47
+ alias_method :orig_find_one, :find_one
48
+
49
+ def find_one(opts={})
50
+ (doc = orig_find_one(opts)) ? (@klass ? @klass.load(doc) : doc) : nil
51
+ end
52
+
53
+ # override method aliasing in Plucky::Query
54
+ alias_method :first, :find_one
55
+
56
+ # Extend map to accept key argument, allowing stuff like:
57
+ # map(:path) # equivalent to map {|d| d[:path]}
58
+ def map(key = nil, &block)
59
+ m = []
60
+ key ? each {|d| m << d[key]} : each {|d| m << block[d]}
61
+ m
62
+ end
63
+
64
+ # Return an array of object ids.
65
+ def map_id
66
+ map(:_id)
67
+ end
68
+
69
+ def group(key, initial, reduce, finalize = nil)
70
+ key = [key] unless key.is_a?(Array)
71
+ collection.group(key.map {|k| k.to_s}, criteria.to_hash,
72
+ initial, reduce, finalize)
73
+ end
74
+
75
+ def group_and_count(key)
76
+ group(key, {'sum' => 0}, "function(doc, prev) { prev.sum += 1}")
77
+ end
78
+
79
+ def group_and_avg(group, key)
80
+ group(group, {'count' => 0, 'sum' => 0},
81
+ "function(doc, out) {out.count++; out.sum += doc.#{key}}",
82
+ "function(out){ if (out.count > 0) out.avg = out.sum / out.count }"
83
+ ).map {|g| g.symbolize_keys}.sort_by {|g| -g[:avg]}
84
+ end
85
+
86
+ def min(key)
87
+ d = sort(key).first
88
+ d ? d[key] : nil
89
+ end
90
+
91
+ def max(key)
92
+ d = sort(key.desc).first
93
+ d ? d[key] : nil
94
+ end
95
+
96
+ def avg(key)
97
+ values = raw.select(key).map(key.to_s)
98
+ values.inject(0) {|m, i| m += i} / values.size.to_f
99
+ end
100
+
101
+ def insert_rate(opts={})
102
+ opts[:limit] ||= 100
103
+ opts[:key] ||= :_id
104
+ docs = raw.limit(opts[:limit]).sort(:_id.desc).select(opts[:key]).all
105
+ if docs.empty?
106
+ nil
107
+ else
108
+ t1, t2 = docs.first[opts[:key].to_s], docs.last[opts[:key].to_s]
109
+ if opts[:key] == :_id
110
+ t1, t2 = t1.generation_time, t2.generation_time
111
+ end
112
+ docs.size.to_f / (t1 - t2)
113
+ end
114
+ end
115
+
116
+ alias_method :orig_sort, :sort
117
+
118
+ # Extend the sort API to allow stuff like:
119
+ # sort(:kind, :stamp)
120
+ # sort(:kind, :stamp.desc) # see Symbol#desc below
121
+ def sort(*args)
122
+ if args.all? {|a| a.is_a?(Symbol) || a.is_a?(Array)}
123
+ orig_sort(args.map {|a| a.is_a?(Symbol) ? [a, 1] : a})
124
+ else
125
+ orig_sort(*args)
126
+ end
127
+ end
128
+
129
+ # Add select method to select the fields to return
130
+ def select(fields)
131
+ if (fields == []) || (fields.nil?)
132
+ fields = [:_id]
133
+ end
134
+ clone.tap {|q| q.options[:fields] = fields}
135
+ end
136
+
137
+ # Returns a query that does not converted docs to model instances
138
+ def raw
139
+ @klass ? clone.tap {|q| q.klass = nil} : self
140
+ end
141
+
142
+ def update_values(doc, opts={})
143
+ collection.update(criteria.to_hash, doc, opts)
144
+ end
145
+
146
+ def paginate(opts)
147
+ per_page = opts[:per_page] || 50
148
+ page = opts[:page] || 1
149
+
150
+ paginated = limit(per_page).skip((page - 1) * per_page)
151
+ paginated.set_pagination_info(page, per_page, count)
152
+ paginated
153
+ end
154
+
155
+ # Sets the pagination info
156
+ def set_pagination_info(page_no, page_size, record_count)
157
+ @current_page = page_no
158
+ @per_page = page_size
159
+ @total_count = record_count
160
+ @total_pages = (record_count / page_size.to_f).ceil
161
+
162
+ extend PaginationMethods
163
+
164
+ self
165
+ end
166
+
167
+ module PaginationMethods
168
+ attr_accessor :per_page, :total_pages, :current_page, :total_count
169
+
170
+ # Returns the previous page number or nil if the current page is the first
171
+ def prev_page
172
+ @current_page > 1 ? (@current_page - 1) : nil
173
+ end
174
+
175
+ # Returns the next page number or nil if the current page is the last page
176
+ def next_page
177
+ @current_page < @total_pages ? (@current_page + 1) : nil
178
+ end
179
+
180
+ # Returns the page range
181
+ def page_range
182
+ 1..@total_pages
183
+ end
184
+
185
+ # Returns the record range for the current page
186
+ def current_page_record_range
187
+ return (0..0) if @current_page > @total_pages
188
+
189
+ a = 1 + (@current_page - 1) * @per_page
190
+ b = a + @per_page - 1
191
+ b = @total_count if b > @total_count
192
+ a..b
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,31 @@
1
+ module ToughGuy
2
+ class SingletonModel < Model
3
+ def self.instance
4
+ @instance ||= (first || create)
5
+ end
6
+
7
+ def self.resident_instance
8
+ @instance
9
+ end
10
+
11
+ def self.all_resident_instances
12
+ list = @subclass_map ?
13
+ @subclass_map.values.map {|c| c.resident_instance}.compact : []
14
+ @instance ? (list << resident_instance) : list
15
+ end
16
+
17
+ def self.reset
18
+ @instance = nil
19
+ end
20
+
21
+ def self.delete_all
22
+ super
23
+ reset
24
+ end
25
+
26
+ def delete
27
+ super
28
+ model.reset
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,4 @@
1
+ # encoding: UTF-8
2
+ module ToughGuy
3
+ Version = '0.1.0'
4
+ end
@@ -0,0 +1,33 @@
1
+ $:.unshift(File.expand_path('../../lib', __FILE__))
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+
6
+ Bundler.require(:default, :test)
7
+
8
+ require 'toughguy'
9
+
10
+ port = ENV.fetch "BOXEN_MONGODB_PORT", 27017
11
+ connection = Mongo::MongoClient.new('127.0.0.1', port.to_i)
12
+ ToughGuy::Model.connection = connection
13
+ ToughGuy::Model.database = DB = connection.db('test')
14
+
15
+ RSpec.configure do |config|
16
+ config.filter_run :focused => true
17
+ config.alias_example_to :fit, :focused => true
18
+ config.alias_example_to :xit, :pending => true
19
+ config.run_all_when_everything_filtered = true
20
+
21
+ config.before(:suite) do
22
+ DB.collections.reject { |collection|
23
+ collection.name =~ /system\./
24
+ }.map(&:drop_indexes)
25
+ end
26
+
27
+ config.before(:each) do
28
+ DB.collections.reject { |collection|
29
+ collection.name =~ /system\./
30
+ }.map(&:remove)
31
+ end
32
+ end
33
+
@@ -0,0 +1,193 @@
1
+ require 'helper'
2
+
3
+ class Mau < ToughGuy::Model
4
+ ensure_index([['blah', Mongo::ASCENDING]])
5
+ end
6
+
7
+ describe "Model#update" do
8
+ before do
9
+ Mau.drop
10
+ end
11
+
12
+ it "should replace the old values with the given values" do
13
+ m = Mau.create(a: 1, b: 2)
14
+ id = m.id
15
+ m.update(c: 3, d: 4)
16
+ m.refresh
17
+ m.values.should == {c: 3, d: 4, _id: id}
18
+ end
19
+ end
20
+
21
+ describe "Model#set" do
22
+ before do
23
+ Mau.drop
24
+ end
25
+
26
+ it "should merge the given values" do
27
+ m = Mau.create(a: 1, b: 2)
28
+ id = m.id
29
+ m.set(c: 3, d: 4)
30
+ m.refresh
31
+ m.values.should == {a: 1, b: 2, c: 3, d: 4, _id: id}
32
+ end
33
+
34
+ it "should replace nested hashes" do
35
+ m = Mau.create(a: 1, b: {c: 3})
36
+ id = m.id
37
+ m.set(b: {d: 4})
38
+ m.refresh
39
+ m.values.should == {a: 1, b: {d: 4}, _id: id}
40
+ end
41
+ end
42
+
43
+ describe "Model#collection" do
44
+ it "should default to Model.collection" do
45
+ m1 = Mau.new(a: 1)
46
+ m2 = Mau.new(b: 2)
47
+ m1.collection.should == m2.collection
48
+ m1.collection.should == Mau.collection
49
+ end
50
+
51
+ it "should be configurable per instance" do
52
+ c1 = DB['maus']
53
+ c2 = DB['maus']
54
+ c1.should_not == c2
55
+ m1 = Mau.new({a: 1}, c1)
56
+ m2 = Mau.new({b: 2}, c2)
57
+ m1.collection.should_not == m2.collection
58
+ m1.collection.should == c1
59
+ m2.collection.should == c2
60
+ end
61
+ end
62
+
63
+ describe "Model.with_db" do
64
+ before do
65
+ @db2 = ToughGuy::Model.connection['dbxxx']
66
+ @mau2 = Mau.with_db(@db2)
67
+ end
68
+
69
+ it "should create a descendant class" do
70
+ @mau2.class.should == Class
71
+ @mau2.superclass.should == Mau
72
+ end
73
+
74
+ it "should change the associated database to the specifed one" do
75
+ Mau.database.name.should == 'test'
76
+ Mau.database.should_not == @db2
77
+ @mau2.database.name.should == 'dbxxx'
78
+ end
79
+ end
80
+
81
+ class Single < ToughGuy::SingletonModel
82
+ set_collection_name :single
83
+
84
+ def self.reset
85
+ @instance = nil
86
+ collection.drop
87
+ end
88
+ end
89
+
90
+ describe "A singleton model" do
91
+ before do
92
+ Single.reset
93
+ end
94
+
95
+ it "should provide a single instance from the first doc in the collection" do
96
+ Single.collection.insert(a: 1, b: 2)
97
+ s1 = Single.instance
98
+ s1.a.should == 1
99
+ s1.b.should == 2
100
+ s2 = Single.instance
101
+ s2.object_id.should == s1.object_id
102
+ end
103
+
104
+ it "should create an instance automatically if the collection is empty" do
105
+ s1 = Single.instance
106
+ Single.collection.count.should == 1
107
+ s2 = Single.instance
108
+ Single.collection.count.should == 1
109
+ s2.object_id.should == s1.object_id
110
+ end
111
+
112
+ it "should always update the same record for the instance" do
113
+ s1 = Single.instance
114
+ s1.set(a: 1)
115
+ Single.collection.count.should == 1
116
+ s1.set(b: 2)
117
+ Single.collection.count.should == 1
118
+ s2 = Single.instance
119
+ s2.object_id.should == s1.object_id
120
+ s2.a.should == 1
121
+ s2.b.should == 2
122
+ end
123
+
124
+ it "should provide all resident instances" do
125
+ s1 = Single.with_db('s1')
126
+ s2 = Single.with_db('s2')
127
+ s1.instance
128
+
129
+ Single.all_resident_instances.should == [s1.instance]
130
+ s2.instance
131
+ Single.all_resident_instances.size.should == 2
132
+ Single.all_resident_instances.should include(s1.instance)
133
+ Single.all_resident_instances.should include(s2.instance)
134
+
135
+ Single.instance
136
+ Single.all_resident_instances.size.should == 3
137
+ Single.all_resident_instances.should include(s1.instance)
138
+ Single.all_resident_instances.should include(s2.instance)
139
+ Single.all_resident_instances.should include(Single.instance)
140
+ end
141
+ end
142
+
143
+ class Separate < ToughGuy::Model
144
+ set_collection_name :separate
145
+ end
146
+
147
+ describe "Model.with_db" do
148
+ before do
149
+ @c = ToughGuy::Model.connection
150
+ end
151
+
152
+ it "should provide a subclass of the model associated with a separate db" do
153
+ s1 = Separate.with_db('s1')
154
+ s2 = Separate.with_db('s2')
155
+ s1.superclass.should == Separate
156
+ s2.superclass.should == Separate
157
+ s1.db.name.should == 's1'
158
+ s2.db.name.should == 's2'
159
+ s1.drop
160
+ d1 = s1.create(a: 1)
161
+ s2.drop
162
+ d2 = s2.create(b: 2)
163
+ s1.count.should == 1
164
+ s2.count.should == 1
165
+
166
+ @c['s1']['separate'].count.should == 1
167
+ @c['s2']['separate'].count.should == 1
168
+
169
+ @c['s1']['separate'].find.to_a.first["a"].should == 1
170
+ @c['s2']['separate'].find.to_a.first["b"].should == 2
171
+
172
+ d2.destroy
173
+
174
+ s1.count.should == 1
175
+ s2.count.should == 0
176
+ @c['s1']['separate'].count.should == 1
177
+ @c['s2']['separate'].count.should == 0
178
+
179
+ d1.destroy
180
+ s1.count.should == 0
181
+ @c['s1']['separate'].count.should == 0
182
+ end
183
+
184
+ it "should cache subclasses for subsequent calls" do
185
+ s1 = Separate.with_db('s1')
186
+ s2 = Separate.with_db('s2')
187
+ s3 = Separate.with_db('s1')
188
+ s4 = Separate.with_db('s2')
189
+
190
+ s1.object_id.should == s3.object_id
191
+ s2.object_id.should == s4.object_id
192
+ end
193
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: UTF-8
2
+ require File.expand_path('../lib/toughguy/version', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'toughguy'
6
+ s.homepage = 'http://github.com/ciconia/toughguy'
7
+ s.summary = 'Simple MongoDB ORM'
8
+ s.require_path = 'lib'
9
+ s.authors = ['Sharon Rosner']
10
+ s.email = ['ciconia@gmail.com']
11
+ s.version = ToughGuy::Version
12
+ s.platform = Gem::Platform::RUBY
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_dependency 'mongo', '1.11.1'
20
+ s.add_dependency 'bson_ext', '1.11.1'
21
+ s.add_dependency 'plucky', '0.6.6'
22
+ s.add_dependency 'assistance', '0.1.5'
23
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: toughguy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sharon Rosner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mongo
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 1.11.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 1.11.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: bson_ext
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 1.11.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 1.11.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: plucky
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 0.6.6
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 0.6.6
55
+ - !ruby/object:Gem::Dependency
56
+ name: assistance
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 0.1.5
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 0.1.5
69
+ description:
70
+ email:
71
+ - ciconia@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - Gemfile
78
+ - Gemfile.lock
79
+ - LICENSE
80
+ - README.md
81
+ - Rakefile
82
+ - lib/toughguy.rb
83
+ - lib/toughguy/ext.rb
84
+ - lib/toughguy/mapped_model.rb
85
+ - lib/toughguy/mapped_query.rb
86
+ - lib/toughguy/model.rb
87
+ - lib/toughguy/query.rb
88
+ - lib/toughguy/singleton_model.rb
89
+ - lib/toughguy/version.rb
90
+ - spec/helper.rb
91
+ - spec/toughguy_spec.rb
92
+ - toughguy.gemspec
93
+ homepage: http://github.com/ciconia/toughguy
94
+ licenses: []
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.4.2
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: Simple MongoDB ORM
116
+ test_files:
117
+ - spec/helper.rb
118
+ - spec/toughguy_spec.rb