mongoid_token 1.1.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.autotest CHANGED
@@ -1 +0,0 @@
1
- require 'autotest/growl'
data/README.md CHANGED
@@ -4,30 +4,22 @@
4
4
 
5
5
  This library is a quick and simple way to generate unique, random tokens
6
6
  for your mongoid documents, in the cases where you can't, or don't want
7
- to use slugs, or the default MongoDB IDs.
7
+ to use slugs, or the default MongoDB ObjectIDs.
8
8
 
9
9
  Mongoid::Token can help turn this:
10
10
 
11
- http://myawesomewebapp.com/video/4dcfbb3c6a4f1d4c4a000012/edit
11
+ http://bestappever.com/video/4dcfbb3c6a4f1d4c4a000012
12
12
 
13
13
  Into something more like this:
14
14
 
15
- http://myawesomewebapp.com/video/83xQ3r/edit
16
-
17
-
18
- ## Mongoid 3.x Support
19
-
20
- As of version 1.1.0, Mongoid::Token now supports Mongoid 3.x.
21
-
22
- > If you still require __Mongoid 2.x__ support, please install
23
- > Mongoid::Token 1.0.0.
15
+ http://bestappever.com/video/8tmQ9p
24
16
 
25
17
 
26
18
  ## Getting started
27
19
 
28
20
  In your gemfile, add:
29
21
 
30
- gem 'mongoid_token', '~> 1.1.0'
22
+ gem 'mongoid_token', '~> 2.0.0'
31
23
 
32
24
  Then update your bundle
33
25
 
@@ -39,102 +31,178 @@ In your Mongoid documents, just add `include Mongoid::Token` and the
39
31
  ````
40
32
  class Person
41
33
  include Mongoid::Document
42
- include Mongoid::Timestamps
43
34
  include Mongoid::Token
44
35
 
45
- field :first_name
46
- field :last_name
36
+ field :name
47
37
 
48
- token :length => 8
38
+ token
49
39
  end
50
40
 
51
41
  ````
52
42
 
53
- Obviously, this will create tokens of 8 characters long - make them as
54
- short or as long as you require.
43
+ And that's it! There's lots of configuration options too - which are all
44
+ listed [below](#configuration). By default, the `token` method will
45
+ create tokens 4 characters long, containing random alphanumeric characters.
55
46
 
56
47
  __Note:__ Mongoid::Token leverages Mongoid's 'safe mode' by
57
48
  automatically creating a unique index on your documents using the token
58
49
  field. In order to take advantage of this feature (and ensure that your
59
50
  documents always have unique tokens) remember to create your indexes.
60
51
 
61
- See 'Token collision/duplicate prevention' below for more details.
62
52
 
53
+ ## Finders
63
54
 
64
- ## Options
55
+ By default, the gem will override the existing `find` method in Mongoid,
56
+ in order to search for documents based on their token first (although
57
+ the default behaviour of ObjectIDs is also there). You can disable these
58
+ with the [`skip_finders` configuration option](#skip-finders-skip_finders).
65
59
 
66
- The `token` method has a couple of key options: `length`, which determines the
67
- length (or maximum length, in some cases) and `contains`, which tells
68
- Mongoid::Token which characters to use when generating the token.
60
+ ````
61
+ Video.find("x3v98")
62
+ Account.find("ACC-123456")
63
+ ````
69
64
 
70
- The options for `contains` are as follows:
71
65
 
72
- * `:alphanumeric` - letters (upper and lowercase) and numbers
73
- * `:alpha` - letters (upper and lowercase) only
74
- * `:alpha_lower` - letters (lowercase) only
75
- * `:alpha_upper` - letters (uppercase) only
76
- * `:numeric` - numbers only, anything from 1 character long, up to and
77
- `length`
78
- * `:fixed_numeric` - numbers only, but with the number of characters always the same as `length`
79
- * `:fixed_numeric_no_leading_zeros` - as above, but will never start with
80
- zeros
66
+ ## Configuration
81
67
 
82
- You can also rename the token field, if required, using the
83
- `:field_name` option:
68
+ ### Tokens
84
69
 
85
- * `token :contains => :numeric, :field_name => :purchase_order_number`
70
+ As of `Mongoid::Token` 2.0.0, you can now choose between two different
71
+ systems for managing how your tokens look.
86
72
 
87
- ### Examples:
73
+ For simple setup, you can use
74
+ combination of the [`length`](#length-length) and [`contains`](#contains-contains), which modify the length and
75
+ types of characters to use.
88
76
 
89
- * `token :length => 8, :contains => :alphanumeric` generates something like `8Sdc98dQ`
90
- * `token :length => 5, :contains => :alpha` gereates something like
91
- `ASlkj`
92
- * token :length
93
- * `token :length => 4, :contains => :numeric` could generate anything
94
- from `0` upto `9999` - but in a random order
95
- * `token :length => 4, :contains => :fixed_numeric` will generate
96
- anything from `0000` to `9999` in a random order
97
- * `token :length => 4, :contains => :fixed_numeric_no_leading_zeros` will
98
- generate anything from `1000` to `9999` in a random order
77
+ For when you need to generate more complex tokens, you can use the
78
+ [`pattern`](#pattern-pattern) option, which allows for very low-level control of the precise
79
+ structure of your tokens, as well as allowing for static strings, like
80
+ prefixes, infixes or suffixes.
99
81
 
82
+ #### Length (`:length`)
100
83
 
101
- ## Finders
84
+ This one is easy, it's just an integer.
102
85
 
103
- The library also contains a finder method for looking up your documents
104
- called `find_by_token`, e.g:
86
+ __Example:__
105
87
 
106
- Person.find_by_token('7dDn8q')
88
+ ````
89
+ token :length => 6 # Tokens are now of length 6
90
+ token :length => 12 # Whow, whow, whow. Slow down egghead.
91
+ ````
107
92
 
93
+ You get the idea.
108
94
 
109
- ## Adding tokens to existing documents
95
+ The only caveat here is that if used in combination with the
96
+ `:contains => :numeric` option, tokens may vary in length _up to_ the
97
+ specified length.
110
98
 
111
- If you've got an app with existing data that you would like to add
112
- tokens to - all you need to do is save each of your models and they will
113
- be assigned a token, if it's missing.
99
+ #### Contains (`:contains`)
114
100
 
101
+ Contains has 7 different options:
115
102
 
116
- ## Token collision/duplicate prevention
103
+ * `:alphanumeric` - contains uppercase & lowercase characters, as well
104
+ as numbers
105
+ * `:alpha` - contains only uppercase & lowercase characters
106
+ * `:alpha_upper` - contains only uppercase letters
107
+ * `:alpha_lower` - contains only lowercase letters
108
+ * `:numeric` - integer, length may be shorter than `:length`
109
+ * `:fixed_numeric` - integer, but will always be of length `:length`
110
+ * `:fixed_numeric_no_leading_zeros` - same as `:fixed_numeric`, but will
111
+ never start with zeros
117
112
 
118
- Mongoid::Token leverages Mongoid's 'safe mode' by
119
- automatically creating a unique index on your documents using the token
120
- field. In order to take advantage of this feature (and ensure that your
121
- documents always have unique tokens) remember to create your indexes.
113
+ __Examples:__
114
+ ````
115
+ token :contains => :alpha_upper, :length => 8
116
+ token :contains => :fixed_numeric
117
+ ````
118
+
119
+ #### Patterns (`:pattern`)
120
+
121
+ New in 2.0.0, patterns allow you find-grained control over how your
122
+ tokens look. It's great for generating random data that has a
123
+ requirements to also have some basic structure. If you use the
124
+ `:pattern` option, it will override both the `:length` and `:contains`
125
+ options.
126
+
127
+ This was designed to operate in a similar way to something like `strftime`,
128
+ if the syntax offends you - please open an issue, I'd love to get some
129
+ feedback here and better refine how these are generated.
130
+
131
+ Any characters in the string are treated as static, except those that are
132
+ proceeded by a `%`. Those special characters represent a single, randomly
133
+ generated character, and are as follows:
134
+
135
+ * `%s` - any uppercase, lowercase, or numeric character
136
+ * `%w` - any uppercase, or lowercase character
137
+ * `%c` - any lowercase character
138
+ * `%C` - any uppercase character
139
+ * `%d` - any digit
140
+ * `%D` - any non-zero digit
141
+
142
+ __Example:__
143
+
144
+ ````
145
+ token :pattern => "PRE-%C%C-%d%d%d%d" # Generates something like: 'PRE-ND-3485'
146
+ ````
122
147
 
123
- You can read more about indexes in the [Mongoid docs](http://mongoid.org/docs/indexing.html).
148
+ You can also add a repetition modifier, which can help improve readability on
149
+ more complex patterns. You simply add any integer after the letter.
124
150
 
125
- Additionally, Mongoid::Token will attempt to create a token 3 times
126
- before eventually giving up and raising a
127
- `Mongoid::Token::CollisionRetriesExceeded` exception. To take advantage
128
- of this, one must set `persist_in_safe_mode = true` in your Mongoid
129
- configuration.
151
+ __Examples:__
130
152
 
131
- The number of retry attempts is adjustable in the `token` method using the
132
- `:retry` options. Set it to zero to turn off retrying.
153
+ ````
154
+ token :sku => "APP-%d6" # Generates something like; "APP-638924"
155
+ ````
156
+
157
+ ### Field Name (`:field_name`)
158
+
159
+ This allows you to change the field name used by `Mongoid::Token`
160
+ (default is `:token`). This is particularly handy when you're wanting to
161
+ use multiple tokens one a single document.
162
+
163
+ __Examples:__
164
+ ````
165
+ token :length => 6
166
+ token :field_name => :sharing_token, :length => 12
167
+ token :field_name => :yet_another
168
+ ````
169
+
170
+
171
+ ### Skip Finders (`:skip_finders`)
172
+
173
+ This will prevent the gem from creating the customised finders and
174
+ overrides for the default `find` behaviour used by Mongoid.
175
+
176
+ __Example:__
177
+ ````
178
+ token :skip_finders => true
179
+ ````
180
+
181
+
182
+ ### Override to_param (`:override_to_param`)
183
+
184
+ By default, `Mongoid::Token` will override to_param, to make it an easy
185
+ drop-in replacement for the default ObjectIDs. If needed, you can turn
186
+ this behaviour off:
187
+
188
+ __Example:__
189
+ ````
190
+ token :override_to_param => false
191
+ ````
192
+
193
+
194
+ ### Retry Count (`:retry_count`)
133
195
 
134
- * `token :length => 6, :retry => 4` Will retry token generation 4
135
- times before bailing out
136
- * `token :length => 3, :retry => 0` Retrying disabled
196
+ In the event of a token collision, this gem will attempt to try three
197
+ more times before raising a `Mongoid::Token::CollisionRetriesExceeded`
198
+ error. If you're wanting it to try harder, or less hard, then this
199
+ option is for you.
137
200
 
201
+ __Examples:__
202
+ ````
203
+ token :retry_count => 9
204
+ token :retry_count => 0
205
+ ````
138
206
 
139
207
  # Notes
140
208
 
@@ -145,11 +213,13 @@ greatly appreciated.
145
213
  Thanks to everyone that has contributed to this gem over the past year.
146
214
  Many, many thanks - you guys rawk.
147
215
 
216
+
148
217
  ## Contributors:
149
218
 
150
- * [olliem](https://github.com/olliem)
151
- * [msolli](https://github.com/msolli)
152
- * [siong1987](https://github.com/siong1987)
153
- * [stephan778](https://github.com/stephan778)
154
- * [eagleas](https://github.com/eagleas)
155
- * [jamesotron](https://github.com/jamesotron).
219
+ Thanks to everyone who has provided support for this gem over the years.
220
+ In particular: [olliem](https://github.com/olliem),
221
+ [msolli](https://github.com/msolli),
222
+ [siong1987](https://github.com/siong1987),
223
+ [stephan778](https://github.com/stephan778),
224
+ [eagleas](https://github.com/eagleas), and
225
+ [jamesotron](https://github.com/jamesotron).
@@ -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.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,31 @@
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 Moped::Errors::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
+ end
16
+ end
17
+ end
18
+
19
+ def raise_collision_retries_exceeded_error(field_name, retry_count)
20
+ Rails.logger.warn "[Mongoid::Token] Warning: Maximum token generation retries (#{retry_count}) exceeded on `#{field_name}'." if defined?(Rails)
21
+ raise Mongoid::Token::CollisionRetriesExceeded.new(self, retry_count)
22
+ end
23
+
24
+ def is_duplicate_token_error?(err, document, field_name)
25
+ [11000, 11001].include?(err.details['code']) &&
26
+ err.details['err'] =~ /dup key/ &&
27
+ err.details['err'] =~ /"#{document.send(field_name)}"/
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,19 @@
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.to_s}") do |token|
6
+ self.find_by(field_name.to_sym => token)
7
+ end
8
+
9
+ klass.define_singleton_method :"find_with_#{field_name}" do |*args| # this is going to be painful if tokens happen to look like legal object ids
10
+ args.all?{|arg| Moped::BSON::ObjectId.legal?(arg)} ? send(:"find_without_#{field_name}",*args) : klass.send(:"find_by_#{field_name.to_s}", args.first)
11
+ end
12
+
13
+ # this craziness taken from, and then compacted into a string class_eval
14
+ # http://geoffgarside.co.uk/2007/02/19/activesupport-alias-method-chain-modules-and-class-methods/
15
+ klass.class_eval("class << self; alias_method_chain :find, :#{field_name} if self.method_defined?(:find); end")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,72 @@
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>[cCdDpsw]{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 's'
33
+ alphanumeric(length)
34
+ when 'w'
35
+ alpha(length)
36
+ when 'p'
37
+ "-"
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+ def self.down_character(length = 1)
44
+ Array.new(length).map{['a'..'z'].map{|r|r.to_a}.flatten[rand(26)]}.join
45
+ end
46
+
47
+ def self.up_character(length = 1)
48
+ Array.new(length).map{['A'..'Z'].map{|r|r.to_a}.flatten[rand(26)]}.join
49
+ end
50
+
51
+ def self.integer(length = 1)
52
+ (rand(10**length - 10**(length-1)) + 10**(length-1)).to_s
53
+ end
54
+
55
+ def self.digits(length = 1)
56
+ rand(10**length).to_s.rjust(length, "0")
57
+ end
58
+
59
+ def self.alpha(length = 1)
60
+ Array.new(length).map{['A'..'Z','a'..'z'].map{|r|r.to_a}.flatten[rand(52)]}.join
61
+ end
62
+
63
+ def self.alphanumeric(length = 1)
64
+ (1..length).collect { (i = Kernel.rand(62); i += ((i < 10) ? 48 : ((i < 36) ? 55 : 61 ))).chr }.join
65
+ end
66
+
67
+ def self.punctuation(length = 1)
68
+ Array.new(length).map{['.','-','_','=','+','$'].map{|r|r.to_a}.flatten[rand(52)]}.join
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,68 @@
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[:field_name]
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 pattern
31
+ @options[:pattern] ||= case @options[:contains].to_sym
32
+ when :alphanumeric
33
+ "%s#{@options[:length]}"
34
+ when :alpha
35
+ "%w#{@options[:length]}"
36
+ when :alpha_upper
37
+ "%C#{@options[:length]}"
38
+ when :alpha_lower
39
+ "%c#{@options[:length]}"
40
+ when :numeric
41
+ "%d1,#{@options[:length]}"
42
+ when :fixed_numeric
43
+ "%d#{@options[:length]}"
44
+ when :fixed_numeric_no_leading_zeros
45
+ "%D#{@options[:length]}"
46
+ end
47
+ end
48
+
49
+ private
50
+ def validate_options(options)
51
+ if options.has_key?(:retry)
52
+ STDERR.puts "Mongoid::Token Deprecation Warning: option `retry` has been renamed to `retry_count`. `:retry` will be removed in v2.1"
53
+ options[:retry_count] = options[:retry]
54
+ end
55
+ options
56
+ end
57
+
58
+ def merge_defaults(options)
59
+ {
60
+ :length => 4,
61
+ :retry_count => 3,
62
+ :contains => :alphanumeric,
63
+ :field_name => :token,
64
+ :skip_finders => false,
65
+ :override_to_param => true,
66
+ }.merge(options)
67
+ end
68
+ end
@@ -0,0 +1,63 @@
1
+ require 'mongoid/token/exceptions'
2
+ require 'mongoid/token/options'
3
+ require 'mongoid/token/generator'
4
+ require 'mongoid/token/finders'
5
+ require 'mongoid/token/collision_resolver'
6
+
7
+ module Mongoid
8
+ module Token
9
+ extend ActiveSupport::Concern
10
+
11
+ module ClassMethods
12
+ def initialize_copy(source)
13
+ super
14
+ self.token = nil
15
+ end
16
+
17
+ def token(*args)
18
+ options = Mongoid::Token::Options.new(args.extract_options!)
19
+
20
+ self.field options.field_name, :type => String, :default => nil
21
+ self.index({ options.field_name => 1 }, { :unique => true })
22
+
23
+ resolver = Mongoid::Token::CollisionResolver.new(self, options.field_name, options.retry_count)
24
+ resolver.create_new_token = Proc.new do |document|
25
+ document.send(:create_token, options.field_name, options.pattern)
26
+ end
27
+
28
+ if options.skip_finders? == false
29
+ Finders.define_custom_token_finder_for(self, options.field_name)
30
+ end
31
+
32
+ set_callback(:create, :before) do |document|
33
+ document.create_token options.field_name, options.pattern
34
+ end
35
+
36
+ set_callback(:save, :before) do |document|
37
+ document.create_token_if_nil options.field_name, options.pattern
38
+ end
39
+
40
+ if options.override_to_param?
41
+ self.define_method :to_param do
42
+ self.send(options.field_name) || super
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ protected
49
+ def create_token(field_name, pattern)
50
+ self.send :"#{field_name.to_s}=", self.generate_token(pattern)
51
+ end
52
+
53
+ def create_token_if_nil(field_name, pattern)
54
+ if self[field_name.to_sym].blank?
55
+ self.create_token field_name, pattern
56
+ end
57
+ end
58
+
59
+ def generate_token(pattern)
60
+ Mongoid::Token::Generator.generate pattern
61
+ end
62
+ end
63
+ end