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.
@@ -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'
@@ -0,0 +1,3 @@
1
+ module MongoidToken
2
+ VERSION = "4.0.0"
3
+ end
@@ -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,4 @@
1
+ require File.join(File.dirname(__FILE__), %w[.. .. spec_helper])
2
+
3
+ describe Mongoid::Token::CollisionRetriesExceeded do
4
+ 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