toughguy 0.1.0

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: 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