mongoid-ids 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|