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