mongoid-ids 0.1.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.
- checksums.yaml +7 -0
- data/.autotest +0 -0
- data/.gitignore +5 -0
- data/.rspec +3 -0
- data/.travis.yml +18 -0
- data/Gemfile +10 -0
- data/Guardfile +22 -0
- data/MIT-LICENSE.txt +22 -0
- data/README.md +225 -0
- data/Rakefile +2 -0
- data/benchmarks/benchmark.rb +48 -0
- data/lib/mongoid/ids/collision_resolver.rb +37 -0
- data/lib/mongoid/ids/collisions.rb +33 -0
- data/lib/mongoid/ids/exceptions.rb +16 -0
- data/lib/mongoid/ids/finders.rb +15 -0
- data/lib/mongoid/ids/generator.rb +76 -0
- data/lib/mongoid/ids/options.rb +78 -0
- data/lib/mongoid/ids/version.rb +5 -0
- data/lib/mongoid/ids.rb +83 -0
- data/mongoid-ids.gemspec +22 -0
- data/spec/mongoid/ids/collisions_spec.rb +102 -0
- data/spec/mongoid/ids/exceptions_spec.rb +4 -0
- data/spec/mongoid/ids/finders_spec.rb +41 -0
- data/spec/mongoid/ids/generator_spec.rb +49 -0
- data/spec/mongoid/ids/options_spec.rb +74 -0
- data/spec/mongoid/token_spec.rb +396 -0
- data/spec/spec_helper.rb +35 -0
- metadata +93 -0
data/lib/mongoid/ids.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'mongoid/ids/exceptions'
|
2
|
+
require 'mongoid/ids/options'
|
3
|
+
require 'mongoid/ids/generator'
|
4
|
+
require 'mongoid/ids/finders'
|
5
|
+
require 'mongoid/ids/collision_resolver'
|
6
|
+
|
7
|
+
module Mongoid
|
8
|
+
module Ids
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
# def initialize_copy(source)
|
13
|
+
# super(source)
|
14
|
+
# self.token = nil
|
15
|
+
# end
|
16
|
+
|
17
|
+
def token(*args)
|
18
|
+
options = Mongoid::Ids::Options.new(args.extract_options!)
|
19
|
+
options.field_name = args.join
|
20
|
+
|
21
|
+
add_token_collision_resolver(options)
|
22
|
+
|
23
|
+
if options.field_name == :_id
|
24
|
+
self.field :_id, default: -> { generate_token(options.pattern) }
|
25
|
+
else
|
26
|
+
set_token_callbacks(options)
|
27
|
+
add_token_field_and_index(options)
|
28
|
+
|
29
|
+
define_custom_finders(options) if options.skip_finders? == false
|
30
|
+
override_to_param(options) if options.override_to_param?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def add_token_field_and_index(options)
|
36
|
+
self.field(options.field_name, :type => String, :default => nil)
|
37
|
+
self.index({ options.field_name => 1 }, { :unique => true, :sparse => true })
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_token_collision_resolver(options)
|
41
|
+
resolver = Mongoid::Ids::CollisionResolver.new(self, options.field_name, options.retry_count)
|
42
|
+
resolver.create_new_token = Proc.new do |document|
|
43
|
+
document.send(:create_token, options.field_name, options.pattern)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def define_custom_finders(options)
|
48
|
+
Finders.define_custom_token_finder_for(self, options.field_name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def set_token_callbacks(options)
|
52
|
+
set_callback(:create, :before) do |document|
|
53
|
+
document.create_token_if_nil options.field_name, options.pattern
|
54
|
+
end
|
55
|
+
|
56
|
+
set_callback(:save, :before) do |document|
|
57
|
+
document.create_token_if_nil options.field_name, options.pattern
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def override_to_param(options)
|
62
|
+
self.send(:define_method, :to_param) do
|
63
|
+
self.send(options.field_name) || super()
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
def create_token(field_name, pattern)
|
70
|
+
self.send :"#{field_name.to_s}=", self.generate_token(pattern)
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_token_if_nil(field_name, pattern)
|
74
|
+
if self[field_name.to_sym].blank?
|
75
|
+
self.create_token field_name, pattern
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def generate_token(pattern)
|
80
|
+
Mongoid::Ids::Generator.generate pattern
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/mongoid-ids.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path('../lib', __FILE__)
|
3
|
+
require 'mongoid/ids/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'mongoid-ids'
|
7
|
+
s.version = Mongoid::Ids::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ['Nicholas Bruning', 'Marcos Piccinini']
|
10
|
+
s.homepage = 'http://github.com/nofxx/mongoid-ids'
|
11
|
+
s.licenses = ['MIT']
|
12
|
+
s.summary = %q(A little random, unique token generator for Mongoid documents.)
|
13
|
+
s.description = %q(Mongoid token is a gem for creating random, unique tokens for mongoid documents. Highly configurable and great for making URLs a little more compact.)
|
14
|
+
|
15
|
+
s.rubyforge_project = 'mongoid-ids'
|
16
|
+
s.add_dependency 'mongoid', '~> 4.0.0'
|
17
|
+
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mongoid::Ids::Collisions do
|
4
|
+
let(:document) { Object.new }
|
5
|
+
describe "#resolve_token_collisions" do
|
6
|
+
context "when there is a duplicate token" do
|
7
|
+
let(:resolver) { double("Mongoid::Ids::CollisionResolver") }
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
allow(resolver).to receive(:field_name).and_return(:token)
|
11
|
+
allow(resolver).to receive(:create_new_token_for){|doc|}
|
12
|
+
document.class.send(:include, Mongoid::Ids::Collisions)
|
13
|
+
allow(document).to receive(:is_duplicate_token_error?).and_return(true)
|
14
|
+
end
|
15
|
+
|
16
|
+
context "and there are zero retries" do
|
17
|
+
it "should raise an error after the first try" do
|
18
|
+
allow(resolver).to receive(:retry_count).and_return(0)
|
19
|
+
attempts = 0
|
20
|
+
expect{document.resolve_token_collisions(resolver) { attempts += 1; raise Moped::Errors::OperationFailure.new("","") }}.to raise_error Mongoid::Ids::CollisionRetriesExceeded
|
21
|
+
expect(attempts).to eq 1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "and retries is set to 1" do
|
26
|
+
it "should raise an error after retrying once" do
|
27
|
+
allow(resolver).to receive(:retry_count).and_return(1)
|
28
|
+
attempts = 0
|
29
|
+
expect{document.resolve_token_collisions(resolver) { attempts += 1; raise Moped::Errors::OperationFailure.new("","") }}.to raise_error Mongoid::Ids::CollisionRetriesExceeded
|
30
|
+
expect(attempts).to eq 2
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "and retries is greater than 1" do
|
35
|
+
it "should raise an error after retrying" do
|
36
|
+
allow(resolver).to receive(:retry_count).and_return(3)
|
37
|
+
attempts = 0
|
38
|
+
expect{document.resolve_token_collisions(resolver) { attempts += 1; raise Moped::Errors::OperationFailure.new("","") }}.to raise_error Mongoid::Ids::CollisionRetriesExceeded
|
39
|
+
expect(attempts).to eq 4
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "and a different index is violated" do
|
44
|
+
it "should bubble the operation failure" do
|
45
|
+
allow(document).to receive(:is_duplicate_token_error?).and_return(false)
|
46
|
+
allow(resolver).to receive(:retry_count).and_return(3)
|
47
|
+
e = Moped::Errors::OperationFailure.new("command", {:details => "nope"})
|
48
|
+
expect{document.resolve_token_collisions(resolver) { raise e }}.to raise_error(e)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#raise_collision_retries_exceeded_error" do
|
55
|
+
before(:each) do
|
56
|
+
document.class.send(:include, Mongoid::Ids::Collisions)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should warn the rails logger" do
|
60
|
+
message = nil
|
61
|
+
|
62
|
+
stub_const("Rails", Class.new)
|
63
|
+
|
64
|
+
logger = double("logger")
|
65
|
+
allow(logger).to receive("warn"){ |msg| message = msg }
|
66
|
+
allow(Rails).to receive("logger").and_return(logger)
|
67
|
+
|
68
|
+
begin
|
69
|
+
document.raise_collision_retries_exceeded_error(:token, 3)
|
70
|
+
rescue
|
71
|
+
end
|
72
|
+
expect(message).to_not be_nil
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should raise an error" do
|
76
|
+
expect{ document.raise_collision_retries_exceeded_error(:token, 3) }
|
77
|
+
.to raise_error(Mongoid::Ids::CollisionRetriesExceeded)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "#is_duplicate_token_error?" do
|
82
|
+
before(:each) do
|
83
|
+
document.class.send(:include, Mongoid::Ids::Collisions)
|
84
|
+
end
|
85
|
+
context "when there is a duplicate key error" do
|
86
|
+
it "should return true" do
|
87
|
+
allow(document).to receive("token").and_return("tokenvalue123")
|
88
|
+
err = double("Moped::Errors::OperationFailure")
|
89
|
+
allow(err).to receive("details") do
|
90
|
+
{
|
91
|
+
"err" => "E11000 duplicate key error index: mongoid_ids_test.links.$token_1 dup key: { : \"tokenvalue123\" }",
|
92
|
+
"code" => 11000,
|
93
|
+
"n" => 0,
|
94
|
+
"connectionId" => 130,
|
95
|
+
"ok" => 1.0
|
96
|
+
}
|
97
|
+
document.is_duplicate_token_error?(err, document, :token)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mongoid::Ids::Finders do
|
4
|
+
before :each do
|
5
|
+
end
|
6
|
+
|
7
|
+
it 'define a finder based on a field_name' do
|
8
|
+
klass = Class.new
|
9
|
+
field = :another_token
|
10
|
+
Mongoid::Ids::Finders.define_custom_token_finder_for(klass, field)
|
11
|
+
expect(klass.singleton_methods).to include(:"find_by_#{field}")
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'don\'t override the `find` method of the document' do
|
15
|
+
klass = Class.new
|
16
|
+
klass.define_singleton_method(:find) { |*args| :original_find }
|
17
|
+
klass.define_singleton_method(:find_by) { |*args| :token_find }
|
18
|
+
|
19
|
+
Mongoid::Ids::Finders.define_custom_token_finder_for(klass)
|
20
|
+
|
21
|
+
expect(klass.find(BSON::ObjectId.new)).to eq(:original_find)
|
22
|
+
expect(klass.find(BSON::ObjectId.new, BSON::ObjectId.new)).to eq(:original_find)
|
23
|
+
expect(klass.find()).to eq(:original_find)
|
24
|
+
expect(klass.find(BSON::ObjectId.new, 'token')).to eq(:original_find)
|
25
|
+
expect(klass.find('token')).to eq(:original_find)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'retrieve a document using the dynamic finder' do
|
29
|
+
class Document; include Mongoid::Document; field :token; end
|
30
|
+
document = Document.create!(token: '1234')
|
31
|
+
Mongoid::Ids::Finders.define_custom_token_finder_for(Document)
|
32
|
+
expect(Document.find_by_token('1234')).to eq(document)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'retrieve a document using the `find` method' do
|
36
|
+
class AnotherDocument; include Mongoid::Document; end
|
37
|
+
document = AnotherDocument.create! :_id => '1234'
|
38
|
+
Mongoid::Ids::Finders.define_custom_token_finder_for(AnotherDocument)
|
39
|
+
expect(AnotherDocument.find('1234')).to eq(document)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mongoid::Ids::Generator do
|
4
|
+
describe "#generate" do
|
5
|
+
it "generates lowercase characters" do
|
6
|
+
100.times{ expect(Mongoid::Ids::Generator.generate("%c")).to match(/[a-z]/) }
|
7
|
+
end
|
8
|
+
|
9
|
+
it "generates uppercase characters" do
|
10
|
+
100.times{ expect(Mongoid::Ids::Generator.generate("%C")).to match(/[A-Z]/) }
|
11
|
+
end
|
12
|
+
|
13
|
+
it "generates digits" do
|
14
|
+
100.times{ expect(Mongoid::Ids::Generator.generate("%d")).to match(/[0-9]/) }
|
15
|
+
end
|
16
|
+
|
17
|
+
it "generates non-zero digits" do
|
18
|
+
100.times{ expect(Mongoid::Ids::Generator.generate("%D")).to match(/[1-9]/) }
|
19
|
+
end
|
20
|
+
|
21
|
+
it "generates alphanumeric characters" do
|
22
|
+
100.times{ expect(Mongoid::Ids::Generator.generate("%s")).to match(/[A-Za-z0-9]/) }
|
23
|
+
end
|
24
|
+
|
25
|
+
it "generates upper and lowercase characters" do
|
26
|
+
100.times{ expect(Mongoid::Ids::Generator.generate("%w")).to match(/[A-Za-z]/) }
|
27
|
+
end
|
28
|
+
|
29
|
+
it "generates URL-safe punctuation" do
|
30
|
+
100.times{ expect(Mongoid::Ids::Generator.generate("%p")).to match(/[\.\-\_\=\+\$]/) }
|
31
|
+
end
|
32
|
+
|
33
|
+
it "generates patterns of a fixed length" do
|
34
|
+
100.times{ expect(Mongoid::Ids::Generator.generate("%s8")).to match(/[A-Za-z0-9]{8}/) }
|
35
|
+
end
|
36
|
+
|
37
|
+
it "generates patterns of a variable length" do
|
38
|
+
100.times{ expect(Mongoid::Ids::Generator.generate("%s1,5")).to match(/[A-Za-z0-9]{1,5}/) }
|
39
|
+
end
|
40
|
+
|
41
|
+
it "generates patterns with static prefixes/suffixes" do
|
42
|
+
100.times { expect(Mongoid::Ids::Generator.generate("prefix-%s4-suffix")).to match(/prefix\-[A-Za-z0-9]{4}\-suffix/) }
|
43
|
+
end
|
44
|
+
|
45
|
+
it "generates more complex patterns" do
|
46
|
+
100.times { expect(Mongoid::Ids::Generator.generate("pre-%d4-%C3-%d4")).to match(/pre\-[0-9]{4}\-[A-Z]{3}\-[0-9]{4}/) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mongoid::Ids::Options do
|
4
|
+
before do
|
5
|
+
@options = Mongoid::Ids::Options.new(
|
6
|
+
length: 9999,
|
7
|
+
retry_count: 8888,
|
8
|
+
contains: :nonsense,
|
9
|
+
field_name: :not_a_token
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should have a length' do
|
14
|
+
expect(@options.length).to eq(9999)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should default to a length of 4' do
|
18
|
+
expect(Mongoid::Ids::Options.new.length).to eq(4)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should have a retry count' do
|
22
|
+
expect(@options.retry_count).to eq(8888)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should default to a retry count of 3' do
|
26
|
+
expect(Mongoid::Ids::Options.new.retry_count).to eq(3)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should have a list of characters to contain' do
|
30
|
+
expect(@options.contains).to eq(:nonsense)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should default to an alphanumeric set of characters to contain' do
|
34
|
+
expect(Mongoid::Ids::Options.new.contains).to eq(:alphanumeric)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should default to an alphanumeric set of characters to contain' do
|
38
|
+
expect(Mongoid::Ids::Options.new.contains).to eq(:alphanumeric)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should have a field name' do
|
42
|
+
expect(@options.field_name).to eq(:not_a_token)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should default to a field name of \'_id\'' do
|
46
|
+
expect(Mongoid::Ids::Options.new.field_name).to eq(:_id)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should create a pattern' do
|
50
|
+
expect(Mongoid::Ids::Options.new.pattern).to eq('%s4')
|
51
|
+
end
|
52
|
+
|
53
|
+
describe 'override_to_param' do
|
54
|
+
it 'should be an option' do
|
55
|
+
expect(Mongoid::Ids::Options.new(override_to_param: false)
|
56
|
+
.override_to_param?).to eq false
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should default to true' do
|
60
|
+
expect(Mongoid::Ids::Options.new.override_to_param?).to eq true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe 'skip_finder' do
|
65
|
+
it 'should be an option' do
|
66
|
+
expect(Mongoid::Ids::Options.new(skip_finders: true)
|
67
|
+
.skip_finders?).to eq true
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should default to false' do
|
71
|
+
expect(Mongoid::Ids::Options.new.skip_finders?).to eq false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|