mongoid-ids 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mongoid::Ids::CollisionRetriesExceeded do
4
+ 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