mongoid_token 0.9.1 → 1.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.
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