mongoid_token_r 4.0.0
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 +1 -0
- data/.travis.yml +18 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +242 -0
- data/Rakefile +2 -0
- data/benchmarks/benchmark.rb +49 -0
- data/lib/mongoid/token.rb +81 -0
- data/lib/mongoid/token/collision_resolver.rb +37 -0
- data/lib/mongoid/token/collisions.rb +33 -0
- data/lib/mongoid/token/exceptions.rb +16 -0
- data/lib/mongoid/token/finders.rb +15 -0
- data/lib/mongoid/token/generator.rb +80 -0
- data/lib/mongoid/token/options.rb +78 -0
- data/lib/mongoid_token.rb +1 -0
- data/lib/version.rb +3 -0
- data/mongoid_token.gemspec +23 -0
- data/spec/mongoid/token/collisions_spec.rb +101 -0
- data/spec/mongoid/token/exceptions_spec.rb +4 -0
- data/spec/mongoid/token/finders_spec.rb +30 -0
- data/spec/mongoid/token/generator_spec.rb +57 -0
- data/spec/mongoid/token/options_spec.rb +102 -0
- data/spec/mongoid/token_spec.rb +287 -0
- data/spec/spec_helper.rb +29 -0
- metadata +92 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'mongoid/token/collisions'
|
2
|
+
|
3
|
+
module Mongoid
|
4
|
+
module Token
|
5
|
+
class CollisionResolver
|
6
|
+
attr_accessor :create_new_token
|
7
|
+
attr_reader :klass
|
8
|
+
attr_reader :field_name
|
9
|
+
attr_reader :retry_count
|
10
|
+
|
11
|
+
def initialize(klass, field_name, retry_count)
|
12
|
+
@create_new_token = Proc.new {|doc|}
|
13
|
+
@klass = klass
|
14
|
+
@field_name = field_name
|
15
|
+
@retry_count = retry_count
|
16
|
+
klass.send(:include, Mongoid::Token::Collisions)
|
17
|
+
alias_method_with_collision_resolution(:insert)
|
18
|
+
alias_method_with_collision_resolution(:upsert)
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_new_token_for(document)
|
22
|
+
@create_new_token.call(document)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def alias_method_with_collision_resolution(method)
|
27
|
+
handler = self
|
28
|
+
klass.send(:define_method, :"#{method.to_s}_with_#{handler.field_name}_safety") do |method_options = {}|
|
29
|
+
self.resolve_token_collisions handler do
|
30
|
+
with(:safe => true).send(:"#{method.to_s}_without_#{handler.field_name}_safety", method_options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
klass.alias_method_chain method.to_sym, :"#{handler.field_name}_safety"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Token
|
3
|
+
module Collisions
|
4
|
+
def resolve_token_collisions(resolver)
|
5
|
+
retries = resolver.retry_count
|
6
|
+
begin
|
7
|
+
yield
|
8
|
+
rescue Mongo::Error::OperationFailure => e
|
9
|
+
if is_duplicate_token_error?(e, self, resolver.field_name)
|
10
|
+
if (retries -= 1) >= 0
|
11
|
+
resolver.create_new_token_for(self)
|
12
|
+
retry
|
13
|
+
end
|
14
|
+
raise_collision_retries_exceeded_error resolver.field_name, resolver.retry_count
|
15
|
+
else
|
16
|
+
raise e
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def raise_collision_retries_exceeded_error(field_name, retry_count)
|
22
|
+
Rails.logger.warn "[Mongoid::Token] Warning: Maximum token generation retries (#{retry_count}) exceeded on `#{field_name}'." if defined?(Rails)
|
23
|
+
raise Mongoid::Token::CollisionRetriesExceeded.new(self, retry_count)
|
24
|
+
end
|
25
|
+
|
26
|
+
def is_duplicate_token_error?(err, document, field_name)
|
27
|
+
err.message =~ /(11000|11001)/ &&
|
28
|
+
err.message =~ /dup key/ &&
|
29
|
+
err.message =~ /"#{document.send(field_name)}"/
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Token
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
class CollisionRetriesExceeded < Error
|
6
|
+
def initialize(resource = "unknown resource", attempts = "unspecified")
|
7
|
+
@resource = resource
|
8
|
+
@attempts = attempts
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
"Failed to generate unique token for #{@resource} after #{@attempts} attempts."
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Token
|
3
|
+
module Finders
|
4
|
+
def self.define_custom_token_finder_for(klass, field_name = :token)
|
5
|
+
klass.define_singleton_method(:"find_by_#{field_name}") do |token|
|
6
|
+
if token.is_a?(Array)
|
7
|
+
self.in field_name.to_sym => token
|
8
|
+
else
|
9
|
+
self.find_by field_name.to_sym => token
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# proposed pattern options
|
2
|
+
# %c - lowercase character
|
3
|
+
# %C - uppercase character
|
4
|
+
# %d - digit
|
5
|
+
# %D - non-zero digit / no-leading zero digit if longer than 1
|
6
|
+
# %s - alphanumeric character
|
7
|
+
# %w - upper and lower alpha character
|
8
|
+
# %p - URL-safe punctuation
|
9
|
+
#
|
10
|
+
# Any pattern can be followed by a number, representing how many of that type to generate
|
11
|
+
|
12
|
+
module Mongoid
|
13
|
+
module Token
|
14
|
+
module Generator
|
15
|
+
REPLACE_PATTERN = /%((?<character>[cCdDhHpsw]{1})(?<length>\d+(,\d+)?)?)/
|
16
|
+
|
17
|
+
def self.generate(pattern)
|
18
|
+
pattern.gsub REPLACE_PATTERN do |match|
|
19
|
+
match_data = $~
|
20
|
+
type = match_data[:character]
|
21
|
+
length = [match_data[:length].to_i, 1].max
|
22
|
+
|
23
|
+
case type
|
24
|
+
when 'c'
|
25
|
+
down_character(length)
|
26
|
+
when 'C'
|
27
|
+
up_character(length)
|
28
|
+
when 'd'
|
29
|
+
digits(length)
|
30
|
+
when 'D'
|
31
|
+
integer(length)
|
32
|
+
when 'h'
|
33
|
+
digits(length, 16)
|
34
|
+
when 'H'
|
35
|
+
integer(length, 16)
|
36
|
+
when 's'
|
37
|
+
alphanumeric(length)
|
38
|
+
when 'w'
|
39
|
+
alpha(length)
|
40
|
+
when 'p'
|
41
|
+
"-"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def self.rand_string_from_chars(chars, length = 1)
|
48
|
+
Array.new(length).map{ chars.sample }.join
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.down_character(length = 1)
|
52
|
+
self.rand_string_from_chars ('a'..'z').to_a, length
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.up_character(length = 1)
|
56
|
+
self.rand_string_from_chars ('A'..'Z').to_a, length
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.integer(length = 1, base = 10)
|
60
|
+
(rand(base**length - base**(length-1)) + base**(length-1)).to_s(base)
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.digits(length = 1, base = 10)
|
64
|
+
rand(base**length).to_s(base).rjust(length, "0")
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.alpha(length = 1)
|
68
|
+
self.rand_string_from_chars (('A'..'Z').to_a + ('a'..'z').to_a), length
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.alphanumeric(length = 1)
|
72
|
+
(1..length).collect { (i = Kernel.rand(62); i += ((i < 10) ? 48 : ((i < 36) ? 55 : 61 ))).chr }.join
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.punctuation(length = 1)
|
76
|
+
self.rand_string_from_chars ['.','-','_','=','+','$'], length
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
class Mongoid::Token::Options
|
2
|
+
def initialize(options = {})
|
3
|
+
@options = merge_defaults validate_options(options)
|
4
|
+
end
|
5
|
+
|
6
|
+
def length
|
7
|
+
@options[:length]
|
8
|
+
end
|
9
|
+
|
10
|
+
def retry_count
|
11
|
+
@options[:retry_count]
|
12
|
+
end
|
13
|
+
|
14
|
+
def contains
|
15
|
+
@options[:contains]
|
16
|
+
end
|
17
|
+
|
18
|
+
def field_name
|
19
|
+
!@options[:id] && @options[:field_name] || :_id
|
20
|
+
end
|
21
|
+
|
22
|
+
def skip_finders?
|
23
|
+
@options[:skip_finders]
|
24
|
+
end
|
25
|
+
|
26
|
+
def override_to_param?
|
27
|
+
@options[:override_to_param]
|
28
|
+
end
|
29
|
+
|
30
|
+
def generate_on_init
|
31
|
+
@options[:id] || @options[:generate_on_init]
|
32
|
+
end
|
33
|
+
|
34
|
+
def pattern
|
35
|
+
@options[:pattern] ||= case @options[:contains].to_sym
|
36
|
+
when :alphanumeric
|
37
|
+
"%s#{@options[:length]}"
|
38
|
+
when :alpha
|
39
|
+
"%w#{@options[:length]}"
|
40
|
+
when :alpha_upper
|
41
|
+
"%C#{@options[:length]}"
|
42
|
+
when :alpha_lower
|
43
|
+
"%c#{@options[:length]}"
|
44
|
+
when :numeric
|
45
|
+
"%d1,#{@options[:length]}"
|
46
|
+
when :fixed_numeric
|
47
|
+
"%d#{@options[:length]}"
|
48
|
+
when :fixed_numeric_no_leading_zeros
|
49
|
+
"%D#{@options[:length]}"
|
50
|
+
when :fixed_hex_numeric
|
51
|
+
"%h#{@options[:length]}"
|
52
|
+
when :fixed_hex_numeric_no_leading_zeros
|
53
|
+
"%H#{@options[:length]}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
def validate_options(options)
|
59
|
+
if options.has_key?(:retry)
|
60
|
+
STDERR.puts "Mongoid::Token Deprecation Warning: option `retry` has been renamed to `retry_count`. `:retry` will be removed in v2.1"
|
61
|
+
options[:retry_count] = options[:retry]
|
62
|
+
end
|
63
|
+
options
|
64
|
+
end
|
65
|
+
|
66
|
+
def merge_defaults(options)
|
67
|
+
{
|
68
|
+
id: false,
|
69
|
+
length: 4,
|
70
|
+
retry_count: 3,
|
71
|
+
contains: :alphanumeric,
|
72
|
+
field_name: :token,
|
73
|
+
skip_finders: false,
|
74
|
+
override_to_param: true,
|
75
|
+
generate_on_init: false
|
76
|
+
}.merge(options)
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'mongoid/token'
|
data/lib/version.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "mongoid_token_r"
|
7
|
+
s.version = MongoidToken::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Nicholas Bruning"]
|
10
|
+
s.email = ["nicholas@bruning.com.au"]
|
11
|
+
s.homepage = "http://github.com/thetron/mongoid_token"
|
12
|
+
s.licenses = ['MIT']
|
13
|
+
s.summary = %q{A little random, unique token generator for Mongoid documents.}
|
14
|
+
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.}
|
15
|
+
|
16
|
+
s.rubyforge_project = "mongoid_token"
|
17
|
+
s.add_dependency 'mongoid', '~> 5.0'
|
18
|
+
|
19
|
+
s.files = `git ls-files`.split("\n")
|
20
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
21
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
22
|
+
s.require_paths = ["lib"]
|
23
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w[.. .. spec_helper])
|
2
|
+
|
3
|
+
describe Mongoid::Token::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::Token::CollisionResolver") }
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
resolver.stub(:field_name).and_return(:token)
|
11
|
+
resolver.stub(:create_new_token_for){|doc|}
|
12
|
+
document.class.send(:include, Mongoid::Token::Collisions)
|
13
|
+
document.stub(: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
|
+
resolver.stub(:retry_count).and_return(0)
|
19
|
+
attempts = 0
|
20
|
+
expect{document.resolve_token_collisions(resolver) { attempts += 1; raise Mongo::Error::OperationFailure }}.to raise_error Mongoid::Token::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
|
+
resolver.stub(:retry_count).and_return(1)
|
28
|
+
attempts = 0
|
29
|
+
expect{document.resolve_token_collisions(resolver) { attempts += 1; raise Mongo::Error::OperationFailure }}.to raise_error Mongoid::Token::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
|
+
resolver.stub(:retry_count).and_return(3)
|
37
|
+
attempts = 0
|
38
|
+
expect{document.resolve_token_collisions(resolver) { attempts += 1; raise Mongo::Error::OperationFailure }}.to raise_error Mongoid::Token::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
|
+
document.stub(:is_duplicate_token_error?).and_return(false)
|
46
|
+
resolver.stub(:retry_count).and_return(3)
|
47
|
+
e = Mongo::Error::OperationFailure.new("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::Token::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
|
+
logger.stub("warn"){ |msg| message = msg }
|
66
|
+
Rails.stub("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) }.to raise_error(Mongoid::Token::CollisionRetriesExceeded)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "#is_duplicate_token_error?" do
|
81
|
+
before(:each) do
|
82
|
+
document.class.send(:include, Mongoid::Token::Collisions)
|
83
|
+
end
|
84
|
+
context "when there is a duplicate key error" do
|
85
|
+
it "should return true" do
|
86
|
+
document.stub("token").and_return("tokenvalue123")
|
87
|
+
err = double("Mongo::Error::OperationFailure")
|
88
|
+
err.stub("details").and_return do
|
89
|
+
{
|
90
|
+
"err" => "E11000 duplicate key error index: mongoid_token_test.links.$token_1 dup key: { : \"tokenvalue123\" }",
|
91
|
+
"code" => 11000,
|
92
|
+
"n" => 0,
|
93
|
+
"connectionId" => 130,
|
94
|
+
"ok" => 1.0
|
95
|
+
}
|
96
|
+
document.is_duplicate_token_error?(err, document, :token)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w[.. .. spec_helper])
|
2
|
+
|
3
|
+
describe Mongoid::Token::Finders do
|
4
|
+
after do
|
5
|
+
Object.send(:remove_const, :Document) if Object.constants.include?(:Document)
|
6
|
+
Object.send(:remove_const, :AnotherDocument) if Object.constants.include?(:AnotherDocument)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "define a finder based on a field_name" do
|
10
|
+
klass = Class.new
|
11
|
+
field = :another_token
|
12
|
+
Mongoid::Token::Finders.define_custom_token_finder_for(klass, field)
|
13
|
+
klass.singleton_methods.should include(:"find_by_#{field}")
|
14
|
+
end
|
15
|
+
|
16
|
+
it "retrieve a document using the dynamic finder" do
|
17
|
+
class Document; include Mongoid::Document; field :token; end
|
18
|
+
document = Document.create!(:token => "1234")
|
19
|
+
Mongoid::Token::Finders.define_custom_token_finder_for(Document)
|
20
|
+
Document.find_by_token("1234").should == document
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'retrieves multiple documents using the dynamic finder' do
|
24
|
+
class Document; include Mongoid::Document; field :token; end
|
25
|
+
document = Document.create!(:token => "1234")
|
26
|
+
document2 = Document.create!(:token => "5678")
|
27
|
+
Mongoid::Token::Finders.define_custom_token_finder_for(Document)
|
28
|
+
Document.find_by_token(["1234", "5678"]).should == [document, document2]
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w[.. .. spec_helper])
|
2
|
+
|
3
|
+
describe Mongoid::Token::Generator do
|
4
|
+
describe "#generate" do
|
5
|
+
it "generates lowercase characters" do
|
6
|
+
100.times{ Mongoid::Token::Generator.generate("%c").should =~ /[a-z]/ }
|
7
|
+
end
|
8
|
+
|
9
|
+
it "generates uppercase characters" do
|
10
|
+
100.times{ Mongoid::Token::Generator.generate("%C").should =~ /[A-Z]/ }
|
11
|
+
end
|
12
|
+
|
13
|
+
it "generates digits" do
|
14
|
+
100.times{ Mongoid::Token::Generator.generate("%d").should =~ /[0-9]/ }
|
15
|
+
end
|
16
|
+
|
17
|
+
it "generates non-zero digits" do
|
18
|
+
100.times{ Mongoid::Token::Generator.generate("%D").should =~ /[1-9]/ }
|
19
|
+
end
|
20
|
+
|
21
|
+
it "generates hexdigits" do
|
22
|
+
100.times{ Mongoid::Token::Generator.generate("%h").should =~ /[0-9a-f]/ }
|
23
|
+
end
|
24
|
+
|
25
|
+
it "generates non-zero hexdigits" do
|
26
|
+
100.times{ Mongoid::Token::Generator.generate("%H").should =~ /[1-9a-f]/ }
|
27
|
+
end
|
28
|
+
|
29
|
+
it "generates alphanumeric characters" do
|
30
|
+
100.times{ Mongoid::Token::Generator.generate("%s").should =~ /[A-Za-z0-9]/ }
|
31
|
+
end
|
32
|
+
|
33
|
+
it "generates upper and lowercase characters" do
|
34
|
+
100.times{ Mongoid::Token::Generator.generate("%w").should =~ /[A-Za-z]/ }
|
35
|
+
end
|
36
|
+
|
37
|
+
it "generates URL-safe punctuation" do
|
38
|
+
100.times{ Mongoid::Token::Generator.generate("%p").should =~ /[\.\-\_\=\+\$]/ }
|
39
|
+
end
|
40
|
+
|
41
|
+
it "generates patterns of a fixed length" do
|
42
|
+
100.times{ Mongoid::Token::Generator.generate("%s8").should =~ /[A-Za-z0-9]{8}/ }
|
43
|
+
end
|
44
|
+
|
45
|
+
it "generates patterns of a variable length" do
|
46
|
+
100.times{ Mongoid::Token::Generator.generate("%s1,5").should =~ /[A-Za-z0-9]{1,5}/ }
|
47
|
+
end
|
48
|
+
|
49
|
+
it "generates patterns with static prefixes/suffixes" do
|
50
|
+
100.times { Mongoid::Token::Generator.generate("prefix-%s4-suffix").should =~ /prefix\-[A-Za-z0-9]{4}\-suffix/ }
|
51
|
+
end
|
52
|
+
|
53
|
+
it "generates more complex patterns" do
|
54
|
+
100.times { Mongoid::Token::Generator.generate("pre-%d4-%C3-%d4").should =~ /pre\-[0-9]{4}\-[A-Z]{3}\-[0-9]{4}/ }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|