minidoc 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.ruby-version +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +76 -0
- data/Rakefile +11 -0
- data/lib/minidoc/associations.rb +60 -0
- data/lib/minidoc/autoload.rb +1 -0
- data/lib/minidoc/connection.rb +28 -0
- data/lib/minidoc/counters.rb +44 -0
- data/lib/minidoc/duplicate_key.rb +12 -0
- data/lib/minidoc/finders.rb +50 -0
- data/lib/minidoc/grid.rb +16 -0
- data/lib/minidoc/indexes.rb +31 -0
- data/lib/minidoc/read_only.rb +11 -0
- data/lib/minidoc/record_invalid.rb +9 -0
- data/lib/minidoc/test_helpers.rb +24 -0
- data/lib/minidoc/timestamps.rb +37 -0
- data/lib/minidoc/validations.rb +40 -0
- data/lib/minidoc/value.rb +21 -0
- data/lib/minidoc.rb +244 -0
- data/minidoc.gemspec +23 -0
- data/test/activemodel_test.rb +28 -0
- data/test/belongs_to_test.rb +49 -0
- data/test/connection_test.rb +20 -0
- data/test/counters_test.rb +48 -0
- data/test/duplicate_key_test.rb +21 -0
- data/test/grid_test.rb +33 -0
- data/test/helper.rb +22 -0
- data/test/indexes_test.rb +52 -0
- data/test/locale/en.yml +5 -0
- data/test/persistence_test.rb +183 -0
- data/test/query_test.rb +40 -0
- data/test/read_only_test.rb +34 -0
- data/test/timestamps_test.rb +21 -0
- data/test/uniqueness_validator_test.rb +68 -0
- data/test/validations_test.rb +36 -0
- metadata +196 -0
data/lib/minidoc.rb
ADDED
@@ -0,0 +1,244 @@
|
|
1
|
+
require "mongo"
|
2
|
+
require "virtus"
|
3
|
+
require "active_model"
|
4
|
+
require "active_support/core_ext"
|
5
|
+
|
6
|
+
class Minidoc
|
7
|
+
VERSION = "0.0.1"
|
8
|
+
|
9
|
+
require "minidoc/associations"
|
10
|
+
require "minidoc/connection"
|
11
|
+
require "minidoc/counters"
|
12
|
+
require "minidoc/finders"
|
13
|
+
require "minidoc/grid"
|
14
|
+
require "minidoc/indexes"
|
15
|
+
require "minidoc/read_only"
|
16
|
+
require "minidoc/record_invalid"
|
17
|
+
require "minidoc/duplicate_key"
|
18
|
+
require "minidoc/timestamps"
|
19
|
+
require "minidoc/validations"
|
20
|
+
require "minidoc/value"
|
21
|
+
|
22
|
+
include Connection
|
23
|
+
include Finders
|
24
|
+
include Validations
|
25
|
+
include Virtus.model
|
26
|
+
extend ActiveModel::Naming
|
27
|
+
include ActiveModel::Conversion
|
28
|
+
include ActiveModel::Validations
|
29
|
+
|
30
|
+
attribute :_id, BSON::ObjectId
|
31
|
+
alias_attribute :id, :_id
|
32
|
+
|
33
|
+
def self.delete_all
|
34
|
+
collection.remove({})
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.create(attrs = {})
|
38
|
+
new(attrs).tap(&:save)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.create!(*args)
|
42
|
+
new(*args).tap(&:save!)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.delete(id)
|
46
|
+
collection.remove(_id: BSON::ObjectId(id.to_s))
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.set(id, attributes)
|
50
|
+
id = BSON::ObjectId(id.to_s)
|
51
|
+
update_one(id, "$set" => attributes)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.unset(id, *keys)
|
55
|
+
id = BSON::ObjectId(id.to_s)
|
56
|
+
|
57
|
+
unsets = {}
|
58
|
+
keys.each do |key|
|
59
|
+
unsets[key] = 1
|
60
|
+
end
|
61
|
+
|
62
|
+
update_one(id, "$unset" => unsets)
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.update_one(id, updates)
|
66
|
+
collection.update({ "_id" => id }, updates)
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.atomic_set(query, attributes)
|
70
|
+
result = collection.update(query, "$set" => attributes)
|
71
|
+
result["ok"] == 1 && result["n"] == 1
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.value_class
|
75
|
+
@value_class ||= Class.new(self) do
|
76
|
+
attribute_set.each do |attr|
|
77
|
+
private "#{attr.name}="
|
78
|
+
end
|
79
|
+
|
80
|
+
private :attributes=
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# For databases that support it (e.g. TokuMX), perform the block within a
|
85
|
+
# transaction. For information on the +isolation+ argument, see
|
86
|
+
# https://www.percona.com/doc/percona-tokumx/commands.html#beginTransaction
|
87
|
+
def self.transaction(isolation = "mvcc")
|
88
|
+
return yield unless tokumx?
|
89
|
+
|
90
|
+
begin
|
91
|
+
database.command(beginTransaction: 1, isolation: isolation)
|
92
|
+
yield
|
93
|
+
rescue Exception => error
|
94
|
+
database.command(rollbackTransaction: 1) rescue nil
|
95
|
+
raise
|
96
|
+
ensure
|
97
|
+
begin
|
98
|
+
database.command(commitTransaction: 1) unless error
|
99
|
+
rescue Exception
|
100
|
+
database.command(rollbackTransaction: 1)
|
101
|
+
raise
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Rescue a duplicate key exception in the given block. Returns the result of
|
107
|
+
# the block, or +false+ if the exception was raised.
|
108
|
+
def self.rescue_duplicate_key_errors
|
109
|
+
yield
|
110
|
+
rescue Minidoc::DuplicateKey
|
111
|
+
false
|
112
|
+
rescue Mongo::OperationFailure => ex
|
113
|
+
if Minidoc::DuplicateKey.duplicate_key_exception(ex)
|
114
|
+
false
|
115
|
+
else
|
116
|
+
raise
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.tokumx?
|
121
|
+
@server_info ||= connection.server_info
|
122
|
+
@server_info.key?("tokumxVersion")
|
123
|
+
end
|
124
|
+
|
125
|
+
def initialize(attrs = {})
|
126
|
+
if attrs["_id"].nil? && attrs[:_id].nil?
|
127
|
+
attrs[:_id] = BSON::ObjectId.new
|
128
|
+
end
|
129
|
+
|
130
|
+
@new_record = true
|
131
|
+
@destroyed = false
|
132
|
+
|
133
|
+
super(attrs)
|
134
|
+
end
|
135
|
+
|
136
|
+
def ==(other)
|
137
|
+
other.is_a?(self.class) && self.id && self.id == other.id
|
138
|
+
end
|
139
|
+
|
140
|
+
def new_record?
|
141
|
+
@new_record
|
142
|
+
end
|
143
|
+
|
144
|
+
def destroyed?
|
145
|
+
@destroyed
|
146
|
+
end
|
147
|
+
|
148
|
+
def persisted?
|
149
|
+
!(new_record? || destroyed?)
|
150
|
+
end
|
151
|
+
|
152
|
+
def delete
|
153
|
+
self.class.delete(id)
|
154
|
+
end
|
155
|
+
|
156
|
+
def destroy
|
157
|
+
delete
|
158
|
+
@destroyed = true
|
159
|
+
end
|
160
|
+
|
161
|
+
def reload
|
162
|
+
new_object = self.class.find(self.id)
|
163
|
+
|
164
|
+
self.class.attribute_set.each do |attr|
|
165
|
+
self[attr.name] = new_object[attr.name]
|
166
|
+
end
|
167
|
+
|
168
|
+
self
|
169
|
+
end
|
170
|
+
|
171
|
+
def save
|
172
|
+
valid? ? create_or_update : false
|
173
|
+
end
|
174
|
+
|
175
|
+
def save!
|
176
|
+
valid? ? create_or_update : raise(RecordInvalid.new(self))
|
177
|
+
end
|
178
|
+
|
179
|
+
def set(attributes)
|
180
|
+
self.class.set(id, attributes)
|
181
|
+
|
182
|
+
attributes.each do |name, value|
|
183
|
+
self[name] = value
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def unset(*keys)
|
188
|
+
self.class.unset(id, *keys)
|
189
|
+
|
190
|
+
keys.each do |key|
|
191
|
+
self[key] = nil
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def atomic_set(query, attributes)
|
196
|
+
query[:_id] = id
|
197
|
+
|
198
|
+
if self.class.atomic_set(query, attributes)
|
199
|
+
attributes.each do |name, value|
|
200
|
+
self[name] = value
|
201
|
+
end
|
202
|
+
|
203
|
+
true
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def as_value
|
208
|
+
self.class.value_class.new(attributes)
|
209
|
+
end
|
210
|
+
|
211
|
+
def to_key
|
212
|
+
[id.to_s]
|
213
|
+
end
|
214
|
+
|
215
|
+
def to_param
|
216
|
+
id.to_s
|
217
|
+
end
|
218
|
+
|
219
|
+
private
|
220
|
+
|
221
|
+
def create_or_update
|
222
|
+
new_record? ? create : update
|
223
|
+
@new_record = false
|
224
|
+
true
|
225
|
+
rescue Mongo::OperationFailure => exception
|
226
|
+
if (duplicate_key_exception = Minidoc::DuplicateKey.duplicate_key_exception(exception))
|
227
|
+
raise duplicate_key_exception
|
228
|
+
else
|
229
|
+
raise
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def create
|
234
|
+
self.class.collection << attributes
|
235
|
+
end
|
236
|
+
|
237
|
+
def update
|
238
|
+
self.class.collection.update({ _id: id }, attributes)
|
239
|
+
end
|
240
|
+
|
241
|
+
if ActiveSupport.respond_to?(:run_load_hooks)
|
242
|
+
ActiveSupport.run_load_hooks(:mongo)
|
243
|
+
end
|
244
|
+
end
|
data/minidoc.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
Gem::Specification.new do |spec|
|
3
|
+
spec.name = "minidoc"
|
4
|
+
spec.version = "0.0.1"
|
5
|
+
spec.authors = ["Bryan Helmkamp"]
|
6
|
+
spec.email = ["bryan@brynary.com"]
|
7
|
+
spec.summary = %q{Lightweight wrapper for MongoDB documents}
|
8
|
+
spec.homepage = "https://github.com/brynary/minidoc"
|
9
|
+
spec.license = "MIT"
|
10
|
+
|
11
|
+
spec.files = `git ls-files`.split($/)
|
12
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
13
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
14
|
+
spec.require_paths = ["lib"]
|
15
|
+
|
16
|
+
spec.add_dependency "activesupport", ">= 3.0.0"
|
17
|
+
spec.add_dependency "activemodel", ">= 3.0.0"
|
18
|
+
spec.add_dependency "virtus", "~> 1.0.0"
|
19
|
+
spec.add_dependency "mongo", "~> 1"
|
20
|
+
spec.add_development_dependency "minitest"
|
21
|
+
spec.add_development_dependency "mocha"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
class ActiveModelTest < Minidoc::TestCase
|
4
|
+
def test_model_name
|
5
|
+
assert_equal "User", User.model_name.to_s
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_to_model
|
9
|
+
user = User.new
|
10
|
+
assert_equal user, user.to_model
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_to_key
|
14
|
+
user = User.new
|
15
|
+
user.id = BSON::ObjectId('52955618f9f6a52444000001')
|
16
|
+
assert_equal ["52955618f9f6a52444000001"], user.to_key
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_to_param
|
20
|
+
user = User.new
|
21
|
+
user.id = BSON::ObjectId('52955618f9f6a52444000001')
|
22
|
+
assert_equal "52955618f9f6a52444000001", user.to_param
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_to_path
|
26
|
+
assert_equal "users/user", User.new.to_partial_path
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
class BelongsToTest < Minidoc::TestCase
|
4
|
+
class Cat < Minidoc
|
5
|
+
include Minidoc::Associations
|
6
|
+
belongs_to :owner, class_name: "User"
|
7
|
+
end
|
8
|
+
|
9
|
+
class User < ::User
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_loading
|
13
|
+
assert_nil Cat.new.owner
|
14
|
+
user = User.create
|
15
|
+
cat = Cat.new(owner_id: user.id)
|
16
|
+
assert_equal user.id, cat.owner.id
|
17
|
+
cat.save
|
18
|
+
assert_equal user.id, cat.owner.id
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_caching
|
22
|
+
user = User.create(name: "Bryan")
|
23
|
+
cat = Cat.create(owner_id: user.id)
|
24
|
+
assert_equal "Bryan", cat.owner.name
|
25
|
+
user.set(name: "Noah")
|
26
|
+
assert_equal "Bryan", cat.owner.name # doesn't change
|
27
|
+
assert_equal "Noah", cat.reload.owner.name # changes
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_cache_expiry_on_field_update
|
31
|
+
user = User.create(name: "Bryan")
|
32
|
+
cat = Cat.create(owner_id: user.id)
|
33
|
+
assert_equal "Bryan", cat.owner.name
|
34
|
+
user.set(name: "Noah")
|
35
|
+
assert_equal "Bryan", cat.owner.name # doesn't change
|
36
|
+
cat.owner = user
|
37
|
+
assert_equal "Noah", cat.owner.name # changes
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_cache_expiry_on_id_update
|
41
|
+
user = User.create(name: "Bryan")
|
42
|
+
cat = Cat.create(owner_id: user.id)
|
43
|
+
assert_equal "Bryan", cat.owner.name
|
44
|
+
user.set(name: "Noah")
|
45
|
+
assert_equal "Bryan", cat.owner.name # doesn't change
|
46
|
+
cat.owner_id = user.id
|
47
|
+
assert_equal "Noah", cat.owner.name # changes
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
class ConnectionTest < Minidoc::TestCase
|
4
|
+
class Company < Minidoc
|
5
|
+
self.collection_name = "accounts"
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_collection_name
|
9
|
+
assert_equal "users", User.collection_name
|
10
|
+
assert_equal "accounts", Company.collection_name
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_collection
|
14
|
+
assert_equal "users", User.collection.name
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_database
|
18
|
+
assert_equal "minidoc_test", User.database.name
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
class CountersTest < Minidoc::TestCase
|
4
|
+
class SimpleCounter < Minidoc
|
5
|
+
include Minidoc::Counters
|
6
|
+
|
7
|
+
counter :counter
|
8
|
+
end
|
9
|
+
|
10
|
+
class AdvancedCounter < Minidoc
|
11
|
+
include Minidoc::Counters
|
12
|
+
|
13
|
+
counter :counter, start: 2, step_size: 3
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_incrementing
|
17
|
+
x = SimpleCounter.create!
|
18
|
+
|
19
|
+
assert_equal 0, x.counter
|
20
|
+
assert_equal 1, x.increment_counter
|
21
|
+
assert_equal 2, x.increment_counter
|
22
|
+
assert_equal 3, x.increment_counter
|
23
|
+
assert_equal 3, x.reload.counter
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_options
|
27
|
+
x = AdvancedCounter.create!
|
28
|
+
|
29
|
+
assert_equal 2, x.counter
|
30
|
+
assert_equal 5, x.increment_counter
|
31
|
+
assert_equal 8, x.increment_counter
|
32
|
+
assert_equal 11, x.increment_counter
|
33
|
+
assert_equal 11, x.reload.counter
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_threading
|
37
|
+
x = SimpleCounter.create!
|
38
|
+
counters = []
|
39
|
+
|
40
|
+
[
|
41
|
+
Thread.new { 5.times { counters << x.increment_counter } },
|
42
|
+
Thread.new { 5.times { counters << x.increment_counter } },
|
43
|
+
Thread.new { 5.times { counters << x.increment_counter } },
|
44
|
+
].map(&:join)
|
45
|
+
|
46
|
+
assert_equal 15, counters.uniq.length
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
class DuplicateKeyTest < Minidoc::TestCase
|
4
|
+
def test_rescue_duplicate_key_errors_result
|
5
|
+
user = User.create!
|
6
|
+
result = Minidoc.rescue_duplicate_key_errors do
|
7
|
+
User.create!(name: "two")
|
8
|
+
end
|
9
|
+
|
10
|
+
assert_equal "two", result.name
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_rescue_duplicate_key_errors_false
|
14
|
+
user = User.create!
|
15
|
+
result = Minidoc.rescue_duplicate_key_errors do
|
16
|
+
User.create!(_id: user.id, name: "two")
|
17
|
+
end
|
18
|
+
|
19
|
+
assert_equal false, result
|
20
|
+
end
|
21
|
+
end
|
data/test/grid_test.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
class GridTest < Minidoc::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
super
|
7
|
+
@grid = Minidoc::Grid.new(Minidoc.database)
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_get_with_string
|
11
|
+
doc_id = @grid.put("estamos en espana")
|
12
|
+
as_string = doc_id.to_s
|
13
|
+
document = @grid.get(as_string)
|
14
|
+
assert_equal("estamos en espana", document.read)
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_get_json
|
18
|
+
hash = { "foo" => { "bar" => 1 } }
|
19
|
+
doc_id = @grid.put(hash.to_json)
|
20
|
+
as_string = doc_id.to_s
|
21
|
+
assert_equal(hash, @grid.get_json(as_string))
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_delete
|
25
|
+
doc_id = @grid.put("estamos en espana")
|
26
|
+
as_string = doc_id.to_s
|
27
|
+
@grid.delete(as_string)
|
28
|
+
assert_raises Mongo::GridFileNotFound do
|
29
|
+
@grid.get(doc_id)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'minidoc'
|
2
|
+
require 'minidoc/test_helpers'
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'mocha/mini_test'
|
5
|
+
|
6
|
+
I18n.enforce_available_locales = false
|
7
|
+
I18n.load_path << File.expand_path("../locale/en.yml", __FILE__)
|
8
|
+
|
9
|
+
class User < Minidoc
|
10
|
+
attribute :name, String
|
11
|
+
attribute :age, Integer
|
12
|
+
end
|
13
|
+
|
14
|
+
$mongo = Mongo::MongoClient.from_uri(ENV["MONGODB_URI"] || "mongodb://localhost")
|
15
|
+
Minidoc.connection = $mongo
|
16
|
+
Minidoc.database_name = "minidoc_test"
|
17
|
+
|
18
|
+
class Minidoc::TestCase < Minitest::Test
|
19
|
+
def setup
|
20
|
+
Minidoc::TestHelpers.clear_database
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
class IndexesTest < Minidoc::TestCase
|
4
|
+
class Company < Minidoc
|
5
|
+
self.collection_name = "accounts"
|
6
|
+
|
7
|
+
include Minidoc::Indexes
|
8
|
+
|
9
|
+
attribute :name
|
10
|
+
attribute :title
|
11
|
+
attribute :description
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_ensure_index_single
|
15
|
+
Company.ensure_index(:name)
|
16
|
+
|
17
|
+
assert_equal %w( name ), indexed_keys(Company, "name_1")
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_ensure_index_multiple
|
21
|
+
Company.ensure_index([:name, :title])
|
22
|
+
|
23
|
+
assert_equal %w( name title ), indexed_keys(Company, "name_1_title_1")
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_ensure_index_options
|
27
|
+
Company.ensure_index(:name, unique: true)
|
28
|
+
|
29
|
+
assert index_info(Company, "name_1")["unique"]
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_rescue_duplicate_key_errors_not_raising_exception
|
33
|
+
Company.ensure_index(:name, unique: true)
|
34
|
+
|
35
|
+
Company.rescue_duplicate_key_errors do
|
36
|
+
Company.create!(name: "CodeClimate")
|
37
|
+
Company.create!(name: "CodeClimate")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def index_info(klass, name)
|
44
|
+
klass.collection.index_information[name] || {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def indexed_keys(klass, name)
|
48
|
+
key_info = index_info(klass, name)["key"] || {}
|
49
|
+
key_info.keys
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|