minidoc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+ en:
2
+ activemodel:
3
+ errors:
4
+ messages:
5
+ taken: "has already been taken"