minidoc 0.0.1

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.
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"