mongoid_token 0.9.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -2,3 +2,4 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
+ .DS_Store
data/Gemfile CHANGED
@@ -4,11 +4,12 @@ source "http://rubygems.org"
4
4
  gemspec
5
5
 
6
6
  group :test do
7
+ gem 'activesupport', '~> 3.0'
7
8
  gem 'database_cleaner'
8
- gem 'rspec'
9
+ gem 'rspec', '2.5.0'
9
10
  gem 'autotest'
10
11
  gem 'autotest-growl'
11
- gem 'mongoid', '~> 2.0'
12
+ gem 'mongoid', '2.0.0'
12
13
  gem 'bson_ext'
13
14
  gem 'mongoid-rspec'
14
15
  end
data/README.md CHANGED
@@ -17,7 +17,7 @@ Into something more like this:
17
17
 
18
18
  In your gemfile, add:
19
19
 
20
- gem 'mongoid_token', '~. 0.9.0'
20
+ gem 'mongoid_token', '~> 1.0.0'
21
21
 
22
22
  Then update your bundle
23
23
 
@@ -43,10 +43,17 @@ end
43
43
  Obviously, this will create tokens of 8 characters long - make them as
44
44
  short or as long as you require.
45
45
 
46
+ __Note:__ Mongoid::Token leverages Mongoid's 'safe mode' by
47
+ automatically creating a unique index on your documents using the token
48
+ field. In order to take advantage of this feature (and ensure that your
49
+ documents always have unique tokens) remember to create your indexes.
50
+
51
+ See 'Token collision/duplicate prevention' below for more details.
52
+
46
53
 
47
54
  ## Options
48
55
 
49
- The `token` method takes two options: `length`, which determines the
56
+ The `token` method has a couple of key options: `length`, which determines the
50
57
  length (or maximum length, in some cases) and `contains`, which tells
51
58
  Mongoid::Token which characters to use when generating the token.
52
59
 
@@ -58,6 +65,11 @@ The options for `contains` are as follows:
58
65
  `length`
59
66
  * `:fixed_numeric` - numbers only, but with the number of characters always the same as `length`
60
67
 
68
+ You can also rename the token field, if required, using the
69
+ `:field_name` option:
70
+
71
+ * `token :contains => :numeric, :field_name => :purchase_order_number`
72
+
61
73
  ### Examples:
62
74
 
63
75
  * `token :length => 8, :contains => :alphanumeric` generates something like `8Sdc98dQ`
@@ -84,7 +96,34 @@ tokens to - all you need to do is save each of your models and they will
84
96
  be assigned a token, if it's missing.
85
97
 
86
98
 
99
+ ## Token collision/duplicate prevention
100
+
101
+ Mongoid::Token leverages Mongoid's 'safe mode' by
102
+ automatically creating a unique index on your documents using the token
103
+ field. In order to take advantage of this feature (and ensure that your
104
+ documents always have unique tokens) remember to create your indexes.
105
+
106
+ You can read more about indexes in the [Mongoid docs](http://mongoid.org/docs/indexing.html).
107
+
108
+ Additionally, Mongoid::Token will attempt to create a token 3 times
109
+ before eventually giving up and raising a
110
+ `Mongoid::Token::CollisionRetriesExceeded` exception. To take advantage
111
+ of this, one must set `persist_in_safe_mode = true` in your Mongoid
112
+ configuration.
113
+
114
+ The number of retry attempts is adjustable in the `token` method using the
115
+ `:retry` options. Set it to zero to turn off retrying.
116
+
117
+ * `token :length => 6, :retry => 4` Will retry token generation 4
118
+ times before bailing out
119
+ * `token :length => 3, :retry => 0` Retrying disabled
120
+
121
+
87
122
  # Notes
88
123
 
89
124
  If you find a problem, please [submit an issue](http://github.com/thetron/mongoid_token/issues) (and a failing test, if
90
- you can). Pull requests and feature requests are always welcome.
125
+ you can). Pull requests and feature requests are always welcome and
126
+ greatly appreciated.
127
+
128
+ Thanks to everyone that has contributed to this gem over the past year,
129
+ in particular [eagleas](https://github.com/eagleas) and [jamesotron](https://github.com/jamesotron). Many, many thanks guys.
@@ -0,0 +1,50 @@
1
+ $: << File.expand_path("../../lib", __FILE__)
2
+
3
+ require 'database_cleaner'
4
+ require 'mongoid'
5
+ require 'mongoid-rspec'
6
+ require 'mongoid_token'
7
+ require 'benchmark'
8
+
9
+ Mongoid.configure do |config|
10
+ config.master = Mongo::Connection.new.db("mongoid_token_benchmark")
11
+ end
12
+
13
+ DatabaseCleaner.strategy = :truncation
14
+
15
+ # start benchmarks
16
+
17
+ token_length = 2
18
+
19
+ class Link
20
+ include Mongoid::Document
21
+ include Mongoid::Token
22
+ field :url
23
+ token :length => 2, :contains => :alphanumeric
24
+ end
25
+
26
+ class NoTokenLink
27
+ include Mongoid::Document
28
+ field :url
29
+ end
30
+
31
+ def create_link(token = true)
32
+ if token
33
+ Link.create(:url => "http://involved.com.au")
34
+ else
35
+ NoTokenLink.create(:url => "http://involved.com.au")
36
+ end
37
+ end
38
+
39
+ Link.destroy_all
40
+ Link.create_indexes
41
+ num_records = [1, 50, 100, 1000, 2000, 3000, 4000]
42
+ puts "-- Alphanumeric token of length #{token_length} (#{62**token_length} possible tokens)"
43
+ Benchmark.bm do |b|
44
+ num_records.each do |qty|
45
+ b.report("#{qty.to_s.rjust(5, " ")} records "){ qty.times{ create_link(false) } }
46
+ b.report("#{qty.to_s.rjust(5, " ")} records tok"){ qty.times{ create_link } }
47
+ Link.destroy_all
48
+ end
49
+ end
50
+
@@ -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
data/lib/mongoid_token.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'mongoid/token/exceptions'
2
+
1
3
  module Mongoid
2
4
  module Token
3
5
  extend ActiveSupport::Concern
@@ -6,9 +8,13 @@ module Mongoid
6
8
  def token(*args)
7
9
  options = args.extract_options!
8
10
  options[:length] ||= 4
11
+ options[:retry] ||= 3
9
12
  options[:contains] ||= :alphanumeric
13
+ options[:field_name] ||= :token
10
14
 
11
- self.field :token, :type => String
15
+ self.field options[:field_name].to_sym, :type => String
16
+ self.index options[:field_name].to_sym, :unique => true
17
+ # should validate token presence? Should this be enforced?
12
18
 
13
19
  set_callback(:create, :before) do |document|
14
20
  document.create_token(options[:length], options[:contains])
@@ -17,41 +23,85 @@ module Mongoid
17
23
  set_callback(:save, :before) do |document|
18
24
  document.create_token_if_nil(options[:length], options[:contains])
19
25
  end
26
+
27
+ after_initialize do # set_callback did not work with after_initialize callback
28
+ self.instance_variable_set :@max_collision_retries, options[:retry]
29
+ self.instance_variable_set :@token_field_name, options[:field_name]
30
+ self.instance_variable_set :@token_length, options[:length]
31
+ self.instance_variable_set :@token_contains, options[:contains]
32
+ end
33
+
34
+ if options[:retry] > 0
35
+ alias_method_chain :save, :safety
36
+ alias_method_chain :save!, :safety
37
+ end
38
+
39
+ self.class_variable_set :@@token_field_name, options[:field_name]
20
40
  end
21
41
 
22
42
  def find_by_token(token)
23
- self.first(:conditions => {:token => token})
43
+ field_name = self.class_variable_get :@@token_field_name
44
+ self.first(:conditions => {field_name.to_sym => token})
24
45
  end
25
46
  end
26
47
 
27
- module InstanceMethods
28
- def to_param
29
- self.token
30
- end
31
-
32
- protected
33
- def create_token(length, characters)
34
- self.token = self.generate_token(length, characters) while self.token.nil? || self.class.exists?(:conditions => {:token => self.token})
35
- end
48
+ def to_param
49
+ self.send(@token_field_name.to_sym)
50
+ end
36
51
 
37
- def create_token_if_nil(length, characters)
38
- self.create_token(length, characters) if self.token.nil?
52
+ protected
53
+ def save_with_safety(args = {}, &block)
54
+ retries = @max_collision_retries
55
+ begin
56
+ # puts "Attempt: #{retries}"
57
+ safely.save_without_safety(args, &block)
58
+ rescue Mongo::OperationFailure => e
59
+ if (retries -= 1) > 0
60
+ self.create_token(@token_length, @token_contains)
61
+ retry
62
+ else
63
+ Rails.logger.warn "[Mongoid::Token] Warning: Maximum to generation retries (#{@max_collision_retries}) exceeded." if defined?(Rails) && Rails.env == 'development'
64
+ raise Mongoid::Token::CollisionRetriesExceeded.new(self, @max_collision_retries)
65
+ end
39
66
  end
67
+ end
40
68
 
41
- def generate_token(length, characters)
42
- case characters
43
- when :alphanumeric
44
- ActiveSupport::SecureRandom.hex(length)[0...length]
45
- when :numeric
46
- rand(10**length).to_s
47
- when :fixed_numeric
48
- rand(10**length).to_s.rjust(length,rand(10).to_s)
49
- when :alpha
50
- Array.new(length).map{['A'..'Z','a'..'z'].map{|r|r.to_a}.flatten[rand(52)]}.join
69
+ def save_with_safety!(args = {}, &block)
70
+ retries = @max_collision_retries
71
+ begin
72
+ #puts "Attempt: #{retries}"
73
+ safely.save_without_safety!(args, &block)
74
+ rescue Mongo::OperationFailure => e
75
+ if (retries -= 1) > 0
76
+ self.create_token(@token_length, @token_contains)
77
+ retry
51
78
  else
52
- ActiveSupport::SecureRandom.hex(length)[0...length]
79
+ Rails.logger.warn "[Mongoid::Token] Warning: Maximum to generation retries (#{@max_collision_retries}) exceeded." if defined?(Rails) && Rails.env == 'development'
80
+ raise Mongoid::Token::CollisionRetriesExceeded.new(self, @max_collision_retries)
53
81
  end
54
82
  end
55
83
  end
84
+
85
+ def create_token(length, characters)
86
+ self.send(:"#{@token_field_name}=", self.generate_token(length, characters))
87
+ #puts "Set #{@token_field_name.to_s} to #{self.send(@token_field_name.to_sym)}"
88
+ end
89
+
90
+ def create_token_if_nil(length, characters)
91
+ self.create_token(length, characters) if self[@token_field_name.to_sym].nil?
92
+ end
93
+
94
+ def generate_token(length, characters = :alphanumeric)
95
+ case characters
96
+ when :alphanumeric
97
+ (1..length).collect { (i = Kernel.rand(62); i += ((i < 10) ? 48 : ((i < 36) ? 55 : 61 ))).chr }.join
98
+ when :numeric
99
+ rand(10**length).to_s
100
+ when :fixed_numeric
101
+ rand(10**length).to_s.rjust(length,rand(10).to_s)
102
+ when :alpha
103
+ Array.new(length).map{['A'..'Z','a'..'z'].map{|r|r.to_a}.flatten[rand(52)]}.join
104
+ end
105
+ end
56
106
  end
57
107
  end
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module MongoidToken
2
- VERSION = "0.9.1"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -10,10 +10,10 @@ Gem::Specification.new do |s|
10
10
  s.email = ["nicholas@bruning.com.au"]
11
11
  s.homepage = "http://github.com/thetron/mongoid_token"
12
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, when you want shorter URLs.}
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
14
 
15
15
  s.rubyforge_project = "mongoid_token"
16
- s.add_dependency 'activesupport', '>= 3.0.0'
16
+ s.add_dependency 'activesupport', '>= 3.0'
17
17
 
18
18
  s.files = `git ls-files`.split("\n")
19
19
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -22,12 +22,37 @@ class Link
22
22
  token :length => 3, :contains => :alphanumeric
23
23
  end
24
24
 
25
+ class FailLink
26
+ include Mongoid::Document
27
+ include Mongoid::Token
28
+ field :url
29
+ token :length => 3, :contains => :alphanumeric, :retry => 0
30
+ end
31
+
25
32
  class Video
26
33
  include Mongoid::Document
27
34
  include Mongoid::Token
28
35
 
29
36
  field :name
30
- token :length => 8, :contains => :alpha
37
+ token :length => 8, :contains => :alpha, :field_name => :vid
38
+ end
39
+
40
+ class Node
41
+ include Mongoid::Document
42
+ include Mongoid::Token
43
+
44
+ field :name
45
+ token :length => 8, :contains => :fixed_numeric
46
+
47
+ embedded_in :cluster
48
+ end
49
+
50
+ class Cluster
51
+ include Mongoid::Document
52
+
53
+ field :name
54
+
55
+ embeds_many :nodes
31
56
  end
32
57
 
33
58
  describe Mongoid::Token do
@@ -35,21 +60,28 @@ describe Mongoid::Token do
35
60
  @account = Account.create(:name => "Involved Pty. Ltd.")
36
61
  @link = Link.create(:url => "http://involved.com.au")
37
62
  @video = Video.create(:name => "Nyan nyan")
63
+
64
+ Account.create_indexes
65
+ Link.create_indexes
66
+ FailLink.create_indexes
67
+ Video.create_indexes
68
+ Node.create_indexes
38
69
  end
39
70
 
40
71
  it "should have a token field" do
41
72
  @account.attributes.include?('token').should == true
42
73
  @link.attributes.include?('token').should == true
43
- @video.attributes.include?('token').should == true
74
+ @video.attributes.include?('vid').should == true
44
75
  end
45
76
 
46
77
  it "should have a token of correct length" do
47
78
  @account.token.length.should == 16
48
79
  @link.token.length.should == 3
49
- @video.token.length.should == 8
80
+ @video.vid.length.should == 8
50
81
  end
51
82
 
52
83
  it "should only generate unique tokens" do
84
+ Link.create_indexes
53
85
  1000.times do
54
86
  @link = Link.create(:url => "http://involved.com.au")
55
87
  Link.count(:conditions => {:token => @link.token}).should == 1
@@ -70,9 +102,9 @@ describe Mongoid::Token do
70
102
  @link.token.gsub(/[A-Za-z0-9]/, "").length.should == 0
71
103
  end
72
104
 
73
- 50.times do
105
+ 50.times do |index|
74
106
  @video = Video.create(:name => "A test video")
75
- @video.token.gsub(/[A-Za-z]/, "").length.should == 0
107
+ @video.vid.gsub(/[A-Za-z]/, "").length.should == 0
76
108
  end
77
109
  end
78
110
 
@@ -89,16 +121,22 @@ describe Mongoid::Token do
89
121
  it "should return the token as its parameter" do
90
122
  @account.to_param.should == @account.token
91
123
  @link.to_param.should == @link.token
92
- @video.to_param.should == @video.token
124
+ @video.to_param.should == @video.vid
93
125
  end
94
126
 
95
127
 
96
- it "should be finable by token" do
128
+ it "should be findable by token" do
97
129
  50.times do |index|
98
130
  Account.create(:name => "A random company #{index}")
99
131
  end
100
132
  Account.find_by_token(@account.token).id.should == @account.id
101
133
  Account.find_by_token(Account.last.token).id.should == Account.last.id
134
+
135
+ 10.times do |index|
136
+ Video.create(:name => "Lord of the Rings, Super Special Edition part #{index}")
137
+ end
138
+ Video.find_by_token(@video.vid).id.should == @video.id
139
+ Video.find_by_token(Video.last.vid).id.should == Video.last.id
102
140
  end
103
141
 
104
142
  it "should create a token, if the token is missing" do
@@ -106,4 +144,46 @@ describe Mongoid::Token do
106
144
  @account.save!
107
145
  @account.token.should_not be_nil
108
146
  end
147
+
148
+ it "should fail with an exception after 3 retries (by default)" do
149
+ Link.destroy_all
150
+ Link.create_indexes
151
+
152
+ @first_link = Link.create(:url => "http://involved.com.au")
153
+ @link = Link.new(:url => "http://fail.com")
154
+ def @link.create_token(l,c) # override to always generate a duplicate
155
+ super
156
+ self.token = Link.first.token
157
+ end
158
+
159
+ lambda{ @link.save }.should raise_error(Mongoid::Token::CollisionRetriesExceeded)
160
+ Link.count.should == 1
161
+ Link.where(:token => @first_link.token).count.should == 1
162
+ end
163
+
164
+ it "should not raise a custom exception if retries are set to zero" do
165
+ FailLink.destroy_all
166
+ FailLink.create_indexes
167
+
168
+ @first_link = FailLink.create(:url => "http://involved.com.au")
169
+ @link = FailLink.new(:url => "http://fail.com")
170
+ def @link.create_token(l,c) # override to always generate a duplicate
171
+ super
172
+ self.token = FailLink.first.token
173
+ end
174
+
175
+ lambda{ @link.save }.should_not raise_error(Mongoid::Token::CollisionRetriesExceeded)
176
+ end
177
+
178
+ it "should create unique indexes on embedded documents" do
179
+ @cluster = Cluster.new(:name => "CLUSTER_001")
180
+ 5.times do |index|
181
+ @cluster.nodes.create!(:name => "NODE_#{index.to_s.rjust(3, '0')}")
182
+ end
183
+
184
+ @cluster.nodes.each do |node|
185
+ node.attributes.include?('token').should == true
186
+ node.token.match(/[0-9]{8}/).should_not == nil
187
+ end
188
+ end
109
189
  end
data/spec/spec_helper.rb CHANGED
@@ -4,6 +4,7 @@ require 'database_cleaner'
4
4
  require 'mongoid'
5
5
  require 'mongoid-rspec'
6
6
  require 'mongoid_token'
7
+ require 'mongoid/token/exceptions'
7
8
 
8
9
  RSpec.configure do |config|
9
10
  config.include Mongoid::Matchers
@@ -13,10 +14,18 @@ RSpec.configure do |config|
13
14
 
14
15
  config.after(:each) do
15
16
  DatabaseCleaner.clean
17
+
18
+ # Added dropping collection to ensure indexes are removed
19
+ Mongoid.master.collections.select do |collection|
20
+ include = collection.name !~ /system/
21
+ include
22
+ end.each(&:drop)
16
23
  end
17
24
  end
18
25
 
19
26
  Mongoid.configure do |config|
20
27
  config.master = Mongo::Connection.new.db("mongoid_token_test")
28
+ config.autocreate_indexes = true
29
+ config.persist_in_safe_mode = true
21
30
  end
22
31
 
metadata CHANGED
@@ -1,78 +1,72 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: mongoid_token
3
- version: !ruby/object:Gem::Version
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
4
5
  prerelease:
5
- version: 0.9.1
6
6
  platform: ruby
7
- authors:
7
+ authors:
8
8
  - Nicholas Bruning
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
-
13
- date: 2011-06-04 00:00:00 Z
14
- dependencies:
15
- - !ruby/object:Gem::Dependency
12
+ date: 2012-02-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
16
15
  name: activesupport
17
- prerelease: false
18
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: &2162530960 !ruby/object:Gem::Requirement
19
17
  none: false
20
- requirements:
21
- - - ">="
22
- - !ruby/object:Gem::Version
23
- version: 3.0.0
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
24
22
  type: :runtime
25
- version_requirements: *id001
26
- description: Mongoid token is a gem for creating random, unique tokens for mongoid documents, when you want shorter URLs.
27
- email:
23
+ prerelease: false
24
+ version_requirements: *2162530960
25
+ description: Mongoid token is a gem for creating random, unique tokens for mongoid
26
+ documents. Highly configurable and great for making URLs a little more compact.
27
+ email:
28
28
  - nicholas@bruning.com.au
29
29
  executables: []
30
-
31
30
  extensions: []
32
-
33
31
  extra_rdoc_files: []
34
-
35
- files:
32
+ files:
36
33
  - .autotest
37
34
  - .gitignore
38
35
  - .rspec
39
36
  - Gemfile
40
37
  - README.md
41
38
  - Rakefile
42
- - lib/.DS_Store
39
+ - benchmarks/benchmark.rb
40
+ - lib/mongoid/token/exceptions.rb
43
41
  - lib/mongoid_token.rb
44
42
  - lib/version.rb
45
43
  - mongoid_token.gemspec
46
- - spec/.DS_Store
47
44
  - spec/mongoid/token_spec.rb
48
45
  - spec/spec_helper.rb
49
46
  homepage: http://github.com/thetron/mongoid_token
50
47
  licenses: []
51
-
52
48
  post_install_message:
53
49
  rdoc_options: []
54
-
55
- require_paths:
50
+ require_paths:
56
51
  - lib
57
- required_ruby_version: !ruby/object:Gem::Requirement
52
+ required_ruby_version: !ruby/object:Gem::Requirement
58
53
  none: false
59
- requirements:
60
- - - ">="
61
- - !ruby/object:Gem::Version
62
- version: "0"
63
- required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
59
  none: false
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: "0"
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
69
64
  requirements: []
70
-
71
65
  rubyforge_project: mongoid_token
72
- rubygems_version: 1.8.3
66
+ rubygems_version: 1.8.7
73
67
  signing_key:
74
68
  specification_version: 3
75
69
  summary: A little random, unique token generator for Mongoid documents.
76
- test_files:
70
+ test_files:
77
71
  - spec/mongoid/token_spec.rb
78
72
  - spec/spec_helper.rb
data/lib/.DS_Store DELETED
Binary file
data/spec/.DS_Store DELETED
Binary file